feat: deploy/deploy.sh 自动化部署体系 — 双tag镜像/快照/迁移门控/回滚,退役根deploy.sh
- compose 四服务补 image: ${BACKEND_TAG}/${FRONTEND_TAG} 插值(换 tag 回滚前提)
- deploy/deploy.sh:工作区校验→PREV_SHA→pull→predeploy 快照(-Fc)→destructive 判定→
build+双tag→.pending-deploy→迁移退出码门控→up -d --no-deps→健康轮询→versions.log→
镜像治理;支持 --dry-run / flock / 基础设施健康门 / 多head预检
- deploy/rollback.sh:双入口(手动读 versions.log 跳 ROLLBACK 行 / --auto 读
.pending-deploy);destructive 走 pg_restore --clean;双 tag 原子切换
- deploy/hotfix.sh:紧急单文件 cp + 强制收口提醒
- .gitignore 补 .pending-deploy/versions.log(运行时状态)
- 退役根 deploy.sh + deploy.env.example(SSH 旧模型,被 deploy/deploy.sh 取代)
This commit is contained in:
@@ -46,3 +46,7 @@ backend/data/
|
|||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Deploy runtime state (per-server, not for repo)
|
||||||
|
deploy/.pending-deploy
|
||||||
|
deploy/versions.log
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
# ═══════════════════════════════════════════════════════════════════
|
|
||||||
# SciLit — 部署配置(供 deploy.sh 使用)
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
|
||||||
#
|
|
||||||
# 复制为 deploy.env,填入实际值,不要提交到 git:
|
|
||||||
# cp deploy.env.example deploy.env
|
|
||||||
#
|
|
||||||
# 然后在 deploy.env 同目录运行:
|
|
||||||
# source deploy.env && ./deploy.sh
|
|
||||||
|
|
||||||
# 服务器 SSH 信息
|
|
||||||
SCILIT_HOST=your-server.com
|
|
||||||
SCILIT_USER=root
|
|
||||||
SCILIT_PATH=/root/scilit-oncology
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
|
||||||
# SciLit — 生产环境一键部署脚本
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
|
||||||
#
|
|
||||||
# 用法:
|
|
||||||
# export SCILIT_HOST=your-server.com
|
|
||||||
# ./deploy.sh # 部署到默认服务器
|
|
||||||
# ./deploy.sh --skip-tests # 跳过本地测试,直接部署
|
|
||||||
# ./deploy.sh --help # 查看全部选项
|
|
||||||
#
|
|
||||||
# 前置条件:
|
|
||||||
# - 本地: SSH 可连接到服务器(推荐用 ~/.ssh/config)
|
|
||||||
# - 服务器: 已装 Docker + docker compose,已 clone 项目
|
|
||||||
# - 服务器: .env 文件已配置好
|
|
||||||
# - 代码仓库: 支持 Gitee / GitHub / 自建 Git(git pull 能工作即可)
|
|
||||||
#
|
|
||||||
# 远程仓库配置:
|
|
||||||
# - 本地 git remote 指向 Gitee:
|
|
||||||
# git remote add gitee https://gitee.com/你的用户名/scilit-oncology.git
|
|
||||||
# # deploy.sh 会尝试 push 到 origin(可设为 Gitee)
|
|
||||||
# - 服务器上 git remote 也指向 Gitee:
|
|
||||||
# git remote set-url origin https://gitee.com/你的用户名/scilit-oncology.git
|
|
||||||
# - 可选: GitHub 同时保留用作备份:
|
|
||||||
# git remote add github https://github.com/你的用户名/scilit-oncology.git
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ── 默认配置 ─────────────────────────────────────────────────────
|
|
||||||
# 改为你的服务器信息
|
|
||||||
SERVER_HOST="${SCILIT_HOST:-your-server.com}"
|
|
||||||
SERVER_USER="${SCILIT_USER:-root}"
|
|
||||||
SERVER_PATH="${SCILIT_PATH:-/root/scilit-oncology}"
|
|
||||||
|
|
||||||
# 本地项目根目录
|
|
||||||
LOCAL_ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
|
|
||||||
# ── 颜色 ─────────────────────────────────────────────────────────
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
info() { echo -e "${GREEN}[✓]${NC} $1"; }
|
|
||||||
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
|
||||||
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
|
||||||
|
|
||||||
# ── 参数解析 ─────────────────────────────────────────────────────
|
|
||||||
SKIP_TESTS=false
|
|
||||||
ENV_FILE=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--skip-tests) SKIP_TESTS=true; shift ;;
|
|
||||||
--env) ENV_FILE="$2"; shift 2 ;;
|
|
||||||
--help)
|
|
||||||
echo "用法: $0 [--skip-tests] [--env .env.prod]"
|
|
||||||
echo ""
|
|
||||||
echo " --skip-tests 跳过本地测试和 lint"
|
|
||||||
echo " --env FILE 指定服务器上的 .env 文件路径(默认: {SERVER_PATH}/.env)"
|
|
||||||
exit 0 ;;
|
|
||||||
*) error "未知参数: $1,使用 --help 查看用法" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# ── 第一步:本地检查 + 测试 ─────────────────────────────────────
|
|
||||||
echo "════════════════════════════════════════════"
|
|
||||||
echo " SciLit 部署"
|
|
||||||
echo " 目标: ${SERVER_USER}@${SERVER_HOST}"
|
|
||||||
echo "════════════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ "$SKIP_TESTS" = false ]; then
|
|
||||||
echo "── 1/4 本地测试 ──"
|
|
||||||
cd "$LOCAL_ROOT/backend"
|
|
||||||
info "运行后端测试..."
|
|
||||||
python -m pytest tests/ -v --no-cov --tb=short 2>&1 | tail -5
|
|
||||||
local EXIT_CODE=$?
|
|
||||||
if [ $EXIT_CODE -ne 0 ]; then
|
|
||||||
error "后端测试失败(exit code $EXIT_CODE),中止部署。用 --skip-tests 跳过。"
|
|
||||||
fi
|
|
||||||
info "后端测试通过"
|
|
||||||
|
|
||||||
cd "$LOCAL_ROOT/frontend"
|
|
||||||
info "构建前端(验证)..."
|
|
||||||
npm run build 2>&1 | tail -3
|
|
||||||
info "前端构建通过"
|
|
||||||
cd "$LOCAL_ROOT"
|
|
||||||
else
|
|
||||||
echo "── 1/4 本地测试 ── 已跳过"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 第二步:推送代码 ─────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo "── 2/4 推送代码 ──"
|
|
||||||
cd "$LOCAL_ROOT"
|
|
||||||
if git status --porcelain | grep -q .; then
|
|
||||||
warn "有未提交的改动,建议先提交再部署。继续?(y/N)"
|
|
||||||
read -r yn
|
|
||||||
if [ "$yn" != "y" ] && [ "$yn" != "Y" ]; then
|
|
||||||
error "已取消"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 尝试推送到 GitHub(失败不阻塞)
|
|
||||||
if git push origin main 2>&1; then
|
|
||||||
info "GitHub push 成功"
|
|
||||||
else
|
|
||||||
warn "GitHub push 失败(网络问题),继续部署..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 第三步:服务器拉取 + 构建 + 启动 ────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo "── 3/4 服务器部署 ──"
|
|
||||||
|
|
||||||
DEPLOY_CMD=$(cat << SSHCMD
|
|
||||||
set -e
|
|
||||||
cd ${SERVER_PATH}
|
|
||||||
|
|
||||||
echo " 拉取代码..."
|
|
||||||
git pull origin main 2>&1 || echo " [!] git pull 失败,继续用本地代码"
|
|
||||||
|
|
||||||
echo " 构建并启动..."
|
|
||||||
docker compose -f docker-compose.prod.yml build 2>&1 | tail -3
|
|
||||||
|
|
||||||
# 优雅重启(不中断现有连接)
|
|
||||||
echo " 正在更新服务..."
|
|
||||||
docker compose -f docker-compose.prod.yml up -d --no-deps --remove-orphans 2>&1
|
|
||||||
|
|
||||||
echo " 等待就绪..."
|
|
||||||
sleep 5
|
|
||||||
curl -sf http://localhost:8000/health && echo " [✓] backend 健康检查通过" || echo " [!] backend 健康检查失败"
|
|
||||||
curl -sf http://localhost:80 && echo " [✓] frontend 健康检查通过" || echo " [!] frontend 健康检查失败"
|
|
||||||
|
|
||||||
echo " 清理旧镜像..."
|
|
||||||
docker image prune -f 2>&1 | tail -1
|
|
||||||
SSHCMD
|
|
||||||
)
|
|
||||||
|
|
||||||
ssh -o StrictHostKeyChecking=no "${SERVER_USER}@${SERVER_HOST}" bash <<< "$DEPLOY_CMD"
|
|
||||||
|
|
||||||
info "服务器部署完成"
|
|
||||||
|
|
||||||
# ── 第四步:日志 ─────────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo "── 4/4 查看日志 ──"
|
|
||||||
echo ""
|
|
||||||
echo " 实时日志: ssh ${SERVER_USER}@${SERVER_HOST} 'cd ${SERVER_PATH} && docker compose -f docker-compose.prod.yml logs -f'"
|
|
||||||
echo " backend: ssh ${SERVER_USER}@${SERVER_HOST} 'cd ${SERVER_PATH} && docker compose -f docker-compose.prod.yml logs -f backend'"
|
|
||||||
echo " 重新部署: cd $(pwd) && bash deploy.sh"
|
|
||||||
echo ""
|
|
||||||
info "部署完毕 🎉"
|
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# deploy.sh — SciLit 生产一键部署(docker-compose.prod.yml)
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# bash deploy/deploy.sh # 正式部署(在服务器 /root/scilit 下执行)
|
||||||
|
# bash deploy/deploy.sh --dry-run # 只打印将执行的命令,不执行任何变更
|
||||||
|
# bash deploy/deploy.sh --help
|
||||||
|
#
|
||||||
|
# 前置条件:
|
||||||
|
# - 在 git 仓库根目录的 deploy/ 下执行(自动定位 REPO_ROOT 与 .env)
|
||||||
|
# - 基础设施(postgres/redis/elasticsearch/minio)已在跑(脚本内置健康门)
|
||||||
|
# - 生产部署期间勿并发(脚本自带 flock 单实例锁)
|
||||||
|
#
|
||||||
|
# 流程(对应 docs/16 §2 设计):
|
||||||
|
# 遗留标记检查 → source .env → flock → 工作区校验 → 基础设施健康门 →
|
||||||
|
# PREV_SHA → git pull → before alembic head → predeploy 快照 →
|
||||||
|
# destructive 判定 → build + 双 tag → .pending-deploy → 迁移(退出码门控) →
|
||||||
|
# after alembic head → up -d --no-deps → 健康轮询 → versions.log → 镜像治理
|
||||||
|
#
|
||||||
|
# 失败处理分两段:
|
||||||
|
# · 迁移前失败(快照/构建阶段)→ 容器仍是旧的,只清理中间态,不触发回滚
|
||||||
|
# · 迁移已跑/容器已切后失败 → 调 rollback.sh --auto(读本次 .pending-deploy)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
COMPOSE_FILE="docker-compose.prod.yml"
|
||||||
|
COMPOSE=(docker compose -f "$COMPOSE_FILE")
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/data/backups}"
|
||||||
|
KEEP_TAGS=10 # 保留最近 N 个带 tag 版本镜像(§5 count 制)
|
||||||
|
HEALTH_POLLS=12 # 健康轮询次数 × 5s = 60s 上限
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
||||||
|
|
||||||
|
DRY_RUN=0
|
||||||
|
# 所有变更命令统一走 run():--dry-run 下只打印不执行
|
||||||
|
run() {
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo -e "${YELLOW}[DRY-RUN]${NC} $*"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
用法: bash deploy/deploy.sh [--dry-run] [--help]
|
||||||
|
|
||||||
|
--dry-run 只打印将执行的命令,不执行任何变更(SHA 显示当前状态)
|
||||||
|
--help 显示本帮助
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) DRY_RUN=1 ;;
|
||||||
|
--help) usage; exit 0 ;;
|
||||||
|
*) usage; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 1. 遗留 .pending-deploy 检测(I:上次部署中途崩溃的残留,不静默覆盖)──
|
||||||
|
if [ -f "$DEPLOY_DIR/.pending-deploy" ]; then
|
||||||
|
error "⚠️ 上一次部署异常退出,残留 ${DEPLOY_DIR}/.pending-deploy。
|
||||||
|
请先确认当前状态(容器/迁移/versions.log)再继续,然后删除该标记或人工处理后重跑。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. source .env(G:PG_PASSWORD/REDIS_PASSWORD 供 pg_dump 备选与探活)──
|
||||||
|
if [ ! -f "$REPO_ROOT/.env" ]; then
|
||||||
|
error "未找到 ${REPO_ROOT}/.env(含密钥,不进 git)。请先在服务器配置。"
|
||||||
|
fi
|
||||||
|
set -a; source "$REPO_ROOT/.env"; set +a
|
||||||
|
|
||||||
|
# ── 3. flock 单实例锁(防两人/两终端并发部署)──
|
||||||
|
exec 9>/tmp/scilit-deploy.lock
|
||||||
|
flock -n 9 || error "已有部署/回滚在进行中(/tmp/scilit-deploy.lock 被锁)。"
|
||||||
|
|
||||||
|
# ── 4. 工作区校验(防 hotfix 残留导致 git pull 冲突)──
|
||||||
|
# 只拦「已跟踪文件的改动」(这才是 pull 冲突来源);未跟踪文件(如服务器运维
|
||||||
|
# 工具 backup.sh/tmp_*.py)warn 不拦
|
||||||
|
STALE_CHANGES="$(git status --porcelain | grep -v '^??' || true)"
|
||||||
|
if [ -n "$STALE_CHANGES" ]; then
|
||||||
|
error "工作区有已跟踪文件改动(可能为 hotfix 残留,git pull 会冲突):
|
||||||
|
$STALE_CHANGES"
|
||||||
|
fi
|
||||||
|
UNTRACKED="$(git status --porcelain | grep '^??' || true)"
|
||||||
|
if [ -n "$UNTRACKED" ]; then
|
||||||
|
warn "存在未跟踪文件(不影响 pull,仅当新代码含同名文件时需注意):
|
||||||
|
$UNTRACKED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 5. 基础设施健康门(P14/N5:有 healthcheck 判 healthy、无则判 running)──
|
||||||
|
infra_ok() {
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "[DRY-RUN] 健康门:postgres/redis/elasticsearch/minio 模拟通过"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local svc cid health status
|
||||||
|
for svc in postgres redis elasticsearch minio; do
|
||||||
|
cid=$("${COMPOSE[@]}" ps -q "$svc" 2>/dev/null || true)
|
||||||
|
if [ -z "$cid" ]; then
|
||||||
|
error "基础设施 $svc 未运行。请先冷启动:
|
||||||
|
docker compose -f $COMPOSE_FILE up -d postgres redis elasticsearch minio gitea"
|
||||||
|
fi
|
||||||
|
health=$(docker inspect --format '{{.State.Health.Status}}' "$cid" 2>/dev/null || true)
|
||||||
|
status=$(docker inspect --format '{{.State.Status}}' "$cid" 2>/dev/null || true)
|
||||||
|
if [ -n "$health" ]; then
|
||||||
|
[ "$health" = "healthy" ] || error "基础设施 $svc health=$health(期望 healthy)。请先确认状态再部署。"
|
||||||
|
else
|
||||||
|
[ "$status" = "running" ] || error "基础设施 $svc status=$status(无 healthcheck,期望 running)。"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
info "基础设施健康门通过"
|
||||||
|
}
|
||||||
|
infra_ok
|
||||||
|
|
||||||
|
# ── 6. 采 PREV_SHA(F:destructive 判定基线,必须在 git pull 之前)──
|
||||||
|
PREV_SHA="$(git rev-parse HEAD)"
|
||||||
|
info "当前生产版本: $PREV_SHA"
|
||||||
|
|
||||||
|
# ── 7. git pull ──
|
||||||
|
run git pull
|
||||||
|
|
||||||
|
# ── 取 alembic 当前 revision ──
|
||||||
|
# 用 psql 直读 alembic_version(postgres 已健康门保证运行;容器内 socket trust 免密)。
|
||||||
|
# 不用 `run --rm backend alembic current`:before 阶段新镜像还没 build,image:latest
|
||||||
|
# 不存在会触发自动 build(打乱"快照在先"顺序)。表不存在(首次部署)返回空,v5 容错。
|
||||||
|
get_db_rev() {
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then echo "-"; return 0; fi
|
||||||
|
"${COMPOSE[@]}" exec -T postgres psql -U scilit -d scilit -tAc \
|
||||||
|
"SELECT version_num FROM alembic_version" 2>/dev/null | tr -d '[:space:]' | cut -c1-12
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 8. before alembic head(空则记 '-',首次部署容错)──
|
||||||
|
ALEMBIC_BEFORE="$(get_db_rev || true)"
|
||||||
|
info "迁移前 alembic head: ${ALEMBIC_BEFORE:-(空/首次)}"
|
||||||
|
|
||||||
|
# ── 9. predeploy 全量快照(-Fc custom 格式,pg_restore 专用;含全部表)──
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
SNAPSHOT=""
|
||||||
|
snapshot_take() {
|
||||||
|
local ts
|
||||||
|
ts="$(date +%Y%m%d_%H%M%S)"
|
||||||
|
SNAPSHOT="$BACKUP_DIR/predeploy_$ts.dump"
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo -e "${YELLOW}[DRY-RUN]${NC} pg_dump → $SNAPSHOT"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# 容器内 pg_dump(不经宿主端口,与生产 backup.sh 同机制)
|
||||||
|
PGPASSWORD="${PG_PASSWORD}" "${COMPOSE[@]}" exec -T postgres \
|
||||||
|
pg_dump -Fc --no-owner --no-privileges -U scilit -d scilit > "$SNAPSHOT"
|
||||||
|
if [ ! -s "$SNAPSHOT" ]; then
|
||||||
|
rm -f "$SNAPSHOT"
|
||||||
|
error "predeploy 快照生成失败(空文件),中止部署。"
|
||||||
|
fi
|
||||||
|
info "predeploy 快照: $SNAPSHOT"
|
||||||
|
}
|
||||||
|
snapshot_take
|
||||||
|
|
||||||
|
# predeploy 快照保留最近 N=3(E1:count 判定 + sort | head -n -3 | xargs -r rm)
|
||||||
|
prune_snapshots() {
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then return 0; fi
|
||||||
|
local count
|
||||||
|
count="$(ls "$BACKUP_DIR"/predeploy_*.dump 2>/dev/null | wc -l)"
|
||||||
|
if [ "$count" -gt 3 ]; then
|
||||||
|
ls "$BACKUP_DIR"/predeploy_*.dump | sort | head -n -3 | xargs -r rm -f
|
||||||
|
info "已清理旧 predeploy 快照(保留最近 3 个)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
prune_snapshots
|
||||||
|
|
||||||
|
# ── 10. destructive 判定(P12 细化规则 + destructive_* 文件名/# DESTRUCTIVE 双保险)──
|
||||||
|
NEW_SHA="$(git rev-parse --short HEAD)"
|
||||||
|
detect_destructive() {
|
||||||
|
local prev="$1" new="$2" files f fn
|
||||||
|
files="$(git diff --name-only "$prev" "$new" -- alembic/versions/)"
|
||||||
|
[ -z "$files" ] && { echo "否"; return 0; }
|
||||||
|
for f in $files; do
|
||||||
|
fn="$(basename "$f")"
|
||||||
|
case "$fn" in
|
||||||
|
destructive_*) echo "是"; return 0 ;;
|
||||||
|
esac
|
||||||
|
if grep -q '# DESTRUCTIVE' "$REPO_ROOT/$f" 2>/dev/null; then echo "是"; return 0; fi
|
||||||
|
done
|
||||||
|
if git diff "$prev" "$new" -- alembic/versions/ | grep '^+' | grep -qE \
|
||||||
|
'op\.drop_(table|column|index)|op\.rename_(table|column|index)|op\.alter_column|existing_type|op\.execute[^)]*(DROP|DELETE|UPDATE|TRUNCATE)'; then
|
||||||
|
echo "是"; return 0
|
||||||
|
fi
|
||||||
|
echo "否"
|
||||||
|
}
|
||||||
|
DESTRUCTIVE="$(detect_destructive "$PREV_SHA" "$(git rev-parse HEAD)")"
|
||||||
|
info "本次迁移 destructive: $DESTRUCTIVE"
|
||||||
|
|
||||||
|
# ── 11. build + 双 tag(build 前 export,compose build 直接打 ${BACKEND_TAG} 标签)──
|
||||||
|
if [ "$PREV_SHA" = "$(git rev-parse HEAD)" ] && [ -z "$(git diff --name-only "$PREV_SHA" -- docker-compose.prod.yml deploy)" ]; then
|
||||||
|
warn "代码无更新(HEAD 未变),执行幂等重部署"
|
||||||
|
fi
|
||||||
|
export BACKEND_TAG="$NEW_SHA" FRONTEND_TAG="$NEW_SHA"
|
||||||
|
info "构建并打标签: backend/frontend @ $NEW_SHA"
|
||||||
|
run "${COMPOSE[@]}" build backend frontend
|
||||||
|
|
||||||
|
# ── 12. 写 .pending-deploy(N1:快照后、迁移前;失败自动回滚唯一数据源)──
|
||||||
|
if [ "$DRY_RUN" -ne 1 ]; then
|
||||||
|
echo "$NEW_SHA $NEW_SHA $DESTRUCTIVE $SNAPSHOT" > "$DEPLOY_DIR/.pending-deploy"
|
||||||
|
fi
|
||||||
|
info "写入部署标记: $DEPLOY_DIR/.pending-deploy"
|
||||||
|
|
||||||
|
# ── 13. alembic 多 head 预检(P15)+ 迁移(K:run --no-deps --rm,退出码即真值)──
|
||||||
|
check_single_head() {
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then return 0; fi
|
||||||
|
local n
|
||||||
|
n="$("${COMPOSE[@]}" run --no-deps --rm backend alembic -c alembic/alembic.ini heads 2>/dev/null | grep -cE '[0-9a-f]{12}\b' || true)"
|
||||||
|
[ "$n" -le 1 ] || error "alembic 多 head($n 个),请先合并(alembic merge 或串行 rebase)。"
|
||||||
|
}
|
||||||
|
check_single_head
|
||||||
|
|
||||||
|
auto_rollback() {
|
||||||
|
echo -e "${RED}[✗] 部署部分完成(迁移已跑/容器已切),触发自动回滚${NC}"
|
||||||
|
"$DEPLOY_DIR/rollback.sh" --auto || warn "自动回滚失败,请人工介入(详见 docs/16 §3 回滚 runbook)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
info "执行迁移: alembic upgrade head(后台 frontend/backend 暂不切换)"
|
||||||
|
if ! run "${COMPOSE[@]}" run --no-deps --rm backend alembic -c alembic/alembic.ini upgrade head; then
|
||||||
|
auto_rollback
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 14. after alembic head ──
|
||||||
|
ALEMBIC_AFTER="$(get_db_rev || true)"
|
||||||
|
info "迁移后 alembic head: ${ALEMBIC_AFTER:-(空/首次)}"
|
||||||
|
|
||||||
|
# ── 15. up -d --no-deps(显式指定应用容器,不动基础设施)──
|
||||||
|
info "切换容器到新版本(backend worker frontend)"
|
||||||
|
if ! run "${COMPOSE[@]}" up -d --no-deps backend worker frontend; then
|
||||||
|
auto_rollback
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 16. 健康轮询(V6:up 后仍在 start_period,轮询而非单次 curl)──
|
||||||
|
wait_backend_healthy() {
|
||||||
|
local i
|
||||||
|
for i in $(seq 1 "$HEALTH_POLLS"); do
|
||||||
|
if "${COMPOSE[@]}" exec -T backend curl -sf http://localhost:8000/health >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
check_frontend() {
|
||||||
|
"${COMPOSE[@]}" exec -T frontend wget -q -O- http://localhost/ >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
info "[DRY-RUN] 健康轮询:backend /health + frontend 200(模拟通过)"
|
||||||
|
elif wait_backend_healthy; then
|
||||||
|
info "backend /health 通过"
|
||||||
|
if check_frontend; then
|
||||||
|
info "frontend 200 通过"
|
||||||
|
else
|
||||||
|
warn "frontend 健康检查未过,但 backend 已就绪——请人工确认前端状态"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
auto_rollback
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 17. 成功 → 写 versions.log + 删 .pending-deploy ──
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo -e "${YELLOW}[DRY-RUN]${NC} 追加 versions.log + 删除 .pending-deploy"
|
||||||
|
else
|
||||||
|
echo "DEPLOY $NEW_SHA $NEW_SHA $DESTRUCTIVE ${ALEMBIC_BEFORE:-} ${ALEMBIC_AFTER:-} $(date '+%Y-%m-%d %H:%M:%S %z')" \
|
||||||
|
>> "$DEPLOY_DIR/versions.log"
|
||||||
|
tail -n 200 "$DEPLOY_DIR/versions.log" > "$DEPLOY_DIR/versions.log.tmp"
|
||||||
|
mv "$DEPLOY_DIR/versions.log.tmp" "$DEPLOY_DIR/versions.log"
|
||||||
|
rm -f "$DEPLOY_DIR/.pending-deploy"
|
||||||
|
fi
|
||||||
|
info "部署成功,已写入 versions.log"
|
||||||
|
|
||||||
|
# ── 18. 镜像治理(只按 count 清理带 tag 版本镜像;age-prune 只清 dangling)──
|
||||||
|
prune_images() {
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then return 0; fi
|
||||||
|
local repo count to_remove t
|
||||||
|
for repo in scilit/backend scilit/frontend; do
|
||||||
|
count="$(docker images "$repo" --format '{{.Tag}}' | grep -v '^latest$' | wc -l)"
|
||||||
|
if [ "$count" -gt "$KEEP_TAGS" ]; then
|
||||||
|
to_remove="$(docker images "$repo" --format '{{.Tag}} {{.CreatedAt}}' \
|
||||||
|
| grep -v '^latest ' | sort -k2 | head -n $((count - KEEP_TAGS)) | awk '{print $1}')"
|
||||||
|
for t in $to_remove; do
|
||||||
|
docker rmi "$repo:$t" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
info "已清理 $repo 旧版本镜像(保留最近 $KEEP_TAGS 个)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
docker image prune --filter "until=168h" -f >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
prune_images
|
||||||
|
|
||||||
|
info "全部完成 ✅ 当前版本: $NEW_SHA"
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# hotfix.sh — 紧急单文件修复(仅限紧急情况)
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# bash deploy/hotfix.sh <服务名> <宿主文件> <容器内路径>
|
||||||
|
# bash deploy/hotfix.sh backend /tmp/fix.py /app/app/services/fix.py
|
||||||
|
#
|
||||||
|
# ⚠️ 强制收口:
|
||||||
|
# docker cp 只是把文件放进运行中的容器,不进镜像——容器一重建即丢。
|
||||||
|
# 用后必须立即: ① git 提交改动 ② 走 deploy.sh 正式部署,否则手工改动与仓库不一致(8-09 教训)。
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
COMPOSE_FILE="docker-compose.prod.yml"
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
||||||
|
|
||||||
|
if [ "$#" -ne 3 ]; then
|
||||||
|
echo "用法: bash deploy/hotfix.sh <服务名> <宿主文件> <容器内路径>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVICE="$1"; HOST_FILE="$2"; CONTAINER_PATH="$3"
|
||||||
|
|
||||||
|
[ -f "$HOST_FILE" ] || error "宿主文件不存在: $HOST_FILE"
|
||||||
|
|
||||||
|
info "复制 $HOST_FILE → $SERVICE:$CONTAINER_PATH"
|
||||||
|
docker compose -f "$COMPOSE_FILE" cp "$HOST_FILE" "$SERVICE:$CONTAINER_PATH"
|
||||||
|
|
||||||
|
if [ "$SERVICE" = "backend" ] || [ "$SERVICE" = "worker" ]; then
|
||||||
|
info "backend/worker 需重启才生效: docker compose -f $COMPOSE_FILE restart $SERVICE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}════════════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${RED}⚠️ hotfix 是脆弱窗口:docker cp 不进镜像,容器重建即丢!${NC}"
|
||||||
|
echo -e "${RED} 必须立即完成以下收口,否则改动丢失且与仓库不一致:${NC}"
|
||||||
|
echo -e "${RED} 1. 把改动同步回仓库(git 提交 + push)${NC}"
|
||||||
|
echo -e "${RED} 2. 尽快走正式部署: bash deploy/deploy.sh${NC}"
|
||||||
|
echo -e "${RED}════════════════════════════════════════════════════════════════${NC}"
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# rollback.sh — 回滚到上一个成功部署版本(backend+frontend 双 tag 原子切换)
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# bash deploy/rollback.sh # 手动回滚:versions.log 最近一次成功部署
|
||||||
|
# bash deploy/rollback.sh <backend_sha> [<frontend_sha>] # 回滚到指定版本
|
||||||
|
# bash deploy/rollback.sh --auto # 部署失败自动回滚(读本次 .pending-deploy)
|
||||||
|
#
|
||||||
|
# 双入口读不同来源(N1 核心洞:失败自动回滚绝不读 versions.log 判 destructive):
|
||||||
|
# · 手动回滚: 旧 sha + destructive 从 versions.log 读(跳过 ROLLBACK 行,E5)
|
||||||
|
# · --auto: 读本次 .pending-deploy(<sha> <sha> <destructive> <快照路径>)
|
||||||
|
#
|
||||||
|
# 数据回退主路径 = pg_restore predeploy 快照(数据 + schema 一起回,先清后恢复);
|
||||||
|
# alembic downgrade 只反向 schema 不恢复数据,仅用于可逆非破坏性调整。
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
COMPOSE_FILE="docker-compose.prod.yml"
|
||||||
|
COMPOSE=(docker compose -f "$COMPOSE_FILE")
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
||||||
|
|
||||||
|
# ── 参数 ──
|
||||||
|
MODE="manual"
|
||||||
|
BACKEND_SHA_ARG=""; FRONTEND_SHA_ARG=""
|
||||||
|
case "${1:-}" in
|
||||||
|
--auto) MODE="auto" ;;
|
||||||
|
"") ;;
|
||||||
|
-*) error "未知参数: $1(用法: rollback.sh [--auto | <backend_sha> [<frontend_sha>]])" ;;
|
||||||
|
*) BACKEND_SHA_ARG="$1"; FRONTEND_SHA_ARG="${2:-$1}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── 前置:source .env + flock ──
|
||||||
|
if [ ! -f "$REPO_ROOT/.env" ]; then
|
||||||
|
error "未找到 ${REPO_ROOT}/.env。"
|
||||||
|
fi
|
||||||
|
set -a; source "$REPO_ROOT/.env"; set +a
|
||||||
|
|
||||||
|
exec 9>/tmp/scilit-deploy.lock
|
||||||
|
flock -n 9 || error "已有部署/回滚在进行中(/tmp/scilit-deploy.lock 被锁)。"
|
||||||
|
|
||||||
|
# ── 解析回滚目标 ──
|
||||||
|
# 从 versions.log 取最近一条非 ROLLBACK 行(E5:跳过回滚事件,防回滚循环)
|
||||||
|
last_deploy_line() {
|
||||||
|
awk '/^DEPLOY /{line=$0} END{print line}' "$DEPLOY_DIR/versions.log" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
OLD_BACKEND_SHA=""; OLD_FRONTEND_SHA=""; DESTRUCTIVE="否"; SNAPSHOT=""
|
||||||
|
|
||||||
|
if [ "$MODE" = "auto" ]; then
|
||||||
|
[ -f "$DEPLOY_DIR/.pending-deploy" ] || error "--auto 需存在 .pending-deploy(本次部署失败残留),未找到。"
|
||||||
|
read -r NEW_BACKEND_SHA NEW_FRONTEND_SHA DESTRUCTIVE SNAPSHOT < "$DEPLOY_DIR/.pending-deploy"
|
||||||
|
# 上一个成功版本 = versions.log 最近一条 DEPLOY 行
|
||||||
|
LINE="$(last_deploy_line)"
|
||||||
|
[ -n "$LINE" ] || error "versions.log 无历史成功部署记录,无法确定回滚目标。"
|
||||||
|
OLD_BACKEND_SHA="$(echo "$LINE" | awk '{print $2}')"
|
||||||
|
OLD_FRONTEND_SHA="$(echo "$LINE" | awk '{print $3}')"
|
||||||
|
info "自动回滚: 本次($NEW_BACKEND_SHA, destructive=$DESTRUCTIVE) → 上一版($OLD_BACKEND_SHA)"
|
||||||
|
else
|
||||||
|
if [ -n "$BACKEND_SHA_ARG" ]; then
|
||||||
|
OLD_BACKEND_SHA="$BACKEND_SHA_ARG"; OLD_FRONTEND_SHA="$FRONTEND_SHA_ARG"
|
||||||
|
# 指定版本:destructive 从 versions.log 匹配该 sha 的最近一行;缺省按否并提示
|
||||||
|
LINE="$(awk -v s="$OLD_BACKEND_SHA" '$2==s || $3==s{line=$0} END{print line}' "$DEPLOY_DIR/versions.log" 2>/dev/null || true)"
|
||||||
|
if [ -n "$LINE" ]; then
|
||||||
|
DESTRUCTIVE="$(echo "$LINE" | awk '{print $4}')"
|
||||||
|
else
|
||||||
|
warn "versions.log 无 $OLD_BACKEND_SHA 记录,destructive 按「否」处理(数据不回退)。如该版本含破坏性迁移,请人工确认。"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
LINE="$(last_deploy_line)"
|
||||||
|
[ -n "$LINE" ] || error "versions.log 无历史成功部署记录。"
|
||||||
|
OLD_BACKEND_SHA="$(echo "$LINE" | awk '{print $2}')"
|
||||||
|
OLD_FRONTEND_SHA="$(echo "$LINE" | awk '{print $3}')"
|
||||||
|
DESTRUCTIVE="$(echo "$LINE" | awk '{print $4}')"
|
||||||
|
info "手动回滚: 最近成功部署 $OLD_BACKEND_SHA(destructive=$DESTRUCTIVE)"
|
||||||
|
fi
|
||||||
|
# 手动 destructive 数据回退:优先本次 predeploy 快照(无则最近一份,尽力而为)
|
||||||
|
if [ "$DESTRUCTIVE" = "是" ]; then
|
||||||
|
SNAPSHOT="$(ls -t /data/backups/predeploy_*.dump 2>/dev/null | head -1 || true)"
|
||||||
|
[ -n "$SNAPSHOT" ] || error "destructive=是 但找不到 predeploy 快照(/data/backups/predeploy_*.dump),数据无法回退。"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 校验旧 tag 镜像存在 ──
|
||||||
|
image_exists() { docker image inspect "$1" >/dev/null 2>&1; }
|
||||||
|
if ! image_exists "scilit/backend:$OLD_BACKEND_SHA"; then
|
||||||
|
error "镜像 scilit/backend:$OLD_BACKEND_SHA 不存在(可能已被镜像治理清理)。请用 docker tag 恢复或选择其他版本。"
|
||||||
|
fi
|
||||||
|
if ! image_exists "scilit/frontend:$OLD_FRONTEND_SHA"; then
|
||||||
|
error "镜像 scilit/frontend:$OLD_FRONTEND_SHA 不存在。请用 docker tag 恢复或选择其他版本。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 数据回退(destructive=是):pg_restore 快照,先清后恢复(L)──
|
||||||
|
if [ "$DESTRUCTIVE" = "是" ]; then
|
||||||
|
if [ "$MODE" = "manual" ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ 该版本含破坏性迁移,将用快照 ${SNAPSHOT} 覆盖当前数据库(pg_restore --clean)。
|
||||||
|
当前库中的所有数据将回退到快照时间点。确认继续?(y/N)${NC}"
|
||||||
|
read -r yn
|
||||||
|
[ "$yn" = "y" ] || [ "$yn" = "Y" ] || error "已取消。"
|
||||||
|
fi
|
||||||
|
info "停止 backend/worker(避免恢复期间写入冲突)"
|
||||||
|
"${COMPOSE[@]}" stop backend worker
|
||||||
|
info "数据回退: pg_restore --clean --if-exists --no-owner ← $SNAPSHOT"
|
||||||
|
PGPASSWORD="${PG_PASSWORD}" "${COMPOSE[@]}" exec -T postgres \
|
||||||
|
pg_restore --clean --if-exists --no-owner -U scilit -d scilit < "$SNAPSHOT"
|
||||||
|
info "数据回退完成"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 双 tag 原子切换(E2:一次 up,绝不分两次)──
|
||||||
|
info "切换: backend+frontend → $OLD_BACKEND_SHA / $OLD_FRONTEND_SHA"
|
||||||
|
BACKEND_TAG="$OLD_BACKEND_SHA" FRONTEND_TAG="$OLD_FRONTEND_SHA" \
|
||||||
|
"${COMPOSE[@]}" up -d --no-deps backend worker frontend
|
||||||
|
|
||||||
|
# ── 记录回滚事件 + 清理标记 ──
|
||||||
|
echo "ROLLBACK $OLD_BACKEND_SHA $OLD_FRONTEND_SHA $DESTRUCTIVE $(date '+%Y-%m-%d %H:%M:%S %z')" \
|
||||||
|
>> "$DEPLOY_DIR/versions.log"
|
||||||
|
tail -n 200 "$DEPLOY_DIR/versions.log" > "$DEPLOY_DIR/versions.log.tmp"
|
||||||
|
mv "$DEPLOY_DIR/versions.log.tmp" "$DEPLOY_DIR/versions.log"
|
||||||
|
if [ "$MODE" = "auto" ]; then
|
||||||
|
rm -f "$DEPLOY_DIR/.pending-deploy"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "回滚完成 ✅ 当前版本: $OLD_BACKEND_SHA"
|
||||||
@@ -111,6 +111,7 @@ services:
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
|
image: scilit/backend:${BACKEND_TAG:-latest}
|
||||||
build: ./backend
|
build: ./backend
|
||||||
networks:
|
networks:
|
||||||
- scilit
|
- scilit
|
||||||
@@ -126,6 +127,7 @@ services:
|
|||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
|
image: scilit/backend:${BACKEND_TAG:-latest}
|
||||||
build: ./backend
|
build: ./backend
|
||||||
networks:
|
networks:
|
||||||
- scilit
|
- scilit
|
||||||
@@ -185,6 +187,7 @@ services:
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
|
image: scilit/backend:${BACKEND_TAG:-latest}
|
||||||
build: ./backend
|
build: ./backend
|
||||||
networks:
|
networks:
|
||||||
- scilit
|
- scilit
|
||||||
@@ -238,6 +241,7 @@ services:
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
image: scilit/frontend:${FRONTEND_TAG:-latest}
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile.prod
|
dockerfile: Dockerfile.prod
|
||||||
|
|||||||
+3
-2
@@ -1,11 +1,12 @@
|
|||||||
# 部署运维方案:生产工程化(一期 + 二期)
|
# 部署运维方案:生产工程化(一期 + 二期)
|
||||||
|
|
||||||
> **日期:** 2026-08-08(v17 更新于 2026-08-10)
|
> **日期:** 2026-08-08(v18 更新于 2026-08-10)
|
||||||
> **版本:** v17.0(当前) **状态:** 一期 3 项已执行(磁盘/备份/TLS)+ 构建加速(Dockerfile 卫生)+ gitea TLS,其余待执行
|
> **版本:** v18.0(当前) **状态:** 一期 §2 deploy/deploy.sh 体系已实现(v18),见版本记录
|
||||||
> **关联:** [docs/10-生产部署文档.md](10-生产部署文档.md)、[docs/11-20260717部署事故分析.md](11-20260717部署事故分析.md)、[docs/12-部署实际操作记录.md](12-部署实际操作记录.md)、[docs/17-生产数据迁移.md](17-生产数据迁移.md)(24 迁移专项,2026-08-09)
|
> **关联:** [docs/10-生产部署文档.md](10-生产部署文档.md)、[docs/11-20260717部署事故分析.md](11-20260717部署事故分析.md)、[docs/12-部署实际操作记录.md](12-部署实际操作记录.md)、[docs/17-生产数据迁移.md](17-生产数据迁移.md)(24 迁移专项,2026-08-09)
|
||||||
>
|
>
|
||||||
> **版本记录:**
|
> **版本记录:**
|
||||||
> - **v17** — 2026-08-10:§7 补「服务器本地改动必须回流 gitea」环节(TLS/Caddy 改动从未提交,git-ify checkout 会丢 HTTPS——回流后服务器 git pull 才安全,与 §2 闭环)
|
> - **v17** — 2026-08-10:§7 补「服务器本地改动必须回流 gitea」环节(TLS/Caddy 改动从未提交,git-ify checkout 会丢 HTTPS——回流后服务器 git pull 才安全,与 §2 闭环)
|
||||||
|
> - **v18** — 2026-08-10:**§2 deploy/deploy.sh 体系落地(首次实现)**——① compose 四服务(backend/worker/migrate/frontend)补 `image:` + `${BACKEND_TAG}`/`${FRONTEND_TAG}` 插值(M2/M4 + 换 tag 回滚前提,build 前 export tag 则 build 产物直接打 `<sha>` 标签);② 新建 `deploy/deploy.sh`(工作区校验 → PREV_SHA(pull 前,F)→ git pull → before head → predeploy 快照 `-Fc` 全量(H)→ destructive 判定(P12 + `destructive_*`/`# DESTRUCTIVE` 双保险)→ build + 双 tag → `.pending-deploy`(N1,落部署目录)→ 多 head 预检(P15)→ 迁移 `run --no-deps --rm` 退出码门控(K)→ after head → `up -d --no-deps backend worker frontend` → 健康轮询(V6)→ versions.log + 删标记 → 镜像治理 count 制;支持 `--dry-run`;顶部 source .env(G)+ flock + 遗留标记检测(I)+ 基础设施健康门(P14/N5));③ 新建 `deploy/rollback.sh`(双入口:手动读 versions.log 跳 ROLLBACK 行(E5)/ `--auto` 读本次 `.pending-deploy`(N1 洞);destructive=是 走 `pg_restore --clean --if-exists`(L);双 tag 原子切换(E2);回滚追 ROLLBACK 行);④ 新建 `deploy/hotfix.sh`(cp + 收口提醒);⑤ .gitignore 补 `.pending-deploy`/`versions.log`;⑥ 退役根 `deploy.sh` + `deploy.env.example`(SSH 旧模型);⑦ docs/deployment-guide.md 同步。复用 migrate_prod.sh 的 `get_db_rev()` 模式(`grep -oE '[0-9a-f]{12}' | tail -1` 防容器 ID 误抓)。脚本用法见 §2 下方或 deploy/*.sh 头部
|
||||||
> - **v16** — 2026-08-10:§3 补批量迁移执行序 + destructive ordering(批次 1 日期收窄须与发代码同窗口);§7 补 gitea TLS 落地;§6 补构建加速落地(腾讯 pip/npm 源 + BuildKit,详见记忆 docker_build_optimization)
|
> - **v16** — 2026-08-10:§3 补批量迁移执行序 + destructive ordering(批次 1 日期收窄须与发代码同窗口);§7 补 gitea TLS 落地;§6 补构建加速落地(腾讯 pip/npm 源 + BuildKit,详见记忆 docker_build_optimization)
|
||||||
> - **v1** — 初稿(生产工程化框架,一期 + 二期)
|
> - **v1** — 初稿(生产工程化框架,一期 + 二期)
|
||||||
> - **v2** — 吸收三档补强:破坏性迁移硬判定、backup.sh 上机查清、扩对盘、compose 范围澄清、挂卷后 logrotate、pre-deploy 快照留存、registry token 轮换、alembic head 采集、增强级
|
> - **v2** — 吸收三档补强:破坏性迁移硬判定、backup.sh 上机查清、扩对盘、compose 范围澄清、挂卷后 logrotate、pre-deploy 快照留存、registry token 轮换、alembic head 采集、增强级
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
```bash
|
```bash
|
||||||
# 1. 服务器上 clone 项目
|
# 1. 服务器上 clone 项目
|
||||||
ssh root@your-server.com
|
ssh root@your-server.com
|
||||||
git clone https://gitee.com/your-org/scilit-oncology.git /root/scilit-oncology
|
git clone https://gitee.com/your-org/scilit-oncology.git /root/scilit
|
||||||
cd /root/scilit-oncology
|
cd /root/scilit
|
||||||
|
|
||||||
# 2. 配置环境变量
|
# 2. 配置环境变量
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
@@ -27,15 +27,26 @@ curl http://localhost:8000/health
|
|||||||
curl http://localhost:80
|
curl http://localhost:80
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用 deploy.sh 自动化部署
|
## 使用 deploy/deploy.sh 自动化部署(生产)
|
||||||
|
|
||||||
|
> 旧根目录 `deploy.sh`(SSH 直连模型)已废弃,替换为 `deploy/` 脚本体系。
|
||||||
|
> 完整设计见 [16-部署运维方案.md](16-部署运维方案.md) §2。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 配置服务器连接信息
|
# 在服务器 /root/scilit(git 仓库 + .env 所在目录)执行
|
||||||
cp deploy.env.example deploy.env
|
|
||||||
# 编辑 deploy.env,填入 SCILIT_HOST / SCILIT_USER / SCILIT_PATH
|
|
||||||
|
|
||||||
# 2. 推送代码并部署
|
# 0. 前置:基础设施已在跑
|
||||||
source deploy.env && ./deploy.sh
|
docker compose -f docker-compose.prod.yml ps
|
||||||
|
|
||||||
|
# 1. 预览将执行的命令(不执行任何变更)
|
||||||
|
bash deploy/deploy.sh --dry-run
|
||||||
|
|
||||||
|
# 2. 正式部署(自动:工作区校验 → git pull → predeploy 快照 →
|
||||||
|
# 迁移 → 容器切换 → 健康轮询 → versions.log)
|
||||||
|
bash deploy/deploy.sh
|
||||||
|
|
||||||
|
# 3. 回滚到上一成功版本
|
||||||
|
bash deploy/rollback.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## PostgreSQL 备份与恢复
|
## PostgreSQL 备份与恢复
|
||||||
|
|||||||
Reference in New Issue
Block a user