Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eadfa0b04a | ||
|
|
eb849ca5ab |
+21
-3
@@ -53,9 +53,27 @@ FROM debian:trixie-slim AS api
|
|||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# clang-format 给 C 代码格式化用,对应旧镜像 apt 装的那个。
|
# clang-format 给 C 代码格式化用,对应旧镜像 apt 装的那个。
|
||||||
# ca-certificates 给 AI 接口的 https 出站用。
|
#
|
||||||
RUN apt-get update \
|
# **不装 ca-certificates。** AI 接口那条 https 出站不需要它 —— Bun 和 Node 一样
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates clang-format \
|
# 内嵌了一份根证书,走自己那份,不读系统的 `/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/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ruff 给 Python 代码格式化用。旧后端是 pip 装的 ruff==0.15.12,这里取同版本的
|
# ruff 给 Python 代码格式化用。旧后端是 pip 装的 ruff==0.15.12,这里取同版本的
|
||||||
|
|||||||
Executable
+126
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# 在**服务器上**起 OJ2 新栈(并行试跑形态:旧栈的 postgres/redis 继续跑,
|
||||||
|
# 这里只起 api / worker / web / judge,旧站一个容器都不用停)。
|
||||||
|
#
|
||||||
|
# cd /root/OJDeploy/OJ2
|
||||||
|
# docker/deploy.sh # 自检 → 构建 → 起栈 → 冒烟
|
||||||
|
# docker/deploy.sh --check # 只自检,不动任何容器
|
||||||
|
# docker/deploy.sh --no-build # 跳过构建(只改了 env / compose 时用)
|
||||||
|
#
|
||||||
|
# 代码怎么上到服务器不归它管。没有 git remote 的话,在本机:
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
|
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
|
||||||
|
|
||||||
|
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 "自检"
|
||||||
|
|
||||||
|
[ -f docker/.env ] || die "docker/.env 不存在。内容见手册第三节,别用 .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
|
||||||
|
|
||||||
|
say "起栈"
|
||||||
|
"${COMPOSE[@]}" up -d
|
||||||
|
"${COMPOSE[@]}" ps
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 冒烟
|
||||||
|
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)"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
$(printf '\033[1;32m✅ 起来了\033[0m')
|
||||||
|
|
||||||
|
还差 NPM 那一步:
|
||||||
|
- proxy host: oj2.xuyue.cc → 宿主机:$port
|
||||||
|
- **Websockets Support 必须打开**,否则学生那边「判题中…」永远不动
|
||||||
|
- client_max_body_size 200M(上传测试用例压缩包)
|
||||||
|
|
||||||
|
之后自己点一遍:登录、提交一道题看结果实时刷出来、后台判题机列表、带图片的题面。
|
||||||
|
EOF
|
||||||
@@ -236,11 +236,36 @@ JUDGE_STATE_DIR=/root/OJDeploy/data/judge_server_oj2
|
|||||||
|
|
||||||
### 2. 起
|
### 2. 起
|
||||||
|
|
||||||
|
有脚本,**在服务器上**跑:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/OJDeploy/OJ2
|
||||||
|
docker/deploy.sh # 自检 → 构建 → 起栈 → 冒烟
|
||||||
|
docker/deploy.sh --check # 只自检,只读,不动任何容器
|
||||||
|
docker/deploy.sh --no-build # 只改了 env / compose 时跳过构建
|
||||||
|
```
|
||||||
|
|
||||||
|
代码怎么上到服务器不归它管(rsync 或以后的 `git pull`,命令在脚本头部注释里)。
|
||||||
|
|
||||||
|
起栈前有五道自检,前两道正是这次在服务器上真撞到的:
|
||||||
|
|
||||||
|
| 自检 | 拦什么 |
|
||||||
|
|---|---|
|
||||||
|
| 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 也中止 —— 那意味着连错库了。
|
||||||
|
|
||||||
|
手动等价于:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p /root/OJDeploy/data/judge_server_oj2/log /root/OJDeploy/data/judge_server_oj2/run
|
mkdir -p /root/OJDeploy/data/judge_server_oj2/log /root/OJDeploy/data/judge_server_oj2/run
|
||||||
cd /root/OJDeploy
|
cd /root/OJDeploy
|
||||||
docker compose -f OJ2/docker/compose.debian.yml --env-file OJ2/docker/.env up -d
|
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 上照常服务。
|
旧站这时候完全没受影响,8080 上照常服务。
|
||||||
@@ -427,7 +452,7 @@ ERROR: database "onlinejudge" is being accessed by other users
|
|||||||
演练时因为目标库本来是空的,数据照样灌进去了 —— 那是运气。目标库有数据的话,
|
演练时因为目标库本来是空的,数据照样灌进去了 —— 那是运气。目标库有数据的话,
|
||||||
接下来就是满屏主键冲突。灾难恢复流程:**先停 oj-api / oj-worker,再恢复。**
|
接下来就是满屏主键冲突。灾难恢复流程:**先停 oj-api / oj-worker,再恢复。**
|
||||||
|
|
||||||
### 3. 「本地能过、容器里过不了」的三个坑(构建期)
|
### 3. 「本地能过、容器里过不了」的四个坑(构建期)
|
||||||
|
|
||||||
都已修好并写进 Dockerfile 的注释,这里只留索引:
|
都已修好并写进 Dockerfile 的注释,这里只留索引:
|
||||||
|
|
||||||
@@ -435,6 +460,11 @@ ERROR: database "onlinejudge" is being accessed by other users
|
|||||||
- `mermaid@9.4.3`(机房老 Chrome 的 legacy 依赖)从容器里连 npmjs 稳定失败 → 换 npmmirror + 重试
|
- `mermaid@9.4.3`(机房老 Chrome 的 legacy 依赖)从容器里连 npmjs 稳定失败 → 换 npmmirror + 重试
|
||||||
- 容器里 bun 用 isolated 布局、本地是扁平的,靠「提升」解析的包在容器里一律找不到
|
- 容器里 bun 用 isolated 布局、本地是扁平的,靠「提升」解析的包在容器里一律找不到
|
||||||
→ 把真正直接 import 的 4 个包补成直接依赖
|
→ 把真正直接 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` 可覆盖。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user