部署优化
This commit is contained in:
@@ -11,7 +11,8 @@ $ErrorActionPreference = "Stop"
|
||||
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
||||
|
||||
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$outFile = Join-Path $OutDir "thebet365-$stamp.sql"
|
||||
$tempFile = Join-Path $OutDir "thebet365-db-manual-$stamp.sql"
|
||||
$outFile = "$tempFile.gz"
|
||||
|
||||
Write-Host "Backing up $DbName to $outFile ..."
|
||||
$env:PGPASSWORD = $env:THEBET365_DB_PASSWORD
|
||||
@@ -19,5 +20,24 @@ if (-not $env:PGPASSWORD) {
|
||||
Write-Warning "Set THEBET365_DB_PASSWORD if your Postgres requires a password."
|
||||
}
|
||||
|
||||
pg_dump -h $DbHost -p $DbPort -U $DbUser -d $DbName -F p -f $outFile
|
||||
pg_dump -h $DbHost -p $DbPort -U $DbUser -d $DbName -F p -f $tempFile
|
||||
|
||||
$inputStream = [System.IO.File]::OpenRead($tempFile)
|
||||
$outputStream = [System.IO.File]::Create($outFile)
|
||||
try {
|
||||
$gzipStream = [System.IO.Compression.GzipStream]::new($outputStream, [System.IO.Compression.CompressionMode]::Compress)
|
||||
try {
|
||||
$inputStream.CopyTo($gzipStream)
|
||||
} finally {
|
||||
$gzipStream.Dispose()
|
||||
}
|
||||
} finally {
|
||||
$inputStream.Dispose()
|
||||
$outputStream.Dispose()
|
||||
}
|
||||
Remove-Item $tempFile -Force
|
||||
|
||||
$hash = (Get-FileHash -Algorithm SHA256 $outFile).Hash.ToLowerInvariant()
|
||||
"$hash $(Split-Path -Leaf $outFile)" | Set-Content -Encoding UTF8 "$outFile.sha256"
|
||||
Write-Host "Done: $outFile"
|
||||
Write-Host "Checksum: $outFile.sha256"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# 备份生产 Docker PostgreSQL 到 ./backups
|
||||
# 备份生产 Docker PostgreSQL 到 ./backups,输出 gzip 与 sha256 校验文件
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -44,3 +44,4 @@ require_docker
|
||||
ensure_env_file || exit 1
|
||||
start_infra
|
||||
backup_database "$PREFIX"
|
||||
prune_old_backups
|
||||
|
||||
69
scripts/backup-prod.sh
Executable file
69
scripts/backup-prod.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# 完整生产备份:PostgreSQL + uploads 卷,输出 gzip/tar.gz 与 sha256 校验文件。
|
||||
|
||||
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"
|
||||
SKIP_UPLOADS=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: scripts/backup-prod.sh [选项]
|
||||
|
||||
默认流程:
|
||||
1. 检查 .env.docker
|
||||
2. 启动并等待 PostgreSQL / Redis
|
||||
3. 备份 PostgreSQL 到 ./backups/*.sql.gz
|
||||
4. 通过正在运行的 api 容器备份 uploads 到 ./backups/*.tar.gz
|
||||
5. 为备份文件生成 .sha256 校验文件
|
||||
|
||||
选项:
|
||||
--prefix NAME 备份文件名前缀,默认 manual
|
||||
--skip-uploads 只备份数据库,不备份 uploads
|
||||
-h, --help 显示帮助
|
||||
|
||||
示例:
|
||||
./scripts/backup-prod.sh
|
||||
./scripts/backup-prod.sh --prefix pre-release
|
||||
./scripts/backup-prod.sh --skip-uploads
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--prefix)
|
||||
PREFIX="${2:?缺少 --prefix 参数值}"
|
||||
shift 2
|
||||
;;
|
||||
--skip-uploads)
|
||||
SKIP_UPLOADS=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "未知参数: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$ROOT"
|
||||
require_docker
|
||||
ensure_env_file || exit 1
|
||||
start_infra
|
||||
backup_database "$PREFIX"
|
||||
|
||||
if [ "$SKIP_UPLOADS" = false ]; then
|
||||
backup_uploads "$PREFIX"
|
||||
else
|
||||
warn "已跳过 uploads 备份"
|
||||
fi
|
||||
|
||||
prune_old_backups
|
||||
log "生产备份完成"
|
||||
@@ -29,6 +29,7 @@ usage() {
|
||||
选项:
|
||||
--no-build 不构建镜像,直接使用服务器已有镜像
|
||||
--images PATH 先 docker load 指定镜像 tar,并自动跳过本机构建
|
||||
--tag TAG 使用指定镜像 tag,并写入 .env.docker 的 IMAGE_TAG
|
||||
--no-seed 不执行生产 seed
|
||||
--init-db 调用 scripts/prod-init-db.sh 清空业务数据并初始化生产数据,仅限全新库或明确重置
|
||||
--skip-backup 与 --init-db 一起使用,跳过 prod-init-db 的备份
|
||||
@@ -37,7 +38,7 @@ usage() {
|
||||
|
||||
示例:
|
||||
./scripts/deploy-first.sh
|
||||
./scripts/deploy-first.sh --images thebet365-images.tar
|
||||
./scripts/deploy-first.sh --images thebet365-images-v1.2.3.tar --tag v1.2.3
|
||||
./scripts/deploy-first.sh --no-build
|
||||
./scripts/deploy-first.sh --init-db
|
||||
EOF
|
||||
@@ -54,6 +55,10 @@ while [ $# -gt 0 ]; do
|
||||
NO_BUILD=true
|
||||
shift 2
|
||||
;;
|
||||
--tag)
|
||||
set_deploy_image_tag "${2:?缺少 --tag 参数值}"
|
||||
shift 2
|
||||
;;
|
||||
--no-seed)
|
||||
RUN_SEED=false
|
||||
shift
|
||||
@@ -86,6 +91,14 @@ require_docker
|
||||
ensure_env_file || exit 1
|
||||
validate_prod_env "$ALLOW_DEFAULT_SECRETS"
|
||||
|
||||
if [ -n "$IMAGE_TAR" ] && [ -z "${DEPLOY_IMAGE_TAG:-}" ]; then
|
||||
inferred_tag="$(infer_image_tag_from_tar "$IMAGE_TAR")"
|
||||
if [ -n "$inferred_tag" ]; then
|
||||
set_deploy_image_tag "$inferred_tag"
|
||||
log "从镜像包文件名推断 tag: $inferred_tag"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$IMAGE_TAR" ]; then
|
||||
load_image_tar "$IMAGE_TAR"
|
||||
fi
|
||||
@@ -96,6 +109,7 @@ if [ "$NO_BUILD" = false ]; then
|
||||
build_app_images
|
||||
else
|
||||
log "跳过镜像构建,使用服务器已有镜像"
|
||||
require_images_for_current_tag
|
||||
fi
|
||||
|
||||
run_prisma_migrations
|
||||
@@ -106,7 +120,7 @@ fi
|
||||
|
||||
log "启动 api / player / admin"
|
||||
compose up -d api player admin
|
||||
wait_for_service_running api 120
|
||||
wait_for_stack_ready
|
||||
|
||||
if [ "$INIT_DB" = true ]; then
|
||||
init_args=()
|
||||
@@ -116,9 +130,11 @@ if [ "$INIT_DB" = true ]; then
|
||||
log "执行生产初始化:这会清空业务数据"
|
||||
CONFIRM=YES "$ROOT/scripts/prod-init-db.sh" "${init_args[@]}"
|
||||
compose restart api
|
||||
wait_for_service_running api 120
|
||||
wait_for_service_health api 180
|
||||
fi
|
||||
|
||||
show_prisma_status
|
||||
compose ps
|
||||
persist_image_tag
|
||||
record_release_state "first-deploy"
|
||||
print_stack_urls
|
||||
|
||||
@@ -6,6 +6,14 @@ COMPOSE_FILE="$ROOT/docker-compose.prod.yml"
|
||||
ENV_FILE="$ROOT/.env.docker"
|
||||
ENV_EXAMPLE_FILE="$ROOT/.env.docker.example"
|
||||
BACKUP_DIR="$ROOT/backups"
|
||||
DEPLOY_STATE_DIR="$ROOT/.deploy"
|
||||
RELEASES_DIR="$DEPLOY_STATE_DIR/releases"
|
||||
CURRENT_RELEASE_FILE="$DEPLOY_STATE_DIR/current-release.env"
|
||||
PREVIOUS_RELEASE_FILE="$DEPLOY_STATE_DIR/previous-release.env"
|
||||
|
||||
DEPLOY_IMAGE_TAG="${DEPLOY_IMAGE_TAG:-}"
|
||||
LAST_DB_BACKUP=""
|
||||
LAST_UPLOADS_BACKUP=""
|
||||
|
||||
log() {
|
||||
printf '%s\n' "[$(basename "$0")] $*"
|
||||
@@ -29,8 +37,36 @@ require_docker() {
|
||||
docker compose version >/dev/null 2>&1 || die "未找到 Docker Compose v2,请确认可执行 docker compose"
|
||||
}
|
||||
|
||||
validate_image_tag() {
|
||||
local tag="$1"
|
||||
printf '%s' "$tag" | grep -Eq '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$' ||
|
||||
die "镜像 tag 不合法: $tag"
|
||||
}
|
||||
|
||||
set_deploy_image_tag() {
|
||||
local tag="$1"
|
||||
[ -n "$tag" ] || die "镜像 tag 不能为空"
|
||||
validate_image_tag "$tag"
|
||||
DEPLOY_IMAGE_TAG="$tag"
|
||||
}
|
||||
|
||||
infer_image_tag_from_tar() {
|
||||
local image_tar="$1"
|
||||
local base tag
|
||||
|
||||
base="$(basename "$image_tar")"
|
||||
tag="${base#thebet365-images-}"
|
||||
tag="${tag%.tar}"
|
||||
|
||||
if [ "$tag" != "$base" ] && [ -n "$tag" ]; then
|
||||
printf '%s' "$tag"
|
||||
fi
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@"
|
||||
local tag
|
||||
tag="$(current_image_tag)"
|
||||
IMAGE_TAG="$tag" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@"
|
||||
}
|
||||
|
||||
ensure_env_file() {
|
||||
@@ -61,13 +97,61 @@ env_value() {
|
||||
printf '%s' "$line"
|
||||
}
|
||||
|
||||
set_env_value() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local tmp
|
||||
|
||||
tmp="$(mktemp)"
|
||||
if grep -Eq "^[[:space:]]*${key}=" "$ENV_FILE"; then
|
||||
awk -v key="$key" -v value="$value" '
|
||||
BEGIN { done = 0 }
|
||||
$0 ~ "^[[:space:]]*" key "=" {
|
||||
if (!done) {
|
||||
print key "=" value
|
||||
done = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!done) print key "=" value
|
||||
}
|
||||
' "$ENV_FILE" > "$tmp"
|
||||
else
|
||||
cp "$ENV_FILE" "$tmp"
|
||||
printf '\n%s=%s\n' "$key" "$value" >> "$tmp"
|
||||
fi
|
||||
mv "$tmp" "$ENV_FILE"
|
||||
}
|
||||
|
||||
current_image_tag() {
|
||||
local tag="${DEPLOY_IMAGE_TAG:-}"
|
||||
|
||||
if [ -z "$tag" ] && [ -f "$ENV_FILE" ]; then
|
||||
tag="$(env_value IMAGE_TAG)"
|
||||
fi
|
||||
|
||||
tag="${tag:-latest}"
|
||||
validate_image_tag "$tag"
|
||||
printf '%s' "$tag"
|
||||
}
|
||||
|
||||
persist_image_tag() {
|
||||
local tag
|
||||
tag="$(current_image_tag)"
|
||||
set_env_value IMAGE_TAG "$tag"
|
||||
log "当前发布镜像 tag: $tag"
|
||||
}
|
||||
|
||||
validate_prod_env() {
|
||||
local allow_defaults="${1:-false}"
|
||||
local postgres_password jwt_secret seed_database
|
||||
local postgres_password jwt_secret seed_database bind_addr
|
||||
|
||||
postgres_password="$(env_value POSTGRES_PASSWORD)"
|
||||
jwt_secret="$(env_value JWT_SECRET)"
|
||||
seed_database="$(env_value SEED_DATABASE)"
|
||||
bind_addr="$(env_value BIND_ADDR)"
|
||||
|
||||
[ -n "$postgres_password" ] || die ".env.docker 缺少 POSTGRES_PASSWORD"
|
||||
[ -n "$jwt_secret" ] || die ".env.docker 缺少 JWT_SECRET"
|
||||
@@ -81,6 +165,10 @@ validate_prod_env() {
|
||||
warn ".env.docker 中 SEED_DATABASE=true 会让 api 每次启动都执行 seed;生产建议设为 false,首次部署脚本会按需一次性 seed"
|
||||
fi
|
||||
|
||||
if [ "${bind_addr:-127.0.0.1}" = "0.0.0.0" ]; then
|
||||
warn "BIND_ADDR=0.0.0.0 会让 player/admin 端口直接对外监听;生产建议经宝塔反代访问"
|
||||
fi
|
||||
|
||||
if [ "$(env_value CHUANGLAN_ACCOUNT)" = "your_account" ] || [ "$(env_value CHUANGLAN_PASSWORD)" = "your_password" ]; then
|
||||
warn "创蓝短信账号仍是示例值,短信验证码功能上线前需要改为真实配置"
|
||||
fi
|
||||
@@ -103,35 +191,17 @@ wait_for_service_health() {
|
||||
|
||||
now="$(date +%s)"
|
||||
if [ $((now - start)) -ge "$timeout" ]; then
|
||||
compose logs --tail=80 "$service" || true
|
||||
compose logs --tail=120 "$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
|
||||
wait_for_stack_ready() {
|
||||
wait_for_service_health api 180
|
||||
wait_for_service_health player 120
|
||||
wait_for_service_health admin 120
|
||||
}
|
||||
|
||||
start_infra() {
|
||||
@@ -142,7 +212,7 @@ start_infra() {
|
||||
}
|
||||
|
||||
build_app_images() {
|
||||
log "构建 api / player / admin 镜像"
|
||||
log "构建 api / player / admin 镜像: $(current_image_tag)"
|
||||
compose build api player admin
|
||||
}
|
||||
|
||||
@@ -153,6 +223,14 @@ load_image_tar() {
|
||||
docker load -i "$image_tar"
|
||||
}
|
||||
|
||||
require_images_for_current_tag() {
|
||||
local tag
|
||||
tag="$(current_image_tag)"
|
||||
docker image inspect "thebet365-api:$tag" >/dev/null 2>&1 || die "缺少镜像: thebet365-api:$tag"
|
||||
docker image inspect "thebet365-player:$tag" >/dev/null 2>&1 || die "缺少镜像: thebet365-player:$tag"
|
||||
docker image inspect "thebet365-admin:$tag" >/dev/null 2>&1 || die "缺少镜像: thebet365-admin:$tag"
|
||||
}
|
||||
|
||||
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'
|
||||
@@ -163,19 +241,81 @@ show_prisma_status() {
|
||||
compose exec -T api sh -c 'cd /app/apps/api && npx prisma migrate status'
|
||||
}
|
||||
|
||||
safe_backup_prefix() {
|
||||
local prefix="${1:-manual}"
|
||||
prefix="$(printf '%s' "$prefix" | tr -c 'A-Za-z0-9_.-' '-')"
|
||||
prefix="${prefix:-manual}"
|
||||
printf '%s' "$prefix"
|
||||
}
|
||||
|
||||
write_checksum() {
|
||||
local file="$1"
|
||||
local dir base
|
||||
|
||||
dir="$(dirname "$file")"
|
||||
base="$(basename "$file")"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
(cd "$dir" && sha256sum "$base" > "$base.sha256")
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
(cd "$dir" && shasum -a 256 "$base" > "$base.sha256")
|
||||
else
|
||||
warn "未找到 sha256sum 或 shasum,跳过校验文件: $file"
|
||||
return 0
|
||||
fi
|
||||
log "校验文件完成: $file.sha256"
|
||||
}
|
||||
|
||||
backup_database() {
|
||||
local prefix="${1:-manual}"
|
||||
local stamp backup_file
|
||||
|
||||
require_command gzip
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
prefix="$(safe_backup_prefix "$prefix")"
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
backup_file="$BACKUP_DIR/thebet365-${prefix}-${stamp}.sql"
|
||||
backup_file="$BACKUP_DIR/thebet365-db-${prefix}-${stamp}.sql.gz"
|
||||
|
||||
log "备份 PostgreSQL -> $backup_file"
|
||||
compose exec -T postgres pg_dump -U thebet365 -d thebet365 -F p > "$backup_file"
|
||||
compose exec -T postgres pg_dump -U thebet365 -d thebet365 -F p | gzip -c > "$backup_file"
|
||||
write_checksum "$backup_file"
|
||||
LAST_DB_BACKUP="$backup_file"
|
||||
log "数据库备份完成: $backup_file"
|
||||
}
|
||||
|
||||
backup_uploads() {
|
||||
local prefix="${1:-manual}"
|
||||
local stamp backup_file
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
prefix="$(safe_backup_prefix "$prefix")"
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
backup_file="$BACKUP_DIR/thebet365-uploads-${prefix}-${stamp}.tar.gz"
|
||||
|
||||
log "备份 uploads 卷 -> $backup_file"
|
||||
compose exec -T api sh -c 'cd /app && mkdir -p uploads && tar -czf - uploads' > "$backup_file"
|
||||
write_checksum "$backup_file"
|
||||
LAST_UPLOADS_BACKUP="$backup_file"
|
||||
log "uploads 备份完成: $backup_file"
|
||||
}
|
||||
|
||||
prune_old_backups() {
|
||||
local days
|
||||
days="$(env_value BACKUP_RETENTION_DAYS)"
|
||||
[ -n "$days" ] || return 0
|
||||
|
||||
case "$days" in
|
||||
*[!0-9]*)
|
||||
warn "BACKUP_RETENTION_DAYS 不是数字,跳过自动清理: $days"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
[ "$days" -gt 0 ] || return 0
|
||||
[ -d "$BACKUP_DIR" ] || return 0
|
||||
log "清理 ${days} 天前的备份"
|
||||
find "$BACKUP_DIR" -type f \( -name '*.sql.gz' -o -name '*.tar.gz' -o -name '*.sha256' \) -mtime +"$days" -delete
|
||||
}
|
||||
|
||||
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:]'
|
||||
}
|
||||
@@ -193,17 +333,59 @@ seed_production_if_missing_admin() {
|
||||
fi
|
||||
}
|
||||
|
||||
current_release_tag() {
|
||||
if [ -f "$CURRENT_RELEASE_FILE" ]; then
|
||||
grep -E '^tag=' "$CURRENT_RELEASE_FILE" | tail -n 1 | cut -d= -f2- || true
|
||||
fi
|
||||
}
|
||||
|
||||
record_release_state() {
|
||||
local source="${1:-deploy}"
|
||||
local tag stamp safe_tag manifest previous_tag
|
||||
|
||||
tag="$(current_image_tag)"
|
||||
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
safe_tag="$(printf '%s' "$tag" | tr -c 'A-Za-z0-9_.-' '-')"
|
||||
manifest="$RELEASES_DIR/${stamp}-${safe_tag}.env"
|
||||
previous_tag="$(current_release_tag)"
|
||||
|
||||
mkdir -p "$RELEASES_DIR"
|
||||
if [ -f "$CURRENT_RELEASE_FILE" ]; then
|
||||
cp "$CURRENT_RELEASE_FILE" "$PREVIOUS_RELEASE_FILE"
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'tag=%s\n' "$tag"
|
||||
printf 'previous_tag=%s\n' "$previous_tag"
|
||||
printf 'deployed_at=%s\n' "$stamp"
|
||||
printf 'source=%s\n' "$source"
|
||||
printf 'api_image=thebet365-api:%s\n' "$tag"
|
||||
printf 'player_image=thebet365-player:%s\n' "$tag"
|
||||
printf 'admin_image=thebet365-admin:%s\n' "$tag"
|
||||
[ -n "$LAST_DB_BACKUP" ] && printf 'db_backup=%s\n' "$LAST_DB_BACKUP"
|
||||
[ -n "$LAST_UPLOADS_BACKUP" ] && printf 'uploads_backup=%s\n' "$LAST_UPLOADS_BACKUP"
|
||||
} > "$manifest"
|
||||
|
||||
cp "$manifest" "$CURRENT_RELEASE_FILE"
|
||||
log "发布状态已记录: $manifest"
|
||||
}
|
||||
|
||||
print_stack_urls() {
|
||||
local player_port admin_port
|
||||
local player_port admin_port bind_addr tag
|
||||
player_port="$(env_value PLAYER_PORT)"
|
||||
admin_port="$(env_value ADMIN_PORT)"
|
||||
bind_addr="$(env_value BIND_ADDR)"
|
||||
tag="$(current_image_tag)"
|
||||
player_port="${player_port:-8082}"
|
||||
admin_port="${admin_port:-8081}"
|
||||
bind_addr="${bind_addr:-127.0.0.1}"
|
||||
|
||||
printf '\n'
|
||||
printf '%s\n' "部署完成:"
|
||||
printf '%s\n' " 玩家端: http://服务器IP:${player_port}"
|
||||
printf '%s\n' " 管理端: http://服务器IP:${admin_port}"
|
||||
printf '%s\n' " 镜像 tag: ${tag}"
|
||||
printf '%s\n' " 玩家端: http://${bind_addr}:${player_port}"
|
||||
printf '%s\n' " 管理端: http://${bind_addr}:${admin_port}"
|
||||
printf '%s\n' " API: 仅在 Docker 网络内暴露,由 player/admin 容器和宝塔反代链路访问"
|
||||
printf '%s\n' " 状态: docker compose -f docker-compose.prod.yml --env-file .env.docker ps"
|
||||
printf '%s\n' " 当前发布: .deploy/current-release.env"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# 后续更新:可选拉代码/加载镜像,备份数据库,构建或使用新镜像,执行迁移并滚动到新容器。
|
||||
# 后续更新:可选拉代码/加载镜像,备份数据,构建或使用新镜像,执行迁移并滚动到新容器。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -20,7 +20,7 @@ usage() {
|
||||
默认流程:
|
||||
1. 检查 .env.docker
|
||||
2. 启动并等待 postgres / redis
|
||||
3. 备份 PostgreSQL 到 ./backups
|
||||
3. 备份 PostgreSQL 与 uploads 到 ./backups
|
||||
4. 构建 api / player / admin 镜像
|
||||
5. 使用新 api 镜像执行 prisma migrate deploy
|
||||
6. 启动/替换 api、player、admin 容器
|
||||
@@ -29,14 +29,15 @@ usage() {
|
||||
选项:
|
||||
--pull 先执行 git pull --ff-only
|
||||
--images PATH 先 docker load 指定镜像 tar,并自动跳过本机构建
|
||||
--tag TAG 使用指定镜像 tag,并写入 .env.docker 的 IMAGE_TAG
|
||||
--no-build 不构建镜像,直接使用服务器已有镜像
|
||||
--no-backup 跳过数据库备份
|
||||
--no-backup 跳过 PostgreSQL 与 uploads 备份
|
||||
--allow-default-secrets 允许 .env.docker 使用示例密钥,仅测试环境使用
|
||||
-h, --help 显示帮助
|
||||
|
||||
示例:
|
||||
./scripts/deploy-update.sh --pull
|
||||
./scripts/deploy-update.sh --images thebet365-images.tar
|
||||
./scripts/deploy-update.sh --images thebet365-images-v1.2.3.tar --tag v1.2.3
|
||||
./scripts/deploy-update.sh --no-build
|
||||
EOF
|
||||
}
|
||||
@@ -52,6 +53,10 @@ while [ $# -gt 0 ]; do
|
||||
NO_BUILD=true
|
||||
shift 2
|
||||
;;
|
||||
--tag)
|
||||
set_deploy_image_tag "${2:?缺少 --tag 参数值}"
|
||||
shift 2
|
||||
;;
|
||||
--no-build)
|
||||
NO_BUILD=true
|
||||
shift
|
||||
@@ -79,6 +84,14 @@ require_docker
|
||||
ensure_env_file || exit 1
|
||||
validate_prod_env "$ALLOW_DEFAULT_SECRETS"
|
||||
|
||||
if [ -n "$IMAGE_TAR" ] && [ -z "${DEPLOY_IMAGE_TAG:-}" ]; then
|
||||
inferred_tag="$(infer_image_tag_from_tar "$IMAGE_TAR")"
|
||||
if [ -n "$inferred_tag" ]; then
|
||||
set_deploy_image_tag "$inferred_tag"
|
||||
log "从镜像包文件名推断 tag: $inferred_tag"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$PULL_CODE" = true ]; then
|
||||
require_command git
|
||||
log "拉取代码: git pull --ff-only"
|
||||
@@ -93,21 +106,26 @@ start_infra
|
||||
|
||||
if [ "$SKIP_BACKUP" = false ]; then
|
||||
backup_database "pre-update"
|
||||
backup_uploads "pre-update"
|
||||
prune_old_backups
|
||||
else
|
||||
warn "已跳过数据库备份"
|
||||
warn "已跳过 PostgreSQL 与 uploads 备份"
|
||||
fi
|
||||
|
||||
if [ "$NO_BUILD" = false ]; then
|
||||
build_app_images
|
||||
else
|
||||
log "跳过镜像构建,使用服务器已有镜像"
|
||||
require_images_for_current_tag
|
||||
fi
|
||||
|
||||
run_prisma_migrations
|
||||
|
||||
log "启动/更新 api / player / admin"
|
||||
compose up -d api player admin
|
||||
wait_for_service_running api 120
|
||||
wait_for_stack_ready
|
||||
show_prisma_status
|
||||
compose ps
|
||||
persist_image_tag
|
||||
record_release_state "update"
|
||||
print_stack_urls
|
||||
|
||||
@@ -38,9 +38,28 @@ if (-not $SkipBackup) {
|
||||
$backupDir = Join-Path $Root "backups"
|
||||
New-Item -ItemType Directory -Force -Path $backupDir | Out-Null
|
||||
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backupFile = Join-Path $backupDir "thebet365-$stamp.sql"
|
||||
$tempFile = Join-Path $backupDir "thebet365-db-prod-init-$stamp.sql"
|
||||
$backupFile = "$tempFile.gz"
|
||||
Write-Host "[prod-init-db] 备份 → $backupFile"
|
||||
docker compose @composeArgs exec -T postgres pg_dump -U thebet365 -d thebet365 -F p | Set-Content -Encoding utf8 $backupFile
|
||||
docker compose @composeArgs exec -T postgres pg_dump -U thebet365 -d thebet365 -F p | Set-Content -Encoding utf8 $tempFile
|
||||
|
||||
$inputStream = [System.IO.File]::OpenRead($tempFile)
|
||||
$outputStream = [System.IO.File]::Create($backupFile)
|
||||
try {
|
||||
$gzipStream = [System.IO.Compression.GzipStream]::new($outputStream, [System.IO.Compression.CompressionMode]::Compress)
|
||||
try {
|
||||
$inputStream.CopyTo($gzipStream)
|
||||
} finally {
|
||||
$gzipStream.Dispose()
|
||||
}
|
||||
} finally {
|
||||
$inputStream.Dispose()
|
||||
$outputStream.Dispose()
|
||||
}
|
||||
Remove-Item $tempFile -Force
|
||||
|
||||
$hash = (Get-FileHash -Algorithm SHA256 $backupFile).Hash.ToLowerInvariant()
|
||||
"$hash $(Split-Path -Leaf $backupFile)" | Set-Content -Encoding UTF8 "$backupFile.sha256"
|
||||
} else {
|
||||
Write-Host "[prod-init-db] 已跳过备份"
|
||||
}
|
||||
|
||||
@@ -49,9 +49,16 @@ if [ "$SKIP_BACKUP" = false ]; then
|
||||
BACKUP_DIR="$ROOT/backups"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="$BACKUP_DIR/thebet365-$STAMP.sql"
|
||||
BACKUP_FILE="$BACKUP_DIR/thebet365-db-prod-init-$STAMP.sql.gz"
|
||||
echo "[prod-init-db] 备份 PostgreSQL → $BACKUP_FILE"
|
||||
"${COMPOSE[@]}" exec -T postgres pg_dump -U thebet365 -d thebet365 -F p > "$BACKUP_FILE"
|
||||
"${COMPOSE[@]}" exec -T postgres pg_dump -U thebet365 -d thebet365 -F p | gzip -c > "$BACKUP_FILE"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
(cd "$BACKUP_DIR" && sha256sum "$(basename "$BACKUP_FILE")" > "$(basename "$BACKUP_FILE").sha256")
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
(cd "$BACKUP_DIR" && shasum -a 256 "$(basename "$BACKUP_FILE")" > "$(basename "$BACKUP_FILE").sha256")
|
||||
else
|
||||
echo "[prod-init-db] 警告:未找到 sha256sum 或 shasum,跳过校验文件"
|
||||
fi
|
||||
echo "[prod-init-db] 备份完成"
|
||||
else
|
||||
echo "[prod-init-db] 已跳过备份 (--skip-backup)"
|
||||
|
||||
91
scripts/rollback.sh
Executable file
91
scripts/rollback.sh
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# 只回滚 api / player / admin 镜像 tag,不自动恢复数据库。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/deploy-lib.sh
|
||||
source "$SCRIPT_DIR/deploy-lib.sh"
|
||||
|
||||
TARGET_TAG=""
|
||||
SKIP_BACKUP=false
|
||||
ALLOW_DEFAULT_SECRETS=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: scripts/rollback.sh --to TAG [选项]
|
||||
|
||||
默认流程:
|
||||
1. 检查 .env.docker
|
||||
2. 确认本机存在 thebet365-api/player/admin:TAG
|
||||
3. 备份 PostgreSQL 与 uploads 到 ./backups
|
||||
4. 将 api / player / admin 切回指定 TAG
|
||||
5. 等待健康检查并记录发布状态
|
||||
|
||||
注意:
|
||||
本脚本只回滚镜像,不回滚数据库结构或数据。
|
||||
如果目标版本涉及不可逆迁移,必须先按备份文件手工恢复数据库。
|
||||
|
||||
选项:
|
||||
--to TAG 目标镜像 tag,必填
|
||||
--no-backup 跳过 PostgreSQL 与 uploads 备份
|
||||
--allow-default-secrets 允许 .env.docker 使用示例密钥,仅测试环境使用
|
||||
-h, --help 显示帮助
|
||||
|
||||
示例:
|
||||
./scripts/rollback.sh --to v1.2.2
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--to)
|
||||
TARGET_TAG="${2:?缺少 --to 参数值}"
|
||||
shift 2
|
||||
;;
|
||||
--no-backup)
|
||||
SKIP_BACKUP=true
|
||||
shift
|
||||
;;
|
||||
--allow-default-secrets)
|
||||
ALLOW_DEFAULT_SECRETS=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "未知参数: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$TARGET_TAG" ] || die "缺少 --to TAG"
|
||||
set_deploy_image_tag "$TARGET_TAG"
|
||||
|
||||
cd "$ROOT"
|
||||
require_docker
|
||||
ensure_env_file || exit 1
|
||||
validate_prod_env "$ALLOW_DEFAULT_SECRETS"
|
||||
require_images_for_current_tag
|
||||
|
||||
start_infra
|
||||
|
||||
if [ "$SKIP_BACKUP" = false ]; then
|
||||
backup_database "pre-rollback"
|
||||
backup_uploads "pre-rollback"
|
||||
prune_old_backups
|
||||
else
|
||||
warn "已跳过 PostgreSQL 与 uploads 备份"
|
||||
fi
|
||||
|
||||
log "回滚 api / player / admin 到 tag: $(current_image_tag)"
|
||||
compose up -d api player admin
|
||||
wait_for_stack_ready
|
||||
show_prisma_status
|
||||
compose ps
|
||||
persist_image_tag
|
||||
record_release_state "rollback"
|
||||
warn "镜像已回滚;数据库未自动恢复。若此次回滚涉及不可逆迁移,请按备份文件手工恢复数据库。"
|
||||
print_stack_urls
|
||||
Reference in New Issue
Block a user