chore(部署): 服务器的数据库和 Redis 改由 OJ2 自己起,不用再改 OJDeploy 下的 compose;附一次性搬家脚本
Deploy / deploy (push) Canceled after 0s

compose.debian.yml 去掉外接形态,postgres / redis 固定由本栈起(PostgreSQL 18、Redis 8)。
docker/move-db-into-oj2.sh 把服务器上旧栈的 16 / 7 倒进来:先编镜像、确认机房已断开、
备份、恢复时剥掉 dump 里的 md5 口令(18 只认 SCRAM)、核对行数和网络登录,最后交给 deploy.sh。

生产库口令原来是默认的 onlinejudge(超级用户、5445 对公网开着),脚本要求换成强口令才执行。

本机按线上「之前」的状态完整演练过:停机到恢复 36 秒,行数一致,登录态保住,提交照常判题。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-13 21:10:09 +08:00
1 parent fad43f513f
commit 625bd49801
7 files changed
+347 -236

No files matched your search

+4 -2
View File
@@ -264,8 +264,10 @@ drizzle-kit 的坑,全在 `docs/database.md`。
**机房那套没有 postgres,连的是服务器的库。** 两个站点共用一个数据库,但各有各的 Redis
和判题沙箱 —— 所以涉及两边的变更要一起做。
`compose.debian.yml` 靠 env 切形态:设 `DATA_DIR` / `DB_HOST` / `REDIS_HOST` 就是接现有的库
(线上就是这个),留空并加 `--profile local-data` 就是自带 postgres / redis。
**服务器上的全部容器(含 postgres / redis)都由 `compose.debian.yml` 起**,升级哪个都只改
仓库里的文件。以前库和 redis 归旧栈那份 `/root/OJDeploy/docker-compose.yml`、这边外接过去,
那种形态已经删了(搬家脚本 `docker/move-db-into-oj2.sh`,一次性的)。库是 PostgreSQL 18,
数据在 `$DATA_DIR/postgresql`;本机 dev 用同一个大版本,别单独换。
⚠️ **`DATA_DIR` 默认值 `../data` 是 `OJ2/data`,不是部署目录的 `data/`。** 沿用旧数据却忘了
设它,会静默起一套空数据(空库、没测试点、图片 404),而且**不报错** —— 这是整个部署里
+11 -21
View File
@@ -6,6 +6,9 @@
# 不会静默起一个用弱默认值的服务。
# 数据库口令。机房那套连的是服务器的库,两边必须一致。
# 这个账号是超级用户、5445 对公网开着(机房要连),必须是强口令,自己生成:
# openssl rand -hex 32
# 它只在库第一次初始化时生效,之后改这里不会改库里的口令 —— 要换得进库 ALTER ROLE。
POSTGRES_PASSWORD=
# 判题机 token。后端和判题机容器读的是同一个变量,自己生成:
@@ -15,8 +18,7 @@ OJ2_JUDGE_TOKEN=
# Redis 口令。会话存在 Redis 里,能连上它就能伪造任意用户(包括超管)的登录态,
# 所以必须设。它会拼进 REDIS_URL,只能用 URL 安全的字符,自己生成:
# openssl rand -hex 32
# 自带数据形态下本栈的 oj-redis 直接用它;外接旧栈 redis 时,那边的 redis-server
# 也要带上同一个 --requirepass(见 docs/deploy.md「Redis 口令」)。
# 服务器上本栈的 oj-redis 直接用它。
# 机房的 redis 不发布端口,只在 compose 网络里,不需要设。
REDIS_PASSWORD=
@@ -36,32 +38,15 @@ AI_HINT_DIAGNOSE=
# --- 数据在哪 ---
#
# 这三个变量决定新栈是「自带 postgres/redis」还是「接着用旧栈的」。
#
# DATA_DIR 是所有数据卷的根(库、测试点、上传的图片、判题机日志)。
# DATA_DIR 是所有数据卷的根(库、redis、测试点、上传的图片、判题机日志)。
# **不设的话默认是 `../data`,也就是 `OJ2/data` —— 不是部署目录的 `data/`。**
# 沿用旧数据时必须写成旧目录的绝对路径,例如服务器上:
# 服务器上写成:
#
# DATA_DIR=/root/OJDeploy/data
#
# 不设它就等于开一套空数据:空库(站点没有用户没有题)、没有测试点、题面图片 404。
DATA_DIR=
# 库和 redis 的位置。留空 = 用本 compose 自己起的容器(要加 `--profile local-data`)。
# 沿用旧栈那两个容器时这样填(它们已经把端口发布在宿主机上了):
#
# DB_HOST=host.docker.internal
# DB_PORT=5445
# REDIS_HOST=host.docker.internal
# REDIS_PORT=5446
#
# host.docker.internal 由 compose 里的 extra_hosts 映射到 host-gateway,不出本机。
# 机房那套没有本地库,DB_HOST 不填时默认指向服务器(默认值在 compose.school.yml 里)。
DB_HOST=
DB_PORT=
REDIS_HOST=
REDIS_PORT=
# --- 并行试跑(oj2.xuyue.cc)才需要 ---
#
# 新站和旧站同时跑的时候,这两个必须设,否则会跟旧栈抢资源。
@@ -84,3 +69,8 @@ JUDGE_STATE_DIR=
# --- 只有机房那套需要 ---
# 机房走 http 直连 IP,没有 TLS,必须 false,否则 Cookie 发不回来。
COOKIE_SECURE=false
# 机房没有本地库,连服务器的 5445。不填时默认指向服务器(默认值在 compose.school.yml 里),
# 换服务器才要填。服务器那套不读这两个。
DB_HOST=
DB_PORT=
+6 -11
View File
@@ -4,14 +4,10 @@
#
# ## 为什么不走 docker compose exec
#
# 线上是**外接形态**:postgres 容器由 /root/OJDeploy/docker-compose.yml 起,不归
# OJ2 这套 compose 管。compose.debian.yml 里虽然也定义了 oj-postgres,但它挂着
# `profiles: ["local-data"]`,只在「自带数据」形态下才启动。所以在 OJ2 目录里跑
#
# docker compose exec -T oj-postgres pg_dumpall ...
#
# 会报 `service "oj-postgres" is not running` —— 容器明明在跑,只是属于另一个
# compose 项目。这里直接按容器名 `docker exec`,跟谁起的无关,两种形态都能用。
# 按容器名 `docker exec`,跟容器是谁起的无关:以前线上的 postgres 由旧栈那份
# /root/OJDeploy/docker-compose.yml 起,在 OJ2 目录里 `docker compose exec` 会报
# `service "oj-postgres" is not running`。现在它归 compose.debian.yml 了,但按名字找
# 照样能用,move-db-into-oj2.sh 搬家时也靠这一点去备份旧容器(CONTAINER=旧容器名)。
#
# ## 用法
#
@@ -104,9 +100,8 @@ human() {
say "自检"
docker ps --filter "name=^/${CONTAINER}\$" --filter status=running -q | grep -q . \
|| die "容器 $CONTAINER 没在跑。用 CONTAINER=... 指定容器名;外接形态下它由
/root/OJDeploy/docker-compose.yml 起:
docker compose -f /root/OJDeploy/docker-compose.yml up -d $CONTAINER"
|| die "容器 $CONTAINER 没在跑。用 CONTAINER=... 指定容器名;服务器上它由本栈起:
docker compose -f docker/compose.debian.yml --env-file docker/.env up -d $CONTAINER"
# 用 psql 真连一次,不用 pg_isready:后者只探「服务在不在」,DB_USER 写错照样说 OK,
# 要等到 pg_dumpall 那步才炸出一句 role does not exist
+13 -48
View File
@@ -1,51 +1,32 @@
# 服务器(xuyue.cc)。数据库在这里,机房那套连的就是这里的 5445。
# 服务器(oj.xuyue.cc)。数据库在这里,机房那套连的就是这里的 5445。
#
# docker compose -f docker/compose.debian.yml --env-file docker/.env up -d --build
# docker/deploy.sh # 平时走这个(自检 → 构建 → 迁移 → 起栈 → 冒烟)
#
# 构建上下文是仓库根,所以 build.context 写 ..(相对本文件)。
#
# 和旧的 docker-compose.debian.yml 的对应关系:
# oj-backend(supervisord 跑 caddy+gunicorn+dramatiq)→ 拆成 oj-web + oj-api + oj-worker
# 端口、数据目录、判题机配置全部保持不变,这样回滚只是换回旧 compose。
#
# ## 两种形态
#
# **自带数据**(演练用的就是这个):postgres 和 redis 也由本文件起,要显式加 profile。
#
# docker compose -f docker/compose.debian.yml --env-file docker/.env --profile local-data up -d
#
# **只换前后端**(默认,上线用这个):旧栈的 postgres / redis 容器继续跑,
# 这里只起 api / worker / web / judge,
# 通过 env 指过去。切换当天数据库进程根本不重启,库和文件都不用挪位置。
#
# docker compose -f docker/compose.debian.yml --env-file docker/.env up -d
#
# 后者要在 env 里设 `DB_HOST` / `REDIS_HOST` / `DATA_DIR`,见 `.env.example` 末尾。
#
# **并行试跑**(新站挂在 oj2.xuyue.cc、旧站原样不动)是「只换前后端」再加两个变量:
# `WEB_PORT`(8080 被旧 backend 占着)和 `JUDGE_STATE_DIR`(两个判题机不能共用运行目录)。
# 这种形态下旧栈一个容器都不用停。见手册第四节。
# **服务器上的全部容器都由这一个文件起**,包括 postgres 和 redis。以前库和 redis 是旧栈
# 那份 /root/OJDeploy/docker-compose.yml 起的、这里通过 DB_HOST 外接过去,升级它们就得去
# 改那份文件;2026-10 起收了进来(搬家用 docker/move-db-into-oj2.sh,一次性的)。
#
# ⚠️ `DATA_DIR` 默认值 `../data` 解析出来是 **`OJ2/data`**,不是部署目录的 `data/`。
# 旧栈用的是 `<部署目录>/data/`,两者不是一个地方 —— 直接用默认值切过去,postgres 会在
# 空目录上初始化一个全新的空库,测试点和题面图片也全都不在。**用旧数据就必须设 `DATA_DIR`。**
# 不设它的话 postgres 会在空目录上初始化一个全新的空库,测试点和题面图片也全都不在,
# 而且**不报错**。`deploy.sh` 为此专门设了自检。
services:
oj-postgres:
image: postgres:18-alpine
container_name: oj-postgres
restart: always
# 只在「自带数据」形态下启动。用旧栈的库时不启动它 —— 否则 5445 端口会和
# 旧的 postgres 撞,而且两个进程开同一个数据目录本来也起不来。
profiles: ["local-data"]
# 18 起镜像的数据目录是 /var/lib/postgresql/18/docker,挂载点要是上一级,
# 还挂 .../data 的话 18 直接拒绝启动。宿主机目录也换了名:`postgres/` 是 16 的老库,
# 18 读不了,分开放才能留着它做回退(升级步骤见 docs/deploy.md「PostgreSQL 升级」)
# 还挂 .../data 的话 18 直接拒绝启动。宿主机目录叫 `postgresql/`,和旧栈 16 的
# `postgres/` 分开:18 读不了 16 的目录,分开放才留得住老库做回退
volumes:
- ${DATA_DIR:-../data}/postgresql:/var/lib/postgresql
environment:
POSTGRES_DB: onlinejudge
POSTGRES_USER: onlinejudge
# 只在空目录上初始化时用一次,之后改它不会改库里的口令(要 ALTER ROLE)。
# 这个账号是超级用户、端口又对公网开着,必须是 openssl rand -hex 32 那种强口令
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?请在 docker/.env 里设置 POSTGRES_PASSWORD}
# 机房那套要连这个端口,必须对外暴露
ports:
@@ -60,10 +41,6 @@ services:
image: redis:8-alpine
container_name: oj-redis
restart: always
# 同上:旧栈的 redis 也发布了 5446,两个一起跑会撞端口。
# redis 里没有非丢不可的东西(会话、判题队列),用旧的那个也无所谓 ——
# 旧后端已经停了,新后端独占它,剩下的 Django / dramatiq 残留键前缀不同,互不干扰。
profiles: ["local-data"]
# 会话存在这里:谁能连上它,谁就能写一条 session:<token> 冒充超管。所以必须带密码,
# 端口也只发布在本机(api / worker 走 compose 网络连 oj-redis,用不着宿主机端口)。
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?}"]
@@ -125,26 +102,16 @@ services:
container_name: oj-api
restart: always
depends_on:
# required: false —— 「只换前后端」时这两个服务不在启动集合里(profile 未启用),
# 严格的 depends_on 会让整个 project 直接判定 invalid(实测过,不是猜的)。
# 代价:自带数据形态下 postgres 起不来时,compose 只警告不中止,oj-api 照样起,
# 然后自己 crash 循环。看 `docker compose ps`,oj-api 会是 unhealthy。
oj-postgres:
condition: service_healthy
required: false
oj-redis:
condition: service_healthy
required: false
# 用旧栈的库时,库在宿主机上(旧 postgres 发布了 5445),走 host-gateway 过去,
# 不出本机、不走公网。DB_HOST 填 host.docker.internal 即可。
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ${DATA_DIR:-../data}/backend:/data
environment: &api-env
DATABASE_URL: postgres://onlinejudge:${POSTGRES_PASSWORD:?}@${DB_HOST:-oj-postgres}:${DB_PORT:-5432}/onlinejudge
DATABASE_URL: postgres://onlinejudge:${POSTGRES_PASSWORD:?}@oj-postgres:5432/onlinejudge
# 密码拼进 URL,所以只能用 URL 安全的字符 —— 用 openssl rand -hex 32 生成
REDIS_URL: redis://:${REDIS_PASSWORD:?请在 docker/.env 里设置 REDIS_PASSWORD}@${REDIS_HOST:-oj-redis}:${REDIS_PORT:-6379}
REDIS_URL: redis://:${REDIS_PASSWORD:?请在 docker/.env 里设置 REDIS_PASSWORD}@oj-redis:6379
JUDGE_SERVER_URL: http://oj-judge:8080
JUDGE_SERVER_TOKEN: ${OJ2_JUDGE_TOKEN:?}
JUDGE_CONCURRENCY: ${JUDGE_CONCURRENCY:-2}
@@ -168,8 +135,6 @@ services:
restart: always
depends_on:
- oj-api
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ${DATA_DIR:-../data}/backend:/data
environment: *api-env
+25 -83
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
#
# 在**服务器上**起 OJ2 新栈(并行试跑形态:旧栈的 postgres/redis 继续跑,
# 这里只起 api / worker / web / judge,旧站一个容器都不用停)。
# 在**服务器上**起整套 OJ2:postgres / redis / 判题沙箱 / api / worker / web,
# 全部由 docker/compose.debian.yml 管。
#
# cd /root/OJDeploy/OJ2
# docker/deploy.sh # 自检 → 构建 → 起栈 → 冒烟
@@ -61,28 +61,12 @@ say "自检"
[ -f docker/.env ] || die "docker/.env 不存在。内容见手册第三节,别用 .env.example 直接改名(里面的值全是空的)"
# 部署形态。判据用 DB_HOST 而不是另加一个开关:它本来就决定了 DATABASE_URL 指向谁,
# 两个变量各说各话的话,迟早会出现「起了自带的库、却连到别处」这种自相矛盾的状态。
#
# DB_HOST 为空 → 自带数据:本栈起 oj-postgres / oj-redis(compose 里挂在 local-data profile 下)
# DB_HOST 非空 → 外接数据:连别处的库,试跑期间连的是旧栈那两个容器
db_host=$(grep -E '^DB_HOST=' docker/.env | tail -1 | cut -d= -f2- || true)
if [ -z "$db_host" ]; then
LOCAL_DATA=1
COMPOSE+=(--profile local-data)
ok "形态:自带数据(postgres / redis 由本栈起)"
else
LOCAL_DATA=0
ok "形态:外接数据(库在 $db_host)"
fi
# 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"
# 以前库和 redis 是外接旧栈的,靠这几个变量指过去;现在 compose 不再读它们。
# 留在 .env 里不碍事,但会让人以为还在生效
for v in DB_HOST DB_PORT REDIS_HOST REDIS_PORT; do
[ -z "$(grep -E "^$v=." docker/.env || true)" ] \
|| warn "docker/.env 里的 $v 已经不用了(库和 redis 由本栈起),可以删掉"
done
# --prebuilt 时产物就是构建输入。缺了的话 docker 只会甩一句 COPY 找不到文件,
# 这里提前拦下并说清楚该怎么办。
@@ -118,38 +102,22 @@ $bad
检查 docker/.env 里的 DATA_DIR(注意:DATA_DIR= 空值等于没设,等号两边不能有空格)"
ok "数据卷都在 $(grep 'source:' <<<"$cfg" | head -1 | sed 's|.*source: ||; s|/backend.*||')"
# ② 库指向必须和形态一致,两个方向都要查
if [ "$LOCAL_DATA" -eq 0 ]; then
# 外接:DB_HOST 不生效会静默回落成 oj-postgres,而这形态下本栈不起那个容器
! grep -q 'DATABASE_URL: postgres://[^@]*@oj-postgres' <<<"$cfg" \
|| die "DB_HOST 没生效,DATABASE_URL 还指着 oj-postgres —— 外接形态下本栈不起它,必然连不上库"
else
# 自带:反过来必须指向服务名 + 容器内端口。连 :5432 一起查,是因为清了 DB_HOST 却
# 忘了清 DB_PORT 的话会拼出 oj-postgres:5445 —— 那是宿主机映射端口,容器网络里不通。
grep -q 'DATABASE_URL: postgres://[^@]*@oj-postgres:5432/' <<<"$cfg" \
|| die "自带数据形态(DB_HOST 为空),但 DATABASE_URL 指向 $(grep -m1 'DATABASE_URL:' <<<"$cfg" | sed 's|.*@||; s|/onlinejudge.*||')。
docker/.env 里 DB_PORT / REDIS_PORT 是不是也得一并清空?"
fi
ok "库指向 $(grep -m1 'DATABASE_URL:' <<<"$cfg" | sed 's|.*@||; s|/onlinejudge.*||')"
# ②b 自带形态下数据目录必须已经是一个 18 版本的库。
# ② 数据目录必须已经是一个 18 版本的库。
# 这是整个部署里唯一会**静默**走歪的地方:目录不对的话 postgres 当成全新部署,
# 在空目录上初始化一个空库 —— 站点起得来、能注册能登录,就是一道题都没有。
if [ "$LOCAL_DATA" -eq 1 ]; then
data_dir=$(grep -E '^DATA_DIR=' docker/.env | tail -1 | cut -d= -f2- || true)
[ -n "$data_dir" ] || die "自带数据形态必须显式设 DATA_DIR。
data_dir=$(grep -E '^DATA_DIR=' docker/.env | tail -1 | cut -d= -f2- || true)
[ -n "$data_dir" ] || die "必须显式设 DATA_DIR。
默认值 ../data 解析出来是 OJ2/data,那是个空目录,postgres 会在上面初始化一个全新的空库,而且不报任何错。"
pgver_file="$data_dir/postgresql/18/docker/PG_VERSION"
if [ ! -f "$pgver_file" ] && [ -f "$data_dir/postgres/PG_VERSION" ]; then
die "$data_dir 下只有 postgres/(PostgreSQL $(cat "$data_dir/postgres/PG_VERSION") 的老库),没有 postgresql/18/docker。
compose 里已经是 postgres:18-alpine,老库要先倒过去,见 docs/deploy.md「PostgreSQL 升级」。"
fi
[ -f "$pgver_file" ] || die "找不到 $pgver_file —— DATA_DIR 指的不是一个已有的 postgres 数据目录。
就这么起下去会得到一个空库。确认 DATA_DIR=$data_dir 对不对。"
pgver=$(cat "$pgver_file")
[ "$pgver" = 18 ] || die "$data_dir/postgresql 是 PostgreSQL $pgver 的数据目录,但 compose 里是 postgres:18-alpine,起不来。"
ok "数据目录 $data_dir/postgresql(PostgreSQL $pgver)"
pgver_file="$data_dir/postgresql/18/docker/PG_VERSION"
if [ ! -e "$data_dir/postgresql" ] && [ -d "$data_dir/postgres" ]; then
die "$data_dir 下只有 postgres/(PostgreSQL 16 的老库,旧栈 compose 起的),
还没有 postgresql/18/docker。先把库搬进本栈:docker/move-db-into-oj2.sh(一次性的,见 docs/deploy.md)"
fi
[ -f "$pgver_file" ] || die "找不到 $pgver_file —— DATA_DIR 指的不是一个已有的 postgres 数据目录。
就这么起下去会得到一个空库。确认 DATA_DIR=$data_dir 对不对。"
pgver=$(cat "$pgver_file")
[ "$pgver" = 18 ] || die "$data_dir/postgresql 是 PostgreSQL $pgver 的数据目录,但 compose 里是 postgres:18-alpine,起不来。"
ok "数据目录 $data_dir/postgresql(PostgreSQL $pgver)"
# ③ 判题机运行目录必须和旧栈分开,否则两个 judger 往同一个目录里写
judge_dir=$(grep -E '^JUDGE_STATE_DIR=' docker/.env | tail -1 | cut -d= -f2- || true)
@@ -171,29 +139,6 @@ if [ -n "$judge_image" ]; then
ok "判题镜像 $judge_image 在本机"
fi
# ⑤ 外接形态才需要预检:库不归本栈管,得确认它已经活着。
# 自带形态下这两个容器就是本栈自己起的,起栈那步会拉起来,这里没什么可查。
if [ "$LOCAL_DATA" -eq 0 ]; then
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 都在跑"
# 外接的 redis 不归本栈管,口令是否真的生效只能在这里查。旧栈那份 compose 的 redis
# 原本没有口令、端口发布在公网上 —— 会话存在里面,等于人人可以伪造超管登录态。
# 不带口令能 PING 通就说明那边还没加 --requirepass,这种状态不许上线。
redis_pw=$(grep -E '^REDIS_PASSWORD=' docker/.env | tail -1 | cut -d= -f2- || true)
[ -n "$redis_pw" ] || die "docker/.env 里没有 REDIS_PASSWORD(openssl rand -hex 32 生成)"
[ "$(docker exec -e REDISCLI_AUTH= oj-redis redis-cli ping 2>/dev/null)" != PONG ] \
|| die "外接的 oj-redis 不带口令也能连上 —— 先给它加 --requirepass,见 docs/deploy.md「Redis 口令」"
[ "$(docker exec -e REDISCLI_AUTH="$redis_pw" oj-redis redis-cli ping 2>/dev/null)" = PONG ] \
|| die "用 docker/.env 的 REDIS_PASSWORD 连不上外接的 oj-redis —— 两边口令不一致"
ok "外接的 redis 要求口令,且和 docker/.env 一致"
else
ok "postgres / redis 由本栈起,不预检"
fi
port=$(grep -E '^WEB_PORT=' docker/.env | tail -1 | cut -d= -f2- || true)
port=${port:-8080}
ok "对外端口 $port"
@@ -212,12 +157,9 @@ fi
# 在起栈**之前**跑:schema 先就位,新代码再启动。
#
# **不要加 --no-deps。** `run` 只会拉起被点名服务的依赖,而 oj-api 的依赖恰好就是
# oj-postgres / oj-redis:
# · 自带数据形态 —— 必须靠它把 postgres 起来,否则这一步连不上库(加过 --no-deps,
# oj-postgres / oj-redis:必须靠它把 postgres 起来,否则这一步连不上库(加过 --no-deps,
# 踩过这个坑)。depends_on 的 condition: service_healthy 还顺带保证了库真的就绪。
# · 外接形态 —— 那两个服务不在启用的 profile 里,depends_on 上的 required: false
# 让 compose 直接跳过,不会多起任何东西。
# 两种形态下 worker / web 都不会被带起来,它们不是 api 的依赖。
# worker / web 不会被带起来,它们不是 api 的依赖。
#
# 含 DROP TABLE 这类破坏性语句的迁移会被拦下并退出 4,需要显式放行:
# OJ2_ALLOW_DESTRUCTIVE=1 docker/deploy.sh
@@ -230,7 +172,7 @@ say "数据库迁移"
它对每种情况都打印了具体该做什么,别照搬下面这几条猜。常见的三种:
· 破坏性语句被拦 → 确认备份后 OJ2_ALLOW_DESTRUCTIVE=1 docker/deploy.sh
· 缺迁移基线记录 → 按它打印的 SQL 插一行
· 连不上库 → 自带数据形态下 postgres 是否起来了、DATA_DIR 对不对"
· 连不上库 → oj-postgres 是否起来了(docker logs oj-postgres)、DATA_DIR 对不对"
say "起栈"
"${COMPOSE[@]}" up -d
@@ -254,13 +196,13 @@ 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"
[ "${total:-0}" -gt 0 ] || die "题目数是 0 —— 连的不是生产库。查 DATA_DIR"
cat <<EOF
$(printf '\033[1;32m✅ 起来了\033[0m')
https://oj2.xuyue.cc (NPM 反代已配好,指向宿主机:$port)
https://oj.xuyue.cc (NPM 反代已配好,指向宿主机:$port)
NPM 那边是一次性的,只有改了 WEB_PORT 才要回去动 proxy host 的端口。另外两项
别关:Websockets Support(关了学生那边「判题中…」永远不动)、
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env bash
#
# 一次性:把服务器上的 postgres / redis 从旧栈那份 /root/OJDeploy/docker-compose.yml
# 搬进 OJ2 的 compose.debian.yml,顺带 PostgreSQL 16 → 18、Redis 7 → 8。
# 搬完以后服务器上的全部容器都归 OJ2 管,旧 compose 文件里那两个服务就没用了。
#
# cd /root/OJDeploy/OJ2
# docker/move-db-into-oj2.sh # 服务器自己编镜像(慢,但不依赖 CI)
# docker/move-db-into-oj2.sh --prebuilt # 用 CI rsync 过来的产物
#
# ## 跑之前
#
# 1. **挑没课的时候。** 两个站点共用这个库,中间两边都用不了。镜像先在旧栈照常服务时
# 编好,真正停机的只有「备份 → 恢复 → 起栈」那一段。
# 2. **机房那台先停应用**(库在这边,机房连着它就恢复不了):
# docker compose -f docker/compose.school.yml --env-file docker/.env.school stop oj-api oj-worker
# 没停的话脚本会查出来还有谁连着库,报出 IP 后中止。
# 3. **docker/.env 的 POSTGRES_PASSWORD 换成强口令**(openssl rand -hex 32)。
# 原来的口令是默认的 onlinejudge,而这个账号是超级用户、5445 对公网开着 ——
# 知道默认口令的人就能拿到整个库,还能在容器里执行命令。新库直接用新口令建,
# 脚本不接受弱口令。机房的 .env.school 要改成同一个值。
#
# ## 它做什么
#
# 自检 → 编镜像 → 停本机 api/worker → 确认没人连着旧库 → 记行数 → 备份
# → 停掉并删除旧的 postgres / redis 容器 → 起本栈的 PostgreSQL 18 / Redis 8
# → 恢复 → 收集统计信息 → 核对行数和网络登录 → docker/deploy.sh --no-build
#
# 旧库的数据目录 `$DATA_DIR/postgres`(16)**一个字节都不动**,新库在
# `$DATA_DIR/postgresql`。Redis 的 `$DATA_DIR/redis` 新旧共用,8 直接读 7 的 dump.rdb。
#
# ## 恢复时口令为什么要剥掉
#
# pg_dumpall 会带一句 `ALTER ROLE onlinejudge ... PASSWORD 'md5…'`,原样灌进去会把新库
# 的口令改回旧的 md5 哈希。而 18 的镜像新建的库 pg_hba 只认 scram-sha-256,md5 存的
# 口令**永远验不过** —— 容器里 docker exec psql 走本地 socket 是 trust,看着一切正常,
# 只有 api 和机房从网络连进来才炸。所以恢复时把那个 PASSWORD 子句删掉,库里留下的就是
# 初始化时按 POSTGRES_PASSWORD 存的 SCRAM 哈希。
#
# ## 中途失败
#
# 停旧容器之前失败:什么都没动,`docker start oj-api oj-worker` 就回去了(容器还是原来的
# 配置)。别用 `docker compose ... start`:新的 compose 里 api 依赖本栈的 oj-postgres,
# 这时它还不存在,compose 直接拒绝。
# 之后失败:旧库目录原样在,回退见 docs/deploy.md「把库搬进 OJ2」。
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
COMPOSE=(docker compose -f docker/compose.debian.yml --env-file docker/.env)
for arg in "$@"; do
case "$arg" in
--prebuilt) export ARTIFACTS=prebuilt ;;
*) echo "未知参数:$arg(可用:--prebuilt)" >&2; exit 2 ;;
esac
done
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
die() { printf '\n\033[1;31m❌ %s\033[0m\n\n' "$*" >&2; exit 1; }
envget() { grep -E "^$1=" docker/.env | tail -1 | cut -d= -f2- || true; }
# ---------------------------------------------------------------- 自检
say "自检"
[ -f docker/.env ] || die "docker/.env 不存在"
data_dir=$(envget DATA_DIR)
[ -n "$data_dir" ] && [ -d "$data_dir" ] || die "DATA_DIR($data_dir)没设或不是目录。服务器上应该是 /root/OJDeploy/data"
[ ! -e "$data_dir/postgresql" ] || die "$data_dir/postgresql 已经存在 —— 已经搬过了?
如果是上次搬到一半失败留下的,确认里面没有要的东西,删掉它再跑。"
ok "数据目录 $data_dir"
pg_pw=$(envget POSTGRES_PASSWORD)
[ "${#pg_pw}" -ge 16 ] && [ "$pg_pw" != onlinejudge ] || die "docker/.env 的 POSTGRES_PASSWORD 太弱(不到 16 位,或者还是默认的 onlinejudge)。
这个账号是超级用户、5445 对公网开着。换一个:openssl rand -hex 32
机房的 docker/.env.school 要改成同一个值。"
[[ "$pg_pw" =~ ^[A-Za-z0-9._~-]+$ ]] || die "POSTGRES_PASSWORD 里有 URL 不安全的字符(它会拼进 DATABASE_URL),用 openssl rand -hex 32 生成"
ok "POSTGRES_PASSWORD 是强口令"
redis_pw=$(envget REDIS_PASSWORD)
[ -n "$redis_pw" ] || die "docker/.env 里没有 REDIS_PASSWORD"
# 旧容器按发布的端口找,不按名字:旧 compose 起的容器叫什么不归我们管
old_pg=$(docker ps --filter publish=5445 --format '{{.Names}}' | head -1)
old_redis=$(docker ps --filter publish=5446 --format '{{.Names}}' | head -1)
[ -n "$old_pg" ] || die "没找到发布 5445 的容器(旧的 postgres)。它没在跑?"
[ -n "$old_redis" ] || die "没找到发布 5446 的容器(旧的 redis)。它没在跑?"
for c in "$old_pg" "$old_redis"; do
files=$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.config_files"}}' "$c")
[[ "$files" != *compose.debian.yml* ]] || die "$c 已经是 OJ2 的 compose 起的 —— 已经搬过了"
done
pgver=$(docker exec "$old_pg" psql -U onlinejudge -d onlinejudge -tAc 'show server_version_num')
[ "${pgver:0:2}" = 16 ] || die "$old_pg 是 PostgreSQL $pgver,这个脚本只认 16"
ok "旧 postgres:$old_pg($(docker inspect -f '{{.Config.Image}}' "$old_pg"))"
ok "旧 redis:$old_redis($(docker inspect -f '{{.Config.Image}}' "$old_redis"))"
redis_src=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Source}}{{end}}{{end}}' "$old_redis")
[ -n "$redis_src" ] || die "$old_redis 的 /data 没挂到宿主机目录上,会话和队列搬不过来"
db_bytes=$(docker exec "$old_pg" psql -U onlinejudge -d postgres -tAc "select pg_database_size('onlinejudge')")
avail=$(( $(df -Pk "$data_dir" | awk 'NR==2 {print $4}') * 1024 ))
[ "$avail" -gt $(( db_bytes * 3 )) ] || die "磁盘余量 $((avail >> 20)) MiB,不到库大小($((db_bytes >> 20)) MiB)的三倍:新库一份、备份一份,还得留余地"
ok "库 $((db_bytes >> 20)) MiB,磁盘余量 $((avail >> 20)) MiB"
judge_image=$("${COMPOSE[@]}" config | grep -m1 -E '^[[:space:]]*image: oj2-judge' | awk '{print $2}')
docker image inspect "$judge_image" >/dev/null 2>&1 || die "判题镜像 $judge_image 不在本机,见 docker/judge/README.md"
# ---------------------------------------------------------------- 编镜像
# 旧栈这时还在照常服务,编完才开始停机
say "编镜像(旧栈照常服务中)"
"${COMPOSE[@]}" build oj-api oj-worker oj-web
docker pull -q postgres:18-alpine >/dev/null
docker pull -q redis:8-alpine >/dev/null
ok "postgres:18-alpine、redis:8-alpine 已拉取"
# ---------------------------------------------------------------- 停机
say "停本机的 api / worker(从这里开始站点不可用)"
"${COMPOSE[@]}" stop oj-api oj-worker
# 机房的 api / worker 也连着这个库。停没停只能从库这边看
clients=""
for _ in $(seq 15); do
clients=$(docker exec "$old_pg" psql -U onlinejudge -d postgres -tAc \
"select string_agg(distinct coalesce(host(client_addr), 'local'), ', ')
from pg_stat_activity
where backend_type = 'client backend' and pid <> pg_backend_pid()")
[ -z "$clients" ] && break
sleep 2
done
[ -z "$clients" ] || die "还有客户端连着旧库:$clients
多半是机房的 api / worker 没停。停掉它们再跑;要先恢复本机服务的话:
docker start oj-api oj-worker"
ok "没有客户端连着旧库"
counts_sql="select (select count(*) from submission) || ' ' || (select count(*) from \"user\") || ' ' || (select count(*) from problem)"
before=$(docker exec "$old_pg" psql -U onlinejudge -d onlinejudge -tAc "$counts_sql")
ok "提交 / 用户 / 题目:$before"
# ---------------------------------------------------------------- 备份
say "备份"
backup_dir="${BACKUP_DIR:-$(dirname "$PWD")/backups}"
marker=$(mktemp)
CONTAINER="$old_pg" BACKUP_DIR="$backup_dir" docker/backup-db.sh
backup=$(find "$backup_dir" -maxdepth 1 -name 'db_backup_*.sql.gz' -newer "$marker" | sort | tail -1)
rm -f "$marker"
[ -n "$backup" ] || die "backup-db.sh 跑完了但找不到新的备份文件($backup_dir)"
ok "备份:$backup"
# ---------------------------------------------------------------- 换容器
say "停掉旧的 postgres / redis"
docker exec -e REDISCLI_AUTH="$redis_pw" "$old_redis" redis-cli save >/dev/null \
|| warn "redis save 失败(口令不对?),会话可能丢,学生重新登录一次就好"
docker stop "$old_pg" "$old_redis" >/dev/null
# 删掉而不只是停:新容器要用同样的名字和端口
docker rm "$old_pg" "$old_redis" >/dev/null
ok "已删除容器 $old_pg、$old_redis(数据目录没动)"
if [ "$redis_src" != "$data_dir/redis" ]; then
mkdir -p "$data_dir/redis"
[ -e "$data_dir/redis/dump.rdb" ] || cp -a "$redis_src/." "$data_dir/redis/"
ok "redis 数据从 $redis_src 拷到 $data_dir/redis"
fi
say "起 PostgreSQL 18 / Redis 8"
"${COMPOSE[@]}" up -d oj-postgres oj-redis
# 不用 pg_isready 等:初始化期间镜像会起一个只听 socket 的临时实例,pg_isready 走
# socket 照样说 OK,这时候灌数据会灌进一个马上要被关掉的实例。走网络连就等得到正式的那个,
# 还顺带验了新口令能不能从网络登录(api 和机房就是这么连的)
tcp_check() {
docker exec -e PGPASSWORD="$pg_pw" oj-postgres sh -c \
'psql -h "$(hostname -i | cut -d" " -f1)" -U onlinejudge -d onlinejudge -tAc "select 1"' >/dev/null 2>&1
}
for _ in $(seq 60); do tcp_check && break; sleep 2; done
tcp_check || die "PostgreSQL 18 两分钟没起来,看 docker logs oj-postgres"
ok "PostgreSQL 18 起来了,新口令能从网络登录"
# ---------------------------------------------------------------- 恢复
say "恢复"
log=$(mktemp)
started=$(date +%s)
gzip -cd "$backup" \
| sed -E "/^ALTER ROLE onlinejudge WITH /s/ PASSWORD '[^']*'//" \
| docker exec -i oj-postgres psql -q -U onlinejudge -d postgres >/dev/null 2>"$log" || true
# 「先删后建」的 dump 往新实例灌,这两条必然出现(见 backup-db.sh 头部),别的都不该有
unexpected=$(grep 'ERROR' "$log" | grep -v -e 'current user cannot be dropped' -e 'role "onlinejudge" already exists' || true)
[ -z "$unexpected" ] || die "恢复时出了预期之外的错(完整日志 $log):
$unexpected"
rm -f "$log"
ok "恢复完成,用时 $(( $(date +%s) - started ))s"
docker exec oj-postgres vacuumdb -U onlinejudge --all --analyze-in-stages -q
ok "统计信息已收集"
after=$(docker exec oj-postgres psql -U onlinejudge -d onlinejudge -tAc "$counts_sql")
[ "$after" = "$before" ] || die "行数对不上:恢复前 $before,恢复后 $after"
ok "提交 / 用户 / 题目:$after,和恢复前一致"
tcp_check || die "恢复之后新口令从网络登不上了 —— dump 里的口令没剥干净?
docker exec -it oj-postgres psql -U onlinejudge -d postgres -c '\\password onlinejudge'"
ok "新口令仍能从网络登录"
# ---------------------------------------------------------------- 起栈
say "起栈(交给 deploy.sh)"
docker/deploy.sh --no-build
cat <<EOF
$(printf '\033[1;32m✅ 搬完了\033[0m')
还剩三件事:
1. 机房:docker/.env.school 的 POSTGRES_PASSWORD 改成和这边一样的新值,然后
docker compose -f docker/compose.school.yml --env-file docker/.env.school up -d
2. /root/OJDeploy/docker-compose.yml 里的 oj-postgres / oj-redis 已经没用了,删掉
(整个文件都没别的服务的话就删文件)。留着的话哪天有人 up 一下,会和本栈抢端口。
3. 旧库目录 $data_dir/postgres 留着做回退,跑稳一两周再删。
备份在 $backup
EOF
+64 -71
View File
@@ -9,7 +9,7 @@
|---|---|---|
| compose | `docker/compose.debian.yml` | `docker/compose.school.yml` |
| env | `docker/.env` | `docker/.env.school` |
| 库 | 本机 postgres(旧栈起的,原地没动) | **没有,连服务器那台的 5445** |
| 库 | 本栈的 `oj-postgres`(PostgreSQL 18) | **没有,连服务器那台的 5445** |
| Redis / 判题沙箱 | 各自一套 | 各自一套 |
| 对外 | NPM 反代 → `WEB_PORT` | http 直连 IP,端口 81 |
@@ -20,42 +20,36 @@ Redis)和 WebSocket 推送(本地 pub/sub)是每站独立的 —— 学生
机房那套 `COOKIE_SECURE=false`:走 http 直连 IP,带 `Secure` 的 Cookie 浏览器不回传,
表现是「登录成功又立刻变未登录」。
## 三种数据形态
## 全部容器都归 OJ2
`compose.debian.yml` 靠 env 切换,判据是 `DB_HOST`:
服务器上 postgres / redis / 判题沙箱 / api / worker / web **全由 `compose.debian.yml` 起**,
升级哪个都只改仓库里的文件。
- **外接数据**(线上就是这个):设 `DATA_DIR` / `DB_HOST` / `REDIS_HOST`,
沿用已经在跑的 postgres 和 redis,本栈只起 api / worker / web / judge。
- **自带数据**(本机、演练):那几个变量留空,起栈时加 `--profile local-data`。
- 试跑形态额外要 `WEB_PORT` 和 `JUDGE_STATE_DIR`(两个判题机不能共用运行目录)。
2026-10 以前库和 redis 是旧栈那份 `/root/OJDeploy/docker-compose.yml` 起的,OJ2 通过
`DB_HOST` / `REDIS_HOST` 外接过去,升个版本就得去服务器上改那份文件。这种「外接」形态
已经删掉了,搬家见下面「把库搬进 OJ2」。
⚠️ **`DATA_DIR` 默认值 `../data` 是 `OJ2/data`,不是部署目录的 `data/`。**
沿用旧数据却忘了设它,会静默挂上一堆空目录:空库、没测试点、题面图片 404,
**而且不报错**。这是整个部署里唯一会静默走歪的地方,`deploy.sh` 专门为它设了一道自检。
忘了设它,postgres 会在空目录上初始化一个空库,测试点和题面图片也都不在,
**而且不报错**。这是整个部署里唯一会静默走歪的地方,`deploy.sh` 专门为它设了自检
(`$DATA_DIR/postgresql/18/docker` 必须已经是一个库)。
## Redis 口令
## 口令
**会话存在 Redis 里**(`session:<token>` → 用户 id),谁能连上它,谁就能写一条会话
冒充任意用户,包括超管。所以服务器那台的 Redis **必须带口令**:`compose.debian.yml`
的 `REDIS_URL` 用 `${REDIS_PASSWORD:?}` 强制要求,`deploy.sh` 在外接形态下还会实测
「不带口令连不上、带 `docker/.env` 的口令连得上」,两条有一条不满足就中止部署。
冒充任意用户,包括超管。所以服务器那台的 Redis **必须带口令**(`REDIS_PASSWORD`,
compose 里 `:?` 强制要求),端口也只发布在 127.0.0.1。2026-09-27 以前它发布在
`0.0.0.0:5446`、不带口令,从公网直接 PING 得通(实测过)。
2026-09-27 以前,旧栈 compose 起的那个 `oj-redis` 发布在 `0.0.0.0:5446`、不带口令,
**从公网直接 PING 得通**(实测过)。给它加口令的一次性步骤,在服务器上:
**数据库口令同理**:`onlinejudge` 是超级用户,5445 对公网开着(机房要连),2026-10 以前
口令就是默认的 `onlinejudge`。搬进 OJ2 时用 `openssl rand -hex 32` 生成的新口令重建,
`move-db-into-oj2.sh` 不接受弱口令。`POSTGRES_PASSWORD` 只在库初始化时生效一次,以后要换:
```bash
cd /root/OJDeploy
PW=$(openssl rand -hex 32)
echo "REDIS_PASSWORD=$PW" >> OJ2/docker/.env
# 编辑 docker-compose.yml 的 oj-redis,加一行($PW 换成上面的值,别提交到任何仓库):
# command: ["redis-server", "--requirepass", "<PW>"]
docker compose up -d oj-redis # 重建 redis,此刻起旧口令(无)的连接全部失败
cd OJ2 && docker/deploy.sh # 马上重新部署,api / worker 带上口令
docker exec -it oj-postgres psql -U onlinejudge -d postgres -c '\password onlinejudge'
# 再改 docker/.env 和机房 docker/.env.school 的 POSTGRES_PASSWORD,两边都重新 up -d
```
两步之间 api 连不上 Redis,站点会 500 一两分钟,挑没人用的时候做。
会话会保留(redis 退出时落盘 RDB),学生不用重新登录。
机房那台的 `oj-redis` 不发布端口、只在 compose 网络里,不需要口令。
## 上线
@@ -87,9 +81,9 @@ docker/deploy.sh --no-build # 只改了 env / compose 时跳过构建
代码怎么上到服务器不归它管(rsync 命令在脚本头部注释里)。
脚本起栈前有一串自检,**每一条都是真撞到过的**:compose 版本 ≥ 2.20、`DATA_DIR`
有没有生效、库指向和形态是否自洽、判题机运行目录有没有和旧栈分开、外接的
postgres / redis 是否活着。起完再跑四条冒烟,**题目数是 0 也中止** —— 那意味着连错库了。
脚本起栈前有一串自检,**每一条都是真撞到过的**:`DATA_DIR` 有没有生效、数据目录是不是
一个 18 的库、判题机运行目录、判题镜像在不在。起完再跑四条冒烟,**题目数是 0 也中止**
—— 那意味着连错库了。
### 判题镜像是自建的,不在 registry 上
@@ -130,8 +124,8 @@ curl -s -o /dev/null -w '未登录进后台 %{http_code}\n' $BASE/api/admin/das
两个失败模式的症状别搞混:
- **题目列表 `"total":0`** → 连错库了(`DB_HOST` 没设,或误加了 `--profile local-data`
起了个自带的空 postgres)。立刻停下来查。
- **题目列表 `"total":0`** → 连错库了(`DATA_DIR` 指到了一个新初始化的空库)。
立刻停下来查。
- **库是对的,但判题全错、题面图片 404** → `DATA_DIR` 指错了。
命令测不到、必须手点的:登录 → 提交一道题看结果**实时刷出来**(这一步同时验证
@@ -164,58 +158,57 @@ WebSocket)→ 后台判题机列表在线 → 后台题目列表翻页 → 带
> **旧栈已经不可逆地下线**(`0002_drop_django_leftovers` 删掉了 Django 的框架表并已在
> 生产库执行完毕),所以「停新栈起旧栈」不再是退路,**唯一退路是从数据库备份恢复**。
## PostgreSQL 升级(16 → 18)与 Redis(7 → 8)
## 把库搬进 OJ2(一次性,顺带 PostgreSQL 16 → 18、Redis 7 → 8)
仓库里的 compose 已经是 `postgres:18-alpine` / `redis:8-alpine`,但**线上的库和服务器的
Redis 不归这几份 compose 管**,由 `/root/OJDeploy/docker-compose.yml` 起,得到服务器上
手工换。机房的 Redis 是 `compose.school.yml` 起的,部署时自己就换了。
`docker/move-db-into-oj2.sh` 把服务器上旧栈 compose 起的 postgres / redis 换成本栈的。
大版本不能原地换镜像(18 读不了 16 的目录),所以走「备份 → 新目录起 18 → 恢复」。
本机用 10-13 的生产备份搭了一套和线上一样的「之前」状态完整演练过:13 万条提交恢复
5 秒,行数一致,登录态保住,提交照常判题,机房那样从外面连 5445 新口令能进、旧口令被拒。
大版本不能原地换镜像:18 读不了 16 的数据目录。走「备份 → 新目录起 18 → 恢复」,
本机拿 2026-10-13 的生产备份完整演练过(13 万条提交,恢复 6 秒)。**两个站点共用这个库**,
所以要挑没课的时候,机房那套也一起停。
**挑没课的时候做**,两个站点共用这个库,中间两边都用不了(镜像在停机前编好,真正停机
的那段不到一分钟)。
```bash
# 1. 两边都停应用(机房那台上同样停 oj-api oj-worker)
# 0. 代码先推上去。CI 会照常 rsync,然后在 deploy.sh 自检停下(数据目录还不是 18)、
# 工作流判红 —— 这是预期的,容器一个没动,旧栈照常服务。
# 1. 机房:停应用
docker compose -f docker/compose.school.yml --env-file docker/.env.school stop oj-api oj-worker
# 2. 服务器:docker/.env 的 POSTGRES_PASSWORD 换成新生成的强口令(openssl rand -hex 32)
# 3. 服务器:搬家(CI 已经把产物 rsync 过来了,所以用 --prebuilt)
cd /root/OJDeploy/OJ2
docker compose -f docker/compose.debian.yml --env-file docker/.env stop oj-api oj-worker
docker exec oj-postgres psql -U onlinejudge -d onlinejudge -tAc 'select count(*) from submission' # 记下来
docker/move-db-into-oj2.sh --prebuilt
# 2. 全量备份(会打印文件路径)
docker/backup-db.sh
# 3. 停 16,改 /root/OJDeploy/docker-compose.yml 的 oj-postgres 和 oj-redis:
# oj-postgres image: postgres:18-alpine
# volumes: - ./data/postgresql:/var/lib/postgresql ← 新目录、挂载点少一级
# oj-redis image: redis:8-alpine ← 别的不动,口令那行留着
docker compose -f /root/OJDeploy/docker-compose.yml stop oj-postgres
docker compose -f /root/OJDeploy/docker-compose.yml up -d oj-postgres oj-redis
# 4. 恢复。只会有两条 ERROR:current user cannot be dropped / role already exists,正常
gzip -cd <备份文件> | docker exec -i oj-postgres psql -q -U onlinejudge -d postgres
# 5. 口令重新存一遍(输入 docker/.env 里的 POSTGRES_PASSWORD),再收集统计信息
docker exec -it oj-postgres psql -U onlinejudge -d postgres -c '\password onlinejudge'
docker exec oj-postgres vacuumdb -U onlinejudge --all --analyze-in-stages
docker exec oj-postgres psql -U onlinejudge -d onlinejudge -tAc 'select count(*) from submission' # 和第 1 步对上
# 6. 起应用(机房那台同样 up -d)
docker compose -f docker/compose.debian.yml --env-file docker/.env up -d oj-api oj-worker
# 4. 机房:docker/.env.school 的 POSTGRES_PASSWORD 改成同一个新值,然后(顺带换上 Redis 8)
docker compose -f docker/compose.school.yml --env-file docker/.env.school up -d --build
```
**第 5 步的改口令不能省。** 生产库的口令是 md5 哈希存的(老库从 Django 时代沿用下来),
dump 原样带过去;而 18 的镜像新建的库 `pg_hba.conf` 只认 `scram-sha-256`,md5 存的口令
**永远验不过**,报 `password authentication failed`。容器里 `docker exec psql` 走本地
socket 是 trust,一切正常,只有 api 和机房从网络连进来才炸 —— 演练时就是这么发现的。
`\password` 让 psql 在本地算好 SCRAM 哈希再发过去,明文不进服务端日志。
脚本每一步都有自检:机房没停(还有客户端连着旧库)会报出 IP 并中止,口令弱也中止,
恢复后行数对不上、新口令从网络登不上也中止。做完按「部署后验证」两边各点一遍。
**第 5 步的 `vacuumdb` 也别省**:dump 恢复不带统计信息,不跑的话查询计划全靠瞎猜,
排名、统计这类大查询会慢一阵,直到 autovacuum 自己补上。
之后:`/root/OJDeploy/docker-compose.yml` 里的 `oj-postgres` / `oj-redis` 删掉(留着的话
哪天有人 `up` 一下会和本栈抢端口);`docker/.env` 里的 `DB_HOST` / `DB_PORT` /
`REDIS_HOST` / `REDIS_PORT` 也删掉(`deploy.sh` 会提醒)。
**Redis 8 直接读 7 留下的 `dump.rdb`**(实测),会话和判题队列都在,学生不用重新登录。
**恢复时为什么剥掉 dump 里的口令**:旧库的口令是 md5 哈希存的,dump 原样带着;而 18 的
镜像新建的库 `pg_hba.conf` 只认 `scram-sha-256`,md5 存的口令**永远验不过**。容器里
`docker exec psql` 走本地 socket 是 trust,一切看着正常,只有 api 和机房从网络连进来才报
`password authentication failed` —— 演练时就是这么发现的。剥掉之后库里留的是初始化时按
`POSTGRES_PASSWORD` 存的 SCRAM 哈希。
**回退**:`./data/postgres` 那个 16 的老目录没动过。把 compose 改回 `postgres:16-alpine`
+ `./data/postgres:/var/lib/postgresql/data`,`up -d oj-postgres` 就回去了 —— 升级之后
写进 18 的数据不会跟着回去。确认 18 稳了再删老目录。
**中途失败**:停旧容器之前失败的,`docker start oj-api oj-worker` 就回去了。之后失败的,
旧库目录 `$DATA_DIR/postgres` 一个字节没动:
```bash
docker rm -f oj-postgres oj-redis # 本栈的 18 / 8
docker compose -f /root/OJDeploy/docker-compose.yml up -d oj-postgres oj-redis # 旧的 16 / 7
# docker/.env 的 POSTGRES_PASSWORD 改回旧口令、DB_HOST 那四个变量填回去,
# 代码退回搬家之前那个提交再 docker/deploy.sh
```
搬家之后写进 18 的数据回退时会丢。跑稳一两周再删 `$DATA_DIR/postgres`。
## 镜像体积