Files
OJ2/docker/deploy.sh
yuetsh 6356643cc9
Some checks failed
Deploy / deploy (push) Has been cancelled
ci(部署): 部署走 GitHub Actions,产物挪到 runner 上编
服务器性能差,镜像 builder 阶段(1238 个包的 bun install + bun compile +
vite build)首次约 5 分钟,光前端就 160s。改成在 runner 上编好 dist/oj2-api
和 apps/web/dist,rsync 过去,服务器那边只剩几条 COPY。

服务器自己编的能力没有砍掉。Dockerfile 顶部加了全局 ARG ARTIFACTS:

  build(默认)    builder 阶段自己编,只要有 docker 就能手动部署
  prebuilt         用构建上下文里现成的产物,builder 不进构建图

实现是两个 scratch 阶段归一成 /artifacts/ 布局,再用变量阶段名
FROM artifacts-${ARTIFACTS} 选一个,运行时阶段不关心产物是谁编的。

deploy.sh 加 --prebuilt:产物缺失在自检就 die;顺带检测 buildx —— 没装的话
compose 会退回 classic builder,那个不看依赖图、所有阶段挨个跑,产物白编
(结果正确但不省时间),黄字提醒不阻断。

.dockerignore 末尾放行两个产物路径,必须在 dist/ 那些规则之后,
dockerignore 是最后一条匹配说了算。

顺手删掉 apps/web/.github/ —— 从 ojnext 抄来的残留,路径不对(GitHub 只认
仓库根的 .github/workflows/)从来没跑过,内容也是 ojnext 的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 18:30:22 -06:00

165 lines
7.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 时用)
# docker/deploy.sh --prebuilt # 用现成产物不在这台机器上编CI 走这条)
#
# 默认(不带 --prebuilt是**服务器自己编**:只要有 docker 就能跑,不依赖 CI
# 也不依赖别处传产物过来。代价是这台机器性能差builder 阶段首次约 5 分钟。
#
# --prebuilt 则要求 dist/oj2-api 和 apps/web/dist 已经在这个目录里GitHub
# Actions 编好后 rsync 过来的),镜像构建只剩几条 COPY。产物缺失会在自检就拦下。
#
# 代码怎么上到服务器不归它管。没有 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 第三节)。
# `sh docker/deploy.sh` 会用 dash 跑Debian 的 /bin/sh 就是 dash而下面那行
# 的 pipefail 是 bash 专有的,一上来就报 `Illegal option -o pipefail`。
# 这行必须在 set 之前,且只能用 dash 也认的语法。
[ -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)
CHECK_ONLY=0
BUILD=1
for arg in "$@"; do
case "$arg" in
--check) CHECK_ONLY=1 ;;
--no-build) BUILD=0 ;;
# compose 里写的是 args: ARTIFACTS: ${ARTIFACTS:-build},导出即可生效
--prebuilt) export ARTIFACTS=prebuilt ;;
*) echo "未知参数:$arg(可用:--check、--no-build、--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; }
# ---------------------------------------------------------------- 自检
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.20depends_on.required 是那个版本才有的字段)"
ok "docker compose $ver"
# --prebuilt 时产物就是构建输入。缺了的话 docker 只会甩一句 COPY 找不到文件,
# 这里提前拦下并说清楚该怎么办。
if [ "${ARTIFACTS:-build}" = prebuilt ]; then
for f in dist/oj2-api apps/web/dist/index.html; do
[ -e "$f" ] || die "--prebuilt 需要现成产物,但 $f 不存在。
要么让 CI 把产物 rsync 过来(.github/workflows/deploy.yml
要么去掉 --prebuilt 让这台机器自己编(慢,但不依赖任何外部环节)。"
done
ok "用现成产物($(du -sh dist/oj2-api apps/web/dist 2>/dev/null | tr '\n' ' ')"
# 没有 buildx 时 compose 会退回 classic builder构建日志末尾那行
# `LABEL com.docker.compose.image.builder=classic` 就是它。classic **不看
# 依赖图,所有阶段挨个跑**,于是 builder 照样 bun install + vite build 一遍,
# 产物白编了 —— 结果是对的,只是一分钟没省下来。
docker buildx version >/dev/null 2>&1 \
|| warn "没装 buildxcompose 会退回老 builder它不跳过用不到的阶段
—— 这次 --prebuilt 省不下时间(结果仍然正确)。
apt-get install docker-buildx-plugin"
else
ok "产物在镜像里现编(首次约 5 分钟CI 部署走的是 --prebuilt"
fi
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