重构
This commit is contained in:
46
scripts/backup-db.sh
Executable file
46
scripts/backup-db.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# 备份生产 Docker PostgreSQL 到 ./backups
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/deploy-lib.sh
|
||||
source "$SCRIPT_DIR/deploy-lib.sh"
|
||||
|
||||
PREFIX="manual"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: scripts/backup-db.sh [选项]
|
||||
|
||||
选项:
|
||||
--prefix NAME 备份文件名前缀,默认 manual
|
||||
-h, --help 显示帮助
|
||||
|
||||
示例:
|
||||
./scripts/backup-db.sh
|
||||
./scripts/backup-db.sh --prefix pre-release
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--prefix)
|
||||
PREFIX="${2:?缺少 --prefix 参数值}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "未知参数: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$ROOT"
|
||||
require_docker
|
||||
ensure_env_file || exit 1
|
||||
start_infra
|
||||
backup_database "$PREFIX"
|
||||
124
scripts/deploy-first.sh
Executable file
124
scripts/deploy-first.sh
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# 首次部署:启动 Docker 栈、执行迁移,并在缺少 admin 时写入生产默认数据。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/deploy-lib.sh
|
||||
source "$SCRIPT_DIR/deploy-lib.sh"
|
||||
|
||||
NO_BUILD=false
|
||||
RUN_SEED=true
|
||||
INIT_DB=false
|
||||
SKIP_BACKUP=false
|
||||
ALLOW_DEFAULT_SECRETS=false
|
||||
IMAGE_TAR=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: scripts/deploy-first.sh [选项]
|
||||
|
||||
默认流程:
|
||||
1. 检查 .env.docker
|
||||
2. 启动 postgres / redis
|
||||
3. 构建 api / player / admin 镜像
|
||||
4. 执行 prisma migrate deploy
|
||||
5. 启动全栈容器
|
||||
6. 如果数据库中没有 admin,执行一次生产 seed
|
||||
|
||||
选项:
|
||||
--no-build 不构建镜像,直接使用服务器已有镜像
|
||||
--images PATH 先 docker load 指定镜像 tar,并自动跳过本机构建
|
||||
--no-seed 不执行生产 seed
|
||||
--init-db 调用 scripts/prod-init-db.sh 清空业务数据并初始化生产数据,仅限全新库或明确重置
|
||||
--skip-backup 与 --init-db 一起使用,跳过 prod-init-db 的备份
|
||||
--allow-default-secrets 允许 .env.docker 使用示例密钥,仅测试环境使用
|
||||
-h, --help 显示帮助
|
||||
|
||||
示例:
|
||||
./scripts/deploy-first.sh
|
||||
./scripts/deploy-first.sh --images thebet365-images.tar
|
||||
./scripts/deploy-first.sh --no-build
|
||||
./scripts/deploy-first.sh --init-db
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--no-build)
|
||||
NO_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--images)
|
||||
IMAGE_TAR="${2:?缺少 --images 参数值}"
|
||||
NO_BUILD=true
|
||||
shift 2
|
||||
;;
|
||||
--no-seed)
|
||||
RUN_SEED=false
|
||||
shift
|
||||
;;
|
||||
--init-db)
|
||||
INIT_DB=true
|
||||
RUN_SEED=false
|
||||
shift
|
||||
;;
|
||||
--skip-backup)
|
||||
SKIP_BACKUP=true
|
||||
shift
|
||||
;;
|
||||
--allow-default-secrets)
|
||||
ALLOW_DEFAULT_SECRETS=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "未知参数: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$ROOT"
|
||||
require_docker
|
||||
ensure_env_file || exit 1
|
||||
validate_prod_env "$ALLOW_DEFAULT_SECRETS"
|
||||
|
||||
if [ -n "$IMAGE_TAR" ]; then
|
||||
load_image_tar "$IMAGE_TAR"
|
||||
fi
|
||||
|
||||
start_infra
|
||||
|
||||
if [ "$NO_BUILD" = false ]; then
|
||||
build_app_images
|
||||
else
|
||||
log "跳过镜像构建,使用服务器已有镜像"
|
||||
fi
|
||||
|
||||
run_prisma_migrations
|
||||
|
||||
if [ "$INIT_DB" = false ] && [ "$RUN_SEED" = true ]; then
|
||||
seed_production_if_missing_admin
|
||||
fi
|
||||
|
||||
log "启动 api / player / admin"
|
||||
compose up -d api player admin
|
||||
wait_for_service_running api 120
|
||||
|
||||
if [ "$INIT_DB" = true ]; then
|
||||
init_args=()
|
||||
if [ "$SKIP_BACKUP" = true ]; then
|
||||
init_args+=(--skip-backup)
|
||||
fi
|
||||
log "执行生产初始化:这会清空业务数据"
|
||||
CONFIRM=YES "$ROOT/scripts/prod-init-db.sh" "${init_args[@]}"
|
||||
compose restart api
|
||||
wait_for_service_running api 120
|
||||
fi
|
||||
|
||||
show_prisma_status
|
||||
compose ps
|
||||
print_stack_urls
|
||||
209
scripts/deploy-lib.sh
Executable file
209
scripts/deploy-lib.sh
Executable file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
DEPLOY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$DEPLOY_LIB_DIR/.." && pwd)"
|
||||
COMPOSE_FILE="$ROOT/docker-compose.prod.yml"
|
||||
ENV_FILE="$ROOT/.env.docker"
|
||||
ENV_EXAMPLE_FILE="$ROOT/.env.docker.example"
|
||||
BACKUP_DIR="$ROOT/backups"
|
||||
|
||||
log() {
|
||||
printf '%s\n' "[$(basename "$0")] $*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '%s\n' "警告: $*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '%s\n' "错误: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "未找到命令: $1"
|
||||
}
|
||||
|
||||
require_docker() {
|
||||
require_command docker
|
||||
docker compose version >/dev/null 2>&1 || die "未找到 Docker Compose v2,请确认可执行 docker compose"
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@"
|
||||
}
|
||||
|
||||
ensure_env_file() {
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENV_EXAMPLE_FILE" ]; then
|
||||
die "未找到 .env.docker 或 .env.docker.example"
|
||||
fi
|
||||
|
||||
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
|
||||
warn "已从 .env.docker.example 创建 .env.docker"
|
||||
warn "请先修改 POSTGRES_PASSWORD、JWT_SECRET、短信配置和端口后再重新执行部署脚本"
|
||||
return 1
|
||||
}
|
||||
|
||||
env_value() {
|
||||
local key="$1"
|
||||
local line
|
||||
line="$(grep -E "^[[:space:]]*${key}=" "$ENV_FILE" | tail -n 1 || true)"
|
||||
line="${line#*=}"
|
||||
line="${line%$'\r'}"
|
||||
line="${line%\"}"
|
||||
line="${line#\"}"
|
||||
line="${line%\'}"
|
||||
line="${line#\'}"
|
||||
printf '%s' "$line"
|
||||
}
|
||||
|
||||
validate_prod_env() {
|
||||
local allow_defaults="${1:-false}"
|
||||
local postgres_password jwt_secret seed_database
|
||||
|
||||
postgres_password="$(env_value POSTGRES_PASSWORD)"
|
||||
jwt_secret="$(env_value JWT_SECRET)"
|
||||
seed_database="$(env_value SEED_DATABASE)"
|
||||
|
||||
[ -n "$postgres_password" ] || die ".env.docker 缺少 POSTGRES_PASSWORD"
|
||||
[ -n "$jwt_secret" ] || die ".env.docker 缺少 JWT_SECRET"
|
||||
|
||||
if [ "$allow_defaults" != "true" ]; then
|
||||
[ "$postgres_password" != "thebet365" ] || die "POSTGRES_PASSWORD 仍是示例值;如确为测试环境,请加 --allow-default-secrets"
|
||||
[ "$jwt_secret" != "change-me-in-production-use-long-random-string" ] || die "JWT_SECRET 仍是示例值;如确为测试环境,请加 --allow-default-secrets"
|
||||
fi
|
||||
|
||||
if [ "$seed_database" = "true" ]; then
|
||||
warn ".env.docker 中 SEED_DATABASE=true 会让 api 每次启动都执行 seed;生产建议设为 false,首次部署脚本会按需一次性 seed"
|
||||
fi
|
||||
|
||||
if [ "$(env_value CHUANGLAN_ACCOUNT)" = "your_account" ] || [ "$(env_value CHUANGLAN_PASSWORD)" = "your_password" ]; then
|
||||
warn "创蓝短信账号仍是示例值,短信验证码功能上线前需要改为真实配置"
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_service_health() {
|
||||
local service="$1"
|
||||
local timeout="${2:-120}"
|
||||
local start now container_id status
|
||||
|
||||
start="$(date +%s)"
|
||||
while true; do
|
||||
container_id="$(compose ps -q "$service" 2>/dev/null || true)"
|
||||
if [ -n "$container_id" ]; then
|
||||
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id" 2>/dev/null || true)"
|
||||
if [ "$status" = "healthy" ] || [ "$status" = "running" ]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
now="$(date +%s)"
|
||||
if [ $((now - start)) -ge "$timeout" ]; then
|
||||
compose logs --tail=80 "$service" || true
|
||||
die "$service 在 ${timeout}s 内未就绪"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_service_running() {
|
||||
local service="$1"
|
||||
local timeout="${2:-120}"
|
||||
local start now container_id running
|
||||
|
||||
start="$(date +%s)"
|
||||
while true; do
|
||||
container_id="$(compose ps -q "$service" 2>/dev/null || true)"
|
||||
if [ -n "$container_id" ]; then
|
||||
running="$(docker inspect -f '{{.State.Running}}' "$container_id" 2>/dev/null || true)"
|
||||
if [ "$running" = "true" ]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
now="$(date +%s)"
|
||||
if [ $((now - start)) -ge "$timeout" ]; then
|
||||
compose logs --tail=120 "$service" || true
|
||||
die "$service 在 ${timeout}s 内未保持运行"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
start_infra() {
|
||||
log "启动 PostgreSQL / Redis"
|
||||
compose up -d postgres redis
|
||||
wait_for_service_health postgres 120
|
||||
wait_for_service_health redis 120
|
||||
}
|
||||
|
||||
build_app_images() {
|
||||
log "构建 api / player / admin 镜像"
|
||||
compose build api player admin
|
||||
}
|
||||
|
||||
load_image_tar() {
|
||||
local image_tar="$1"
|
||||
[ -f "$image_tar" ] || die "镜像包不存在: $image_tar"
|
||||
log "加载镜像包: $image_tar"
|
||||
docker load -i "$image_tar"
|
||||
}
|
||||
|
||||
run_prisma_migrations() {
|
||||
log "执行 Prisma 迁移"
|
||||
compose run --rm --no-deps --entrypoint sh api -c 'cd /app/apps/api && npx prisma migrate deploy && npx prisma generate'
|
||||
}
|
||||
|
||||
show_prisma_status() {
|
||||
log "检查 Prisma 迁移状态"
|
||||
compose exec -T api sh -c 'cd /app/apps/api && npx prisma migrate status'
|
||||
}
|
||||
|
||||
backup_database() {
|
||||
local prefix="${1:-manual}"
|
||||
local stamp backup_file
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
backup_file="$BACKUP_DIR/thebet365-${prefix}-${stamp}.sql"
|
||||
|
||||
log "备份 PostgreSQL -> $backup_file"
|
||||
compose exec -T postgres pg_dump -U thebet365 -d thebet365 -F p > "$backup_file"
|
||||
log "数据库备份完成: $backup_file"
|
||||
}
|
||||
|
||||
admin_user_count() {
|
||||
compose exec -T postgres psql -U thebet365 -d thebet365 -tAc "select count(*) from users where username = 'admin';" 2>/dev/null | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
seed_production_if_missing_admin() {
|
||||
local count
|
||||
count="$(admin_user_count || true)"
|
||||
[ -n "$count" ] || die "无法检查 admin 用户,请确认数据库迁移已成功"
|
||||
|
||||
if [ "$count" = "0" ]; then
|
||||
log "未发现 admin 用户,执行一次生产 seed"
|
||||
compose run --rm --no-deps --entrypoint sh -e SEED_MODE=production -e NODE_ENV=production api -c 'cd /app/apps/api && node dist/infrastructure/database/seed-cli.js'
|
||||
else
|
||||
log "已存在 admin 用户,跳过生产 seed"
|
||||
fi
|
||||
}
|
||||
|
||||
print_stack_urls() {
|
||||
local player_port admin_port
|
||||
player_port="$(env_value PLAYER_PORT)"
|
||||
admin_port="$(env_value ADMIN_PORT)"
|
||||
player_port="${player_port:-8082}"
|
||||
admin_port="${admin_port:-8081}"
|
||||
|
||||
printf '\n'
|
||||
printf '%s\n' "部署完成:"
|
||||
printf '%s\n' " 玩家端: http://服务器IP:${player_port}"
|
||||
printf '%s\n' " 管理端: http://服务器IP:${admin_port}"
|
||||
printf '%s\n' " API: 仅在 Docker 网络内暴露,由 player/admin 容器和宝塔反代链路访问"
|
||||
printf '%s\n' " 状态: docker compose -f docker-compose.prod.yml --env-file .env.docker ps"
|
||||
}
|
||||
113
scripts/deploy-update.sh
Executable file
113
scripts/deploy-update.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# 后续更新:可选拉代码/加载镜像,备份数据库,构建或使用新镜像,执行迁移并滚动到新容器。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/deploy-lib.sh
|
||||
source "$SCRIPT_DIR/deploy-lib.sh"
|
||||
|
||||
PULL_CODE=false
|
||||
NO_BUILD=false
|
||||
SKIP_BACKUP=false
|
||||
ALLOW_DEFAULT_SECRETS=false
|
||||
IMAGE_TAR=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: scripts/deploy-update.sh [选项]
|
||||
|
||||
默认流程:
|
||||
1. 检查 .env.docker
|
||||
2. 启动并等待 postgres / redis
|
||||
3. 备份 PostgreSQL 到 ./backups
|
||||
4. 构建 api / player / admin 镜像
|
||||
5. 使用新 api 镜像执行 prisma migrate deploy
|
||||
6. 启动/替换 api、player、admin 容器
|
||||
7. 检查 prisma migrate status
|
||||
|
||||
选项:
|
||||
--pull 先执行 git pull --ff-only
|
||||
--images PATH 先 docker load 指定镜像 tar,并自动跳过本机构建
|
||||
--no-build 不构建镜像,直接使用服务器已有镜像
|
||||
--no-backup 跳过数据库备份
|
||||
--allow-default-secrets 允许 .env.docker 使用示例密钥,仅测试环境使用
|
||||
-h, --help 显示帮助
|
||||
|
||||
示例:
|
||||
./scripts/deploy-update.sh --pull
|
||||
./scripts/deploy-update.sh --images thebet365-images.tar
|
||||
./scripts/deploy-update.sh --no-build
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--pull)
|
||||
PULL_CODE=true
|
||||
shift
|
||||
;;
|
||||
--images)
|
||||
IMAGE_TAR="${2:?缺少 --images 参数值}"
|
||||
NO_BUILD=true
|
||||
shift 2
|
||||
;;
|
||||
--no-build)
|
||||
NO_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--no-backup)
|
||||
SKIP_BACKUP=true
|
||||
shift
|
||||
;;
|
||||
--allow-default-secrets)
|
||||
ALLOW_DEFAULT_SECRETS=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "未知参数: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$ROOT"
|
||||
require_docker
|
||||
ensure_env_file || exit 1
|
||||
validate_prod_env "$ALLOW_DEFAULT_SECRETS"
|
||||
|
||||
if [ "$PULL_CODE" = true ]; then
|
||||
require_command git
|
||||
log "拉取代码: git pull --ff-only"
|
||||
git pull --ff-only
|
||||
fi
|
||||
|
||||
if [ -n "$IMAGE_TAR" ]; then
|
||||
load_image_tar "$IMAGE_TAR"
|
||||
fi
|
||||
|
||||
start_infra
|
||||
|
||||
if [ "$SKIP_BACKUP" = false ]; then
|
||||
backup_database "pre-update"
|
||||
else
|
||||
warn "已跳过数据库备份"
|
||||
fi
|
||||
|
||||
if [ "$NO_BUILD" = false ]; then
|
||||
build_app_images
|
||||
else
|
||||
log "跳过镜像构建,使用服务器已有镜像"
|
||||
fi
|
||||
|
||||
run_prisma_migrations
|
||||
|
||||
log "启动/更新 api / player / admin"
|
||||
compose up -d api player admin
|
||||
wait_for_service_running api 120
|
||||
show_prisma_status
|
||||
compose ps
|
||||
print_stack_urls
|
||||
@@ -14,6 +14,6 @@ echo ""
|
||||
echo "TheBet365 stack is starting."
|
||||
echo " Player: http://localhost:${PLAYER_PORT:-8082}"
|
||||
echo " Admin: http://localhost:${ADMIN_PORT:-8081}"
|
||||
echo " API: http://localhost:${API_PORT:-3000}/api/docs"
|
||||
echo " API: exposed only inside the docker network; route it through your reverse proxy"
|
||||
echo ""
|
||||
echo "Check status: docker compose -f docker-compose.prod.yml ps"
|
||||
|
||||
0
scripts/prod-init-db.sh
Normal file → Executable file
0
scripts/prod-init-db.sh
Normal file → Executable file
Reference in New Issue
Block a user