增加腾讯云生产 Docker 部署编排与一键脚本

- 提供 API/Web 多阶段镜像、Nginx 反代与 WS/文件路径
- docker-compose.prod 含 Postgres、MinIO、API、前端
- deploy.sh 生成密钥、构建启动、可选种子与备份脚本
This commit is contained in:
yml2213
2026-07-15 15:19:04 +08:00
parent 2b18d654b3
commit d744fc790c
8 changed files with 740 additions and 0 deletions
+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env bash
# 客服云 — 腾讯云 / 任意 Linux 一键生产部署
# 用法:
# ./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
}
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=http://你的公网IP或域名"
warn " STORAGE_PUBLIC_BASE_URL=http://你的公网IP或域名/files"
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
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(如 http://1.2.3.4/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
}
wait_healthy() {
local url="${1:-http://127.0.0.1:${HTTP_PORT:-80}/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 base="${APP_BASE_URL:-http://127.0.0.1:${HTTP_PORT:-80}}"
echo
log "========== 部署完成 =========="
echo " 访问地址: ${base}"
echo " 登录页: ${base}/login"
echo " 健康检查: ${base}/health"
echo " Widget 预览: ${base}/widget/preview"
echo " 图片前缀: ${STORAGE_PUBLIC_BASE_URL}"
echo
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 " 安全提示: 安全组仅放行 80/443;改默认密码;生产务必使用 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:-80}/health" || true
wait_healthy "http://127.0.0.1:${HTTP_PORT:-80}/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-prod_kefu_pgdata kefu-prod_kefu_miniodata"
}
cmd_status() {
need_cmd docker
if [[ -f "$ENV_FILE" ]]; then
compose ps
echo
curl -fsS "http://127.0.0.1:${HTTP_PORT:-80}/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 "$@"