From eb849ca5ab1226beb6539b52ed1fd3252c129248 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 16 Aug 2026 09:30:50 -0600 Subject: [PATCH] =?UTF-8?q?fix(=E9=95=9C=E5=83=8F):=20apt=20=E6=8D=A2?= =?UTF-8?q?=E6=B8=85=E5=8D=8E=E6=BA=90=E3=80=81=E5=8E=BB=E6=8E=89=E7=94=A8?= =?UTF-8?q?=E4=B8=8D=E4=B8=8A=E7=9A=84=20ca-certificates=EF=BC=9B=E8=A1=A5?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## apt-get update 在服务器上卡死 不是慢,是挂住不返回 —— 本机构建从来没事,所以演练没暴露。换清华源。两个细节: - trixie 的源是 deb822 格式,在 `/etc/apt/sources.list.d/debian.sources`, 老的 `sources.list` 在这个基底里是**空文件**,改它没有任何效果。 - **只能换主机名、必须保持 http。** https 源会在这一步失败,因为镜像里还没有根证书。 `ARG APT_MIRROR` 可覆盖。 ## ca-certificates 是多余的 原注释写「给 AI 接口的 https 出站用」,不成立:Bun 和 Node 一样内嵌了一份根证书, 走自己那份,不读系统的 /etc/ssl/certs。 实测:把探针二进制丢进裸 debian:trixie-slim(没有 ca-certificates)请求 api.deepseek.com,握手正常、返回 401(没带 key),和装了的镜像行为一致。 去掉之后重新构建,clang-format / ruff 都在,https 出站照旧,487MB → 483MB。 哪天镜像里加了用 OpenSSL 做 TLS 的东西(curl、wget 之类),这条要重新考虑, 注释里写了。 ## docker/deploy.sh 没有 git remote 时的部署路径:本机 rsync 推代码 → 服务器上构建 → 起栈 → 冒烟。 默认不推 docker/.env(两边不是一回事,覆盖掉是静默故障),要同步显式 --env。 起栈前两道守卫,就是今天在服务器上真撞到的那两种失败:DATA_DIR 没生效(卷指向 OJ2/data)、DB_HOST 没生效(DATABASE_URL 还指着试跑形态下并不存在的 oj-postgres)。 两道守卫都用当天那份坏 env 正反跑过:齐全时放行,抹掉这两个变量时都触发。 两半的语法也都 `bash -n` 过(远端那半是 heredoc,单独渲染后再查的)。 Co-Authored-By: Claude Opus 5 --- docker/Dockerfile | 24 ++++++- docker/deploy.sh | 99 ++++++++++++++++++++++++++++ docs/specs/phase5-cutover-runbook.md | 23 ++++++- 3 files changed, 141 insertions(+), 5 deletions(-) create mode 100755 docker/deploy.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index c3313ba..5b27344 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -53,9 +53,27 @@ FROM debian:trixie-slim AS api ENV NODE_ENV=production # clang-format 给 C 代码格式化用,对应旧镜像 apt 装的那个。 -# ca-certificates 给 AI 接口的 https 出站用。 -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates clang-format \ +# +# **不装 ca-certificates。** AI 接口那条 https 出站不需要它 —— Bun 和 Node 一样 +# 内嵌了一份根证书,走自己那份,不读系统的 `/etc/ssl/certs`。实测:把探针二进制 +# 丢进裸 debian:trixie-slim(没有 ca-certificates)请求 api.deepseek.com, +# 握手正常、返回 401(没带 key),和装了的镜像行为一致。 +# 哪天镜像里加了用 OpenSSL 做 TLS 的东西(curl、wget 之类),这条要重新考虑。 +# +# apt 走国内镜像源,理由和上面的 npm 源一样:deb.debian.org 从服务器上 +# `apt-get update` 会卡死(不是慢,是挂住不返回)。 +# +# 两个坑: +# 1. trixie 的源是 deb822 格式,在 `/etc/apt/sources.list.d/debian.sources`, +# 不是老的 `/etc/apt/sources.list`(那个文件在这个基底里是空的)。 +# 2. **必须保持 http,不能换成 https** —— 镜像里没有根证书(见上), +# https 源会直接证书校验失败。 +# +# 只替换主机名:清华同时提供 /debian 和 /debian-security,路径不用动。 +ARG APT_MIRROR=mirrors.tuna.tsinghua.edu.cn +RUN sed -i "s|deb.debian.org|${APT_MIRROR}|g" /etc/apt/sources.list.d/debian.sources \ + && apt-get update \ + && apt-get install -y --no-install-recommends clang-format \ && rm -rf /var/lib/apt/lists/* # ruff 给 Python 代码格式化用。旧后端是 pip 装的 ruff==0.15.12,这里取同版本的 diff --git a/docker/deploy.sh b/docker/deploy.sh new file mode 100755 index 0000000..79c15ac --- /dev/null +++ b/docker/deploy.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# +# 从本机把 OJ2 推到服务器并重启新栈(并行试跑形态)。 +# +# docker/deploy.sh # 推代码 → 构建 → 重启 → 自检 +# docker/deploy.sh --env # 连 docker/.env 一起推(默认不推,见下) +# SERVER=root@1.2.3.4 docker/deploy.sh # 换台机器 +# +# 默认**不推 docker/.env**:本机那份和服务器那份很容易不是一回事(库地址、端口、 +# AI key 都不同),覆盖掉是静默故障。确实要同步时显式加 --env。 +# +# 这是没有 git remote 时的临时方案。配好 remote 之后应该换成服务器上 git pull, +# 那样服务器上有版本记录,也不会把本机的脏改动带上去。 + +set -euo pipefail + +SERVER=${SERVER:-root@xuyue.cc} +REMOTE_DIR=${REMOTE_DIR:-/root/OJDeploy/OJ2} + +REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +COMPOSE="docker compose -f docker/compose.debian.yml --env-file docker/.env" + +PUSH_ENV=0 +[ "${1:-}" = "--env" ] && PUSH_ENV=1 + +say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; } +die() { printf '\n\033[1;31m❌ %s\033[0m\n' "$*" >&2; exit 1; } + +# ---------------------------------------------------------------- 1. 推代码 +say "1/4 同步代码 → $SERVER:$REMOTE_DIR" + +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') +fi + +# --delete 让服务器和本机一致(被 --exclude 排除的文件不会被删,rsync 默认保护) +rsync -az --delete --info=stats1 "${excludes[@]}" "$REPO/" "$SERVER:$REMOTE_DIR/" + +# ---------------------------------------------------------------- 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 + sleep 2 +done +[ "\$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)" +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) +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" +REMOTE diff --git a/docs/specs/phase5-cutover-runbook.md b/docs/specs/phase5-cutover-runbook.md index 69f3cd3..cc2c517 100644 --- a/docs/specs/phase5-cutover-runbook.md +++ b/docs/specs/phase5-cutover-runbook.md @@ -236,11 +236,25 @@ JUDGE_STATE_DIR=/root/OJDeploy/data/judge_server_oj2 ### 2. 起 +有脚本,从**本机**跑(没有 git remote 时靠 rsync 推代码): + +```bash +docker/deploy.sh # 推代码 → 自检 → 构建 → 起栈 → 冒烟 +SERVER=root@1.2.3.4 docker/deploy.sh +``` + +它默认**不推 `docker/.env`**(本机那份和服务器那份不是一回事,覆盖掉是静默故障), +要同步时显式加 `--env`。起栈前有两道守卫,正是今天在服务器上撞到的那两种: +`DATA_DIR` 没生效(卷指向 `OJ2/data`)、`DB_HOST` 没生效(`DATABASE_URL` 还指着 +试跑形态下并不存在的 `oj-postgres`)—— 命中任一条就中止,不会起一个看着正常的坏栈。 + +手动等价于: + ```bash mkdir -p /root/OJDeploy/data/judge_server_oj2/log /root/OJDeploy/data/judge_server_oj2/run cd /root/OJDeploy docker compose -f OJ2/docker/compose.debian.yml --env-file OJ2/docker/.env up -d -curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8090/ # 期望 200 +curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8180/ # 期望 200 ``` 旧站这时候完全没受影响,8080 上照常服务。 @@ -427,7 +441,7 @@ ERROR: database "onlinejudge" is being accessed by other users 演练时因为目标库本来是空的,数据照样灌进去了 —— 那是运气。目标库有数据的话, 接下来就是满屏主键冲突。灾难恢复流程:**先停 oj-api / oj-worker,再恢复。** -### 3. 「本地能过、容器里过不了」的三个坑(构建期) +### 3. 「本地能过、容器里过不了」的四个坑(构建期) 都已修好并写进 Dockerfile 的注释,这里只留索引: @@ -435,6 +449,11 @@ ERROR: database "onlinejudge" is being accessed by other users - `mermaid@9.4.3`(机房老 Chrome 的 legacy 依赖)从容器里连 npmjs 稳定失败 → 换 npmmirror + 重试 - 容器里 bun 用 isolated 布局、本地是扁平的,靠「提升」解析的包在容器里一律找不到 → 把真正直接 import 的 4 个包补成直接依赖 +- **`apt-get update` 在服务器上卡死**(2026-08-16 试跑时撞上,本机构建从来没事) + → 换清华源。两个细节:trixie 的源是 deb822 格式、在 + `/etc/apt/sources.list.d/debian.sources`(老的 `sources.list` 在这个基底里是空文件); + **只能换主机名、必须保持 http** —— ca-certificates 正是这一步要装的, + 换成 https 会在没有根证书的情况下证书校验失败。`ARG APT_MIRROR` 可覆盖。 ---