fix(部署脚本): 改成在服务器上直接跑,加 --check

上一版是从本机 rsync + ssh 过去的,不是想要的形态。改成服务器上跑:
代码怎么上去(rsync / 以后的 git pull)不归它管,脚本只管起栈。

五道自检,前两道是这次在服务器上真撞到的失败:

  compose 版本 ≥ 2.20   depends_on.required 是那版才有的字段
  DATA_DIR              卷指向 OJ2/data → 中止(空数据,且不报错)
  DB_HOST               DATABASE_URL 还指着 oj-postgres → 中止
  JUDGE_STATE_DIR       没设的话新旧两个判题机共用运行目录
  旧栈还活着            oj-postgres / oj-redis 是新栈的上游

--check 只做只读校验,不建目录不动容器(mkdir 挪到起栈那一段了)。

## 跑出来的两个问题

1. 版本比较写反了。原来用 `sort -V -C` 判「这两行本来就有序」,结果本机
   compose 5.4.0 被判成「太老」。改成取 min(2.20, ver),在 1.29.2 / 2.19.9 /
   2.20.0 / 2.24.5 / 5.4.0 / 0 六个值上逐个验过分界。
2. --check 里的 mkdir 会真的建目录,本机跑直接撞权限。挪走了。

自检全路径实跑过:旧栈没起时正确拦下;造两个同名容器冒充旧栈后全绿通过;
抹掉 DATA_DIR / DB_HOST 两条守卫都按预期中止并打出可操作的提示。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 09:34:39 -06:00
parent eb849ca5ab
commit eadfa0b04a
2 changed files with 122 additions and 84 deletions

View File

