Files
yml2213 0d3543fd4b 部署默认端口改为 18080,支持与宿主机 Caddy 共存
避免与已有 80/443 服务冲突;补充端口检测、Caddyfile 示例与文档说明。
2026-07-15 15:38:07 +08:00

321 lines
9.6 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 客服云 — 腾讯云 / 任意 Linux 一键生产部署
# 默认 HTTP_PORT=18080,避免与宿主机 Caddy(80/443) 冲突;见 deploy/caddy/Caddyfile.example
# 用法:
# ./deploy/scripts/deploy.sh # 构建并启动
# ./deploy/scripts/deploy.sh --seed # 启动后导入演示账号数据
# ./deploy/scripts/deploy.sh --pull-only # 仅 pull/up,不强制 rebuild(需已有镜像)
# ./deploy/scripts/deploy.sh --down # 停止并移除容器(保留数据卷)
# ./deploy/scripts/deploy.sh --status # 查看状态
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
COMPOSE_FILE="docker-compose.prod.yml"
ENV_FILE="deploy/.env.prod"
ENV_EXAMPLE="deploy/.env.prod.example"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[deploy]${NC} $*"; }
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
err() { echo -e "${RED}[error]${NC} $*" >&2; }
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
err "缺少命令: $1"
exit 1
fi
}
compose() {
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@"
}
rand_hex() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 24
else
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c 48
fi
}
# 检测宿主机 TCP 端口是否已被占用(非本 compose 的 web 容器)
port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -tlnH "sport = :${port}" 2>/dev/null | grep -q .
return $?
fi
if command -v lsof >/dev/null 2>&1; then
lsof -iTCP:"${port}" -sTCP:LISTEN -n -P >/dev/null 2>&1
return $?
fi
# 无 ss/lsof 时用 bash /dev/tcp 探测(仅作弱检测)
(echo >/dev/tcp/127.0.0.1/"${port}") >/dev/null 2>&1
}
# 端口是否由本项目的 web 容器占用(升级/重部署时允许)
port_held_by_our_web() {
local port="$1"
docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null \
| grep -E 'kefu-cloud-web' \
| grep -qE "0\\.0\\.0\\.0:${port}->|:::${port}->"
}
check_http_port() {
local port="${HTTP_PORT:-18080}"
if ! [[ "$port" =~ ^[0-9]+$ ]] || [[ "$port" -lt 1 || "$port" -gt 65535 ]]; then
err "HTTP_PORT 无效: ${port}"
exit 1
fi
if [[ "$port" == "80" ]] || [[ "$port" == "443" ]]; then
if port_in_use "$port" && ! port_held_by_our_web "$port"; then
err "HTTP_PORT=${port} 已被占用(常见:宿主机 Caddy/Nginx"
err "请将 $ENV_FILE 中 HTTP_PORT 改为 18080,并由 Caddy 反代到该端口"
err "参考: deploy/caddy/Caddyfile.example 与 deploy/README.md「与现有 Caddy 共存」"
exit 1
fi
elif port_in_use "$port" && ! port_held_by_our_web "$port"; then
err "HTTP_PORT=${port} 已被其它进程占用,请改 $ENV_FILE 中的 HTTP_PORT"
if command -v ss >/dev/null 2>&1; then
ss -tlnp 2>/dev/null | grep -E ":${port}\\s" || true
fi
exit 1
fi
# 80/443 被占用但用户选了 18080 时给提示
if [[ "$port" != "80" ]] && port_in_use 80; then
log "检测到宿主机 :80 已被占用(多为 Caddy),将使用 HTTP_PORT=${port}"
log "部署后请把 Caddy 反代到 127.0.0.1:${port}(见 deploy/caddy/Caddyfile.example"
fi
}
ensure_env() {
if [[ ! -f "$ENV_FILE" ]]; then
if [[ ! -f "$ENV_EXAMPLE" ]]; then
err "找不到 $ENV_EXAMPLE"
exit 1
fi
log "未找到 $ENV_FILE,从示例生成…"
cp "$ENV_EXAMPLE" "$ENV_FILE"
local jwt dbpass minio_pass
jwt="$(rand_hex)"
dbpass="$(rand_hex)"
minio_pass="K$(rand_hex | head -c 16)"
# macOS / GNU sed 兼容
if sed --version >/dev/null 2>&1; then
sed -i "s|^JWT_SECRET=.*|JWT_SECRET=${jwt}|" "$ENV_FILE"
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=${dbpass}|" "$ENV_FILE"
sed -i "s|^STORAGE_SECRET_KEY=.*|STORAGE_SECRET_KEY=${minio_pass}|" "$ENV_FILE"
else
sed -i '' "s|^JWT_SECRET=.*|JWT_SECRET=${jwt}|" "$ENV_FILE"
sed -i '' "s|^DB_PASSWORD=.*|DB_PASSWORD=${dbpass}|" "$ENV_FILE"
sed -i '' "s|^STORAGE_SECRET_KEY=.*|STORAGE_SECRET_KEY=${minio_pass}|" "$ENV_FILE"
fi
warn "已生成随机 JWT_SECRET / DB_PASSWORD / STORAGE_SECRET_KEY"
warn "请编辑 $ENV_FILE,至少设置:"
warn " APP_BASE_URL=https://你的域名 # 经 Caddy 时用 https 域名,勿写 :18080"
warn " STORAGE_PUBLIC_BASE_URL=https://你的域名/files"
warn " HTTP_PORT=18080 # 已有 Caddy 占 80 时保持默认;独占机可改 80"
echo
read -r -p "设置完成后按回车继续(或 Ctrl+C 退出先改配置): " _
fi
# shellcheck disable=SC1090
set -a
# 去除 Windows CRLF
source <(sed 's/\r$//' "$ENV_FILE" | grep -v '^\s*#' | grep -v '^\s*$' || true)
set +a
# 未显式设置时与 compose 默认一致
HTTP_PORT="${HTTP_PORT:-18080}"
if [[ -z "${JWT_SECRET:-}" || "${JWT_SECRET}" == *"请改"* ]]; then
err "请在 $ENV_FILE 中设置有效的 JWT_SECRET"
exit 1
fi
if [[ -z "${DB_PASSWORD:-}" || "${DB_PASSWORD}" == *"请改"* ]]; then
err "请在 $ENV_FILE 中设置有效的 DB_PASSWORD"
exit 1
fi
if [[ -z "${STORAGE_PUBLIC_BASE_URL:-}" || "${STORAGE_PUBLIC_BASE_URL}" == *"你的"* ]]; then
err "请在 $ENV_FILE 中设置 STORAGE_PUBLIC_BASE_URL(如 https://域名/files"
exit 1
fi
if [[ -z "${STORAGE_ACCESS_KEY:-}" || -z "${STORAGE_SECRET_KEY:-}" ]]; then
err "请设置 STORAGE_ACCESS_KEY / STORAGE_SECRET_KEY"
exit 1
fi
if [[ ${#STORAGE_SECRET_KEY} -lt 8 ]]; then
err "STORAGE_SECRET_KEY 长度至少 8 位(MinIO 要求)"
exit 1
fi
check_http_port
}
wait_healthy() {
local url="${1:-http://127.0.0.1:${HTTP_PORT:-18080}/health}"
local i
log "等待服务就绪: $url"
for i in $(seq 1 60); do
if curl -fsS "$url" >/dev/null 2>&1; then
log "健康检查通过"
return 0
fi
sleep 2
done
err "健康检查超时,请执行: docker compose -f $COMPOSE_FILE --env-file $ENV_FILE logs"
return 1
}
print_summary() {
local port="${HTTP_PORT:-18080}"
local base="${APP_BASE_URL:-http://127.0.0.1:${port}}"
echo
log "========== 部署完成 =========="
echo " 访问地址: ${base}"
echo " 登录页: ${base}/login"
echo " 健康检查: ${base}/health"
echo " Widget 预览: ${base}/widget/preview"
echo " 图片前缀: ${STORAGE_PUBLIC_BASE_URL}"
echo " 本机探测: http://127.0.0.1:${port}/healthz web 容器映射端口)"
echo
if [[ "$port" != "80" && "$port" != "443" ]]; then
echo " Caddy 共存:"
echo " 1) 确认本机: curl -fsS http://127.0.0.1:${port}/healthz"
echo " 2) 合并 deploy/caddy/Caddyfile.example 到现有 Caddy,反代 127.0.0.1:${port}"
echo " 3) APP_BASE_URL / STORAGE_PUBLIC_BASE_URL 使用 https 域名(不要写 :${port}"
echo
fi
echo " 常用命令:"
echo " 查看状态 ./deploy/scripts/deploy.sh --status"
echo " 查看日志 docker compose -f $COMPOSE_FILE --env-file $ENV_FILE logs -f"
echo " 导入种子 ./deploy/scripts/deploy.sh --seed-only"
echo " 停止服务 ./deploy/scripts/deploy.sh --down"
echo
echo " 安全提示: 安全组仅放行 22/80/443(不必公网放行 ${port});改默认密码;生产用 HTTPS。"
echo "=============================="
}
cmd_up() {
local seed=0 rebuild=1
while [[ $# -gt 0 ]]; do
case "$1" in
--seed) seed=1 ;;
--pull-only) rebuild=0 ;;
*) ;;
esac
shift
done
need_cmd docker
need_cmd curl
if ! docker compose version >/dev/null 2>&1; then
err "需要 Docker Compose V2docker compose"
exit 1
fi
ensure_env
log "拉取基础镜像…"
compose pull postgres minio minio-init || true
if [[ "$rebuild" -eq 1 ]]; then
log "构建并启动全部服务(api + web + db + minio)…"
compose up -d --build --remove-orphans
else
log "启动服务(不强制 rebuild)…"
compose up -d --remove-orphans
fi
wait_healthy "http://127.0.0.1:${HTTP_PORT:-18080}/health" || true
wait_healthy "http://127.0.0.1:${HTTP_PORT:-18080}/healthz" || true
if [[ "$seed" -eq 1 ]]; then
log "导入演示种子数据…"
compose --profile seed run --rm seed || warn "种子导入失败(可能已存在数据)"
fi
compose ps
print_summary
}
cmd_seed_only() {
need_cmd docker
ensure_env
log "运行 seed…"
compose --profile seed run --rm seed
log "种子完成。默认租户管理员 kefu_admin / kefu_admin123 (若 seed 成功)"
}
cmd_down() {
need_cmd docker
ensure_env
log "停止容器(保留数据卷)…"
compose down
log "已停止。删除数据卷请手动: docker volume rm kefu-cloud_kefu_cloud_pgdata kefu-cloud_kefu_cloud_miniodata"
}
cmd_status() {
need_cmd docker
if [[ -f "$ENV_FILE" ]]; then
compose ps
echo
curl -fsS "http://127.0.0.1:${HTTP_PORT:-18080}/health" && echo || warn "health 不可达"
else
err "缺少 $ENV_FILE"
exit 1
fi
}
main() {
local action="${1:-up}"
case "$action" in
up|--up|"")
shift || true
cmd_up "$@"
;;
--seed)
# 兼容:./deploy.sh --seed
cmd_up --seed
;;
--seed-only)
cmd_seed_only
;;
--down|down)
cmd_down
;;
--status|status)
# 尝试读 HTTP_PORT
if [[ -f "$ENV_FILE" ]]; then
# shellcheck disable=SC1090
set -a; source <(sed 's/\r$//' "$ENV_FILE" | grep -v '^\s*#' | grep -v '^\s*$' || true); set +a
fi
cmd_status
;;
--pull-only)
cmd_up --pull-only
;;
-h|--help|help)
sed -n '2,12p' "$0"
;;
*)
err "未知参数: $action (见 --help"
exit 1
;;
esac
}
main "$@"