diff --git a/deploy/ubuntu-deploy.sh b/deploy/ubuntu-deploy.sh index 8653ba6a..9e367512 100755 --- a/deploy/ubuntu-deploy.sh +++ b/deploy/ubuntu-deploy.sh @@ -11,6 +11,7 @@ BACKEND_DATA_DIR="${PROJECT_ROOT}/apps/backend/data" BACKEND_CONTAINER_UID=1001 BACKEND_CONTAINER_GID=1001 HEALTH_URL="" +READY_TIMEOUT="${READY_TIMEOUT:-120}" log() { printf '\033[1;34m[deploy]\033[0m %s\n' "$*" @@ -60,6 +61,8 @@ validate_env_file() { fail ".env 仍包含示例占位值,请替换后再部署。" fi + [[ "${READY_TIMEOUT}" =~ ^[1-9][0-9]*$ ]] || fail "READY_TIMEOUT 必须是正整数秒数,当前值:${READY_TIMEOUT}" + local site_addr site_addr="$(read_env_value CADDY_SITE_ADDR)" [[ -n "${site_addr}" ]] || fail ".env 缺少 CADDY_SITE_ADDR。" @@ -100,28 +103,122 @@ ensure_data_dir() { warn "sudo chown -R ${BACKEND_CONTAINER_UID}:${BACKEND_CONTAINER_GID} ${BACKEND_DATA_DIR}" } -deploy_stack() { +check_compose_config() { + log "检查 Docker Compose 配置" + docker compose config >/dev/null || fail "Docker Compose 配置校验失败。" +} + +pull_base_images() { log "拉取基础镜像" docker compose pull --ignore-buildable +} - log "构建并启动服务" - if docker compose up -d --build; then +build_images() { + log "构建后端镜像" + docker compose build backend || fail "后端镜像构建失败。" + + log "构建 Web 镜像" + docker compose build web || fail "Web 镜像构建失败。" +} + +wait_for_service() { + local service="$1" + local label="$2" + local attempt container_id status health_status + + log "等待 ${label} 就绪" + + for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do + container_id="$(docker compose ps -q "${service}" 2>/dev/null || true)" + if [[ -n "${container_id}" ]]; then + status="$(docker inspect -f '{{.State.Status}}' "${container_id}" 2>/dev/null || true)" + health_status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "${container_id}" 2>/dev/null || true)" + + if [[ -n "${health_status}" && "${health_status}" == "healthy" ]]; then + log "${label} 已就绪" + return + fi + + if [[ -z "${health_status}" && "${status}" == "running" ]]; then + log "${label} 已启动" + return + fi + + if [[ "${status}" == "exited" || "${status}" == "dead" ]]; then + break + fi + fi + + sleep 1 + done + + print_diagnostics + fail "${label} 启动超时,请根据上面的日志排查。" +} + +start_postgres() { + log "启动 PostgreSQL" + if docker compose up -d --no-deps postgres; then + wait_for_service postgres "PostgreSQL" return fi print_diagnostics - fail "docker compose 启动失败,请根据上面的日志排查。" + fail "PostgreSQL 启动失败,请根据上面的日志排查。" +} + +run_database_migrations() { + log "执行数据库迁移" + if docker compose run --rm --no-deps backend node dist/db/migrate.js; then + log "数据库迁移完成" + return + fi + + print_diagnostics + fail "数据库迁移失败,请根据上面的日志排查。" +} + +restart_backend() { + log "重建并启动后端" + if docker compose up -d --no-deps --force-recreate backend; then + wait_for_service backend "后端" + return + fi + + print_diagnostics + fail "后端启动失败,请根据上面的日志排查。" +} + +restart_web() { + log "重建并启动 Web 入口" + if docker compose up -d --no-deps --force-recreate --remove-orphans web; then + wait_for_service web "Web" + return + fi + + print_diagnostics + fail "Web 启动失败,请根据上面的日志排查。" +} + +deploy_stack() { + check_compose_config + pull_base_images + build_images + start_postgres + run_database_migrations + restart_backend + restart_web } wait_for_health() { log "等待服务健康检查:${HEALTH_URL}" - for _ in $(seq 1 60); do + for _ in $(seq 1 "${READY_TIMEOUT}"); do if curl -fsS "${HEALTH_URL}" >/dev/null 2>&1; then log "服务已就绪" return fi - sleep 2 + sleep 1 done print_diagnostics