@@ -1,99 +1,126 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# 从本机把 OJ2 推到服务器并重启新栈(并行试跑形态)。 # 在**服务器上**起 OJ2 新栈(并行试跑形态:旧栈的 postgres/redis 继续跑,
# 这里只起 api / worker / web / judge旧站一个容器都不用停
# #
# docker/deploy.sh # 推代码 → 构建 → 重启 → 自检 # cd /root/OJDeploy/OJ2
# docker/deploy.sh --env # 连 docker/.env 一起推(默认不推,见下) # docker/deploy.sh # 自检 → 构建 → 起栈 → 冒烟
# SERVER=root@1.2.3.4 docker/deploy.sh # 换台机 # docker/deploy.sh --check # 只自检,不动任何容
# docker/deploy.sh --no-build # 跳过构建(只改了 env / compose 时用)
# #
# 默认**不推 docker/.env**:本机那份和服务器那份很容易不是一回事(库地址、端口、 # 代码怎么上到服务器不归它管。没有 git remote 的话,在本机:
# AI key 都不同),覆盖掉是静默故障。确实要同步时显式加 --env。
# #
# 这是没有 git remote 时的临时方案。配好 remote 之后应该换成服务器上 git pull # rsync -az --delete --exclude node_modules --exclude .git --exclude data \
# 那样服务器上有版本记录,也不会把本机的脏改动带上去。 # --exclude 'docker/.env*' ~/Projects/OJ/OJ2/ root@服务器:/root/OJDeploy/OJ2/
#
# 前提docker/.env 已经填好(内容见 docs/specs/phase5-cutover-runbook.md 第三节)。
set -euo pipefail set -euo pipefail
SERVER=${SERVER:-root@xuyue.cc} cd "$(dirname "${BASH_SOURCE[0]}")/.."
REMOTE_DIR=${REMOTE_DIR:-/root/OJDeploy/OJ2} COMPOSE=(docker compose -f docker/compose.debian.yml --env-file docker/.env)
REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) CHECK_ONLY=0
COMPOSE="docker compose -f docker/compose.debian.yml --env-file docker/.env" BUILD=1
for arg in "$@"; do
case "$arg" in
--check) CHECK_ONLY=1 ;;
--no-build) BUILD=0 ;;
*) echo "未知参数:$arg(可用:--check、--no-build" >&2; exit 2 ;;
esac
done
PUSH_ENV=0 say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
[ "${1:-}" = "--env" ] && PUSH_ENV=1 ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
die() { printf '\n\033[1;31m❌ %s\033[0m\n\n' "$*" >&2; exit 1; }
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; } # ---------------------------------------------------------------- 自检
die() { printf '\n\033[1;31m❌ %s\033[0m\n' "$*" >&2; exit 1; } say "自检"
# ---------------------------------------------------------------- 1. 推代码 [ -f docker/.env ] || die "docker/.env 不存在。内容见手册第三节,别用 .env.example 直接改名(里面的值全是空的)"
say "1/4 同步代码 → $SERVER:$REMOTE_DIR"
excludes=( # compose 版本depends_on.required 是 v2.20 才有的字段,老版本会解析失败
--exclude node_modules ver=$(docker compose version --short 2>/dev/null || echo 0)
--exclude .git # 取 min(2.20, ver):等于 2.20 就说明 ver 不低于它。别用 `sort -V -C`
--exclude data # 运行时数据,服务器上那份才是真的 # 那个判的是「这两行本来就有序」,对 ver 更新的情况会反过来判成太老(写错过一次)。
--exclude 'apps/web/dist' [ "$(printf '2.20\n%s\n' "$ver" | sort -V | head -1)" = "2.20" ] \
) || die "docker compose 版本太老($ver),需要 ≥ 2.20depends_on.required 是那个版本才有的字段)"
if [ "$PUSH_ENV" -eq 0 ]; then ok "docker compose $ver"
excludes+=(--exclude 'docker/.env' --exclude 'docker/.env.*' --include 'docker/.env.example')
cfg=$("${COMPOSE[@]}" config) || die "compose 配置解析失败,看上面的报错"
# ① DATA_DIR不生效的话卷会挂到 OJ2/data 这个空目录上 —— 站点起得来,
# 但没有测试点、题面图片 404而且**不报任何错**。这是唯一会静默走歪的地方。
bad=$(grep 'source:' <<<"$cfg" | grep 'OJ2/data' || true)
[ -z "$bad" ] || die "DATA_DIR 没生效,这些卷指向了 OJ2/data
$bad
检查 docker/.env 里的 DATA_DIR注意DATA_DIR= 空值等于没设,等号两边不能有空格)"
ok "数据卷都在 $(grep 'source:' <<<"$cfg" | head -1 | sed 's|.*source: ||; s|/backend.*||')"
# ② DB_HOST不生效会静默回落成 oj-postgres而试跑形态下那个容器根本没起
! grep -q 'DATABASE_URL: postgres://[^@]*@oj-postgres' <<<"$cfg" \
|| die "DB_HOST 没生效DATABASE_URL 还指着 oj-postgres —— 试跑形态下它不存在,起来必然连不上库"
ok "库指向 $(grep -m1 'DATABASE_URL:' <<<"$cfg" | sed 's|.*@||; s|/onlinejudge.*||')"
# ③ 判题机运行目录必须和旧栈分开,否则两个 judger 往同一个目录里写
judge_dir=$(grep -E '^JUDGE_STATE_DIR=' docker/.env | tail -1 | cut -d= -f2- || true)
[ -n "$judge_dir" ] || die "JUDGE_STATE_DIR 没设 —— 试跑期间新旧两个判题机会共用运行目录"
ok "判题机运行目录 $judge_dir"
# ④ 旧栈的 postgres / redis 得还活着,新栈要连它们
for c in oj-postgres oj-redis; do
docker ps --filter "name=$c" --filter status=running -q | grep -q . \
|| die "旧栈的 $c 没在跑。试跑形态依赖它们 —— 先 docker compose -f /root/OJDeploy/docker-compose.yml up -d $c"
done
ok "旧栈的 postgres / redis 都在跑"
port=$(grep -E '^WEB_PORT=' docker/.env | tail -1 | cut -d= -f2- || true)
port=${port:-8080}
ok "对外端口 $port"
[ "$CHECK_ONLY" -eq 0 ] || { printf '\n\033[1;32m✅ 自检通过(--check没动容器\033[0m\n'; exit 0; }
# ---------------------------------------------------------------- 起栈
mkdir -p "$judge_dir/log" "$judge_dir/run"
if [ "$BUILD" -eq 1 ]; then
say "构建镜像(首次约 5 分钟,之后走缓存很快)"
"${COMPOSE[@]}" build
fi fi
# --delete 让服务器和本机一致(被 --exclude 排除的文件不会被删rsync 默认保护) say "起栈"
rsync -az --delete --info=stats1 "${excludes[@]}" "$REPO/" "$SERVER:$REMOTE_DIR/" "${COMPOSE[@]}" up -d
"${COMPOSE[@]}" ps
# ---------------------------------------------------------------- 2-4. 服务器上 status=starting
ssh "$SERVER" bash -s <<REMOTE for _ in $(seq 30); do
set -euo pipefail status=$(docker inspect -f '{{.State.Health.Status}}' oj-api 2>/dev/null || echo starting)
cd "$REMOTE_DIR" [ "$status" = healthy ] && break
say() { printf '\n\033[1;36m==> %s\033[0m\n' "\$*"; }
die() { printf '\n\033[1;31m❌ %s\033[0m\n' "\$*" >&2; exit 1; }
[ -f docker/.env ] || die "$REMOTE_DIR/docker/.env 不存在,先把 env 放上去(内容见手册第三节)"
say "2/4 起栈前自检"
# 这一条是切换当天唯一会静默走歪的地方DATA_DIR 没生效的话,卷会挂到 OJ2/data
# 这个空目录上 —— 站点起得来,但没有测试点、题面图片 404而且不报任何错。
bad=\$($COMPOSE config | grep 'source:' | grep 'OJ2/data' || true)
[ -z "\$bad" ] || die "DATA_DIR 没生效,这些卷指向了 OJ2/data\$(printf '\n%s' "\$bad")"
# 同理DB_HOST 空着会静默回落成 oj-postgres而试跑形态下那个容器根本没起
$COMPOSE config | grep -q 'DATABASE_URL: postgres://[^@]*@oj-postgres' \
&& die "DB_HOST 没生效DATABASE_URL 还指着 oj-postgres试跑形态下它不存在" || true
# 判题机运行目录必须和旧栈分开,不然两个 judger 往一个目录里写
judge_dir=\$(grep -E '^JUDGE_STATE_DIR=' docker/.env | cut -d= -f2- || true)
[ -n "\$judge_dir" ] || die "JUDGE_STATE_DIR 没设,会和旧判题机共用运行目录"
mkdir -p "\$judge_dir/log" "\$judge_dir/run"
say "3/4 构建镜像(首次约 5 分钟)"
$COMPOSE build
say "4/4 起栈"
$COMPOSE up -d
$COMPOSE ps
# 等 oj-api 变 healthy最多 60 秒
for i in \$(seq 30); do
status=\$(docker inspect -f '{{.State.Health.Status}}' oj-api 2>/dev/null || echo starting)
[ "\$status" = healthy ] && break
sleep 2 sleep 2
done done
[ "\$status" = healthy ] || die "oj-api 没起来(状态 \$status,看 docker logs oj-api --tail 50" [ "$status" = healthy ] || die "oj-api 没起来(状态 $status
看日志docker logs oj-api --tail 50"
port=\$(grep -E '^WEB_PORT=' docker/.env | cut -d= -f2- || echo 8080) # ---------------------------------------------------------------- 冒烟
port=\${port:-8080} say "冒烟(端口 $port"
code() { curl -s -o /dev/null -w '%{http_code}' "http://localhost:$port$1"; }
printf ' 首页 %s\n' "$(code /)"
printf ' 站点配置 %s\n' "$(code /api/site)"
printf ' 未登录进后台 %s (期望 401)\n' "$(code /api/admin/dashboard)"
say "自检(端口 \$port" total=$(curl -s "http://localhost:$port/api/problems" | grep -oE '"total":[0-9]+' | head -1 | cut -d: -f2 || true)
printf '首页 %s\n' "\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:\$port/)" printf ' 题目总数 %s\n' "${total:-读不出来}"
printf '站点配置 %s\n' "\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:\$port/api/site)" [ "${total:-0}" -gt 0 ] || die "题目数是 0 —— 连的不是生产库。查 DB_HOST / POSTGRES_PASSWORD"
printf '未登录进后台 %s期望 401\n' "\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:\$port/api/admin/dashboard)"
total=\$(curl -s "http://localhost:\$port/api/problems" | grep -oE '"total":[0-9]+' | head -1 | cut -d: -f2) cat <<EOF
printf '题目总数 %s\n' "\${total:-读不出来}"
[ "\${total:-0}" -gt 0 ] || die "题目数是 0 —— 连错库了,八成是 DB_HOST 或 POSTGRES_PASSWORD 不对"
printf '\n\033[1;32m✅ 完成。别忘了 NPM 那台 proxy host 指向 %s且 Websockets Support 是开的。\033[0m\n' "\$port" $(printf '\033[1;32m✅ 起来了\033[0m')
REMOTE
还差 NPM 那一步:
- proxy host: oj2.xuyue.cc → 宿主机:$port
- **Websockets Support 必须打开**,否则学生那边「判题中…」永远不动
- client_max_body_size 200M上传测试用例压缩包
之后自己点一遍:登录、提交一道题看结果实时刷出来、后台判题机列表、带图片的题面。
EOF

