diff --git a/docker/deploy.sh b/docker/deploy.sh index 79c15ac..03af030 100755 --- a/docker/deploy.sh +++ b/docker/deploy.sh @@ -1,99 +1,126 @@ #!/usr/bin/env bash # -# 从本机把 OJ2 推到服务器并重启新栈(并行试跑形态)。 +# 在**服务器上**起 OJ2 新栈(并行试跑形态:旧栈的 postgres/redis 继续跑, +# 这里只起 api / worker / web / judge,旧站一个容器都不用停)。 # -# docker/deploy.sh # 推代码 → 构建 → 重启 → 自检 -# docker/deploy.sh --env # 连 docker/.env 一起推(默认不推,见下) -# SERVER=root@1.2.3.4 docker/deploy.sh # 换台机器 +# cd /root/OJDeploy/OJ2 +# docker/deploy.sh # 自检 → 构建 → 起栈 → 冒烟 +# docker/deploy.sh --check # 只自检,不动任何容器 +# docker/deploy.sh --no-build # 跳过构建(只改了 env / compose 时用) # -# 默认**不推 docker/.env**:本机那份和服务器那份很容易不是一回事(库地址、端口、 -# AI key 都不同),覆盖掉是静默故障。确实要同步时显式加 --env。 +# 代码怎么上到服务器不归它管。没有 git remote 的话,在本机: # -# 这是没有 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 -SERVER=${SERVER:-root@xuyue.cc} -REMOTE_DIR=${REMOTE_DIR:-/root/OJDeploy/OJ2} +cd "$(dirname "${BASH_SOURCE[0]}")/.." +COMPOSE=(docker compose -f docker/compose.debian.yml --env-file docker/.env) -REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -COMPOSE="docker compose -f docker/compose.debian.yml --env-file docker/.env" +CHECK_ONLY=0 +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 -[ "${1:-}" = "--env" ] && PUSH_ENV=1 +say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; } +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. 推代码 -say "1/4 同步代码 → $SERVER:$REMOTE_DIR" +[ -f docker/.env ] || die "docker/.env 不存在。内容见手册第三节,别用 .env.example 直接改名(里面的值全是空的)" -excludes=( - --exclude node_modules - --exclude .git - --exclude data # 运行时数据,服务器上那份才是真的 - --exclude 'apps/web/dist' -) -if [ "$PUSH_ENV" -eq 0 ]; then - excludes+=(--exclude 'docker/.env' --exclude 'docker/.env.*' --include 'docker/.env.example') +# compose 版本:depends_on.required 是 v2.20 才有的字段,老版本会解析失败 +ver=$(docker compose version --short 2>/dev/null || echo 0) +# 取 min(2.20, ver):等于 2.20 就说明 ver 不低于它。别用 `sort -V -C`, +# 那个判的是「这两行本来就有序」,对 ver 更新的情况会反过来判成太老(写错过一次)。 +[ "$(printf '2.20\n%s\n' "$ver" | sort -V | head -1)" = "2.20" ] \ + || die "docker compose 版本太老($ver),需要 ≥ 2.20(depends_on.required 是那个版本才有的字段)" +ok "docker compose $ver" + +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 -# --delete 让服务器和本机一致(被 --exclude 排除的文件不会被删,rsync 默认保护) -rsync -az --delete --info=stats1 "${excludes[@]}" "$REPO/" "$SERVER:$REMOTE_DIR/" +say "起栈" +"${COMPOSE[@]}" up -d +"${COMPOSE[@]}" ps -# ---------------------------------------------------------------- 2-4. 服务器上 -ssh "$SERVER" bash -s < %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 +status=starting +for _ in $(seq 30); do + status=$(docker inspect -f '{{.State.Health.Status}}' oj-api 2>/dev/null || echo starting) + [ "$status" = healthy ] && break sleep 2 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)" -printf '首页 %s\n' "\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:\$port/)" -printf '站点配置 %s\n' "\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:\$port/api/site)" -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 || true) +printf ' 题目总数 %s\n' "${total:-读不出来}" +[ "${total:-0}" -gt 0 ] || die "题目数是 0 —— 连的不是生产库。查 DB_HOST / POSTGRES_PASSWORD" -total=\$(curl -s "http://localhost:\$port/api/problems" | grep -oE '"total":[0-9]+' | head -1 | cut -d: -f2) -printf '题目总数 %s\n' "\${total:-读不出来}" -[ "\${total:-0}" -gt 0 ] || die "题目数是 0 —— 连错库了,八成是 DB_HOST 或 POSTGRES_PASSWORD 不对" +cat <