View File

@@ -236,17 +236,28 @@ JUDGE_STATE_DIR=/root/OJDeploy/data/judge_server_oj2
### 2. 起 ### 2. 起
有脚本,**本机**跑(没有 git remote 时靠 rsync 推代码) 有脚本,**在服务器上**跑
```bash ```bash
docker/deploy.sh # 推代码 → 自检 → 构建 → 起栈 → 冒烟 cd /root/OJDeploy/OJ2
SERVER=root@1.2.3.4 docker/deploy.sh docker/deploy.sh # 自检 → 构建 → 起栈 → 冒烟
docker/deploy.sh --check # 只自检,只读,不动任何容器
docker/deploy.sh --no-build # 只改了 env / compose 时跳过构建
``` ```
它默认**不推 `docker/.env`**(本机那份和服务器那份不是一回事,覆盖掉是静默故障), 代码怎么上到服务器不归它管rsync 或以后的 `git pull`,命令在脚本头部注释里)。
要同步时显式加 `--env`。起栈前有两道守卫,正是今天在服务器上撞到的那两种:
`DATA_DIR` 没生效(卷指向 `OJ2/data`)、`DB_HOST` 没生效(`DATABASE_URL` 还指着 起栈前有五道自检,前两道正是这次在服务器上真撞到的:
试跑形态下并不存在的 `oj-postgres`)—— 命中任一条就中止,不会起一个看着正常的坏栈。
| 自检 | 拦什么 |
|---|---|
| compose 版本 | `depends_on.required` 要 ≥ 2.20,老版本解析就会失败 |
| `DATA_DIR` | 卷指向 `OJ2/data` → 中止(空数据,静默) |
| `DB_HOST` | `DATABASE_URL` 还指着 `oj-postgres` → 中止(试跑形态下它不存在) |
| `JUDGE_STATE_DIR` | 没设的话新旧两个判题机共用运行目录 |
| 旧栈 | `oj-postgres` / `oj-redis` 得还活着,新栈连的就是它们 |
起完等 `oj-api` healthy再跑四条冒烟题目数是 0 也中止 —— 那意味着连错库了。
手动等价于: 手动等价于: