Judge0 1.13.1 Docker Compose 部署指南
Judge0 1.13.1 单机 Compose 部署实录:name: judge0-1.13.1-stack 四服务栈(server/workers/PostgreSQL 16.2/Redis 7.2.4)、privileged 与 cgroup v2 要求、8138 端口与 10.89.60.0/24 网络槽、isolate 1.8.1 在 cgroup2 下的 Internal Error 根因与 isolate_job.rb 补丁必须同时挂 server 与 workers、项目名规范化、子网重叠、离线导入 14.2GB 镜像等实测现象与回显。
Judge0 1.13.1 Docker Compose 部署指南
目标:用 Docker Compose 在单机拉起 Judge0 1.13.1 的四服务栈(server + workers + PostgreSQL 16.2 + Redis 7.2.4),跑通同步与异步提交,并如实记录 cgroup v2 主机上 isolate 1.8.1 的失败现象、
isolate_job.rb补丁必须同时挂到两个服务的实测结论,以及项目名规范化、镜像拉取、子网冲突等部署偏差点。
一、环境信息
1.1 主机与运行时
| 项 | 实测值 |
|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS,内核 7.0.0-38-generic |
| cgroup 形态 | cgroup2fs(统一层级,v2) |
| Docker Engine | 29.9.2 |
| Docker Compose | v5.6.1 |
| 工作目录 | /opt/data/quzuo/tmp/judge0/compose |
| 镜像 | judge0/judge0:1.13.1(docker images 显示 14.2GB) |
1.2 栈的编排结果
| 项 | 值 |
|---|---|
| compose 项目名声明 | judge0-1.13.1-stack |
| 实际项目名(标签值) | judge0-1131-stack |
| API 端口 | 8138 → 2358 |
| 自定义网络 | judge0-1.13.1-net,子网 10.89.60.0/24 |
| 容器数 | 4(server / workers / db / redis) |
| 数据库 | PostgreSQL 16.2,数据目录绑定 ./data |
| 队列 | Redis 7.2.4,--appendonly no --requirepass |
容器清单实测:
$ docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
judge0-1131-stack-db-1 postgres:16.2 "docker-entrypoint.s…" db Up 17 minutes 5432/tcp
judge0-1131-stack-redis-1 redis:7.2.4 "docker-entrypoint.s…" redis Up 17 minutes 6379/tcp
judge0-1131-stack-server-1 judge0/judge0:1.13.1 "/api/docker-entrypo…" server Up 16 minutes 0.0.0.0:8138->2358/tcp, [::]:8138->2358/tcp
judge0-1131-stack-workers-1 judge0/judge0:1.13.1 "/api/docker-entrypo…" workers Up 17 minutes 2358/tcp
注意第一行:compose 文件里写的是 name: judge0-1.13.1-stack,但容器名前缀与项目标签都是 judge0-1131-stack——Compose 会规范化项目名,把 . 等字符去掉。这个差异会直接影响 docker compose -p、日志检索与脚本里的名字拼接,见 11.2。
二、镜像选型说明
2.1 为什么固定到 1.13.1
judge0/judge0:latest 与具体 Tag 的行为差异集中在语言清单与 isolate 版本上,升级会同时改动 languages 种子与 isolate_job.rb 的命令行拼装。生产环境应固定 Tag,并在升级前按本文的验证步骤回归一次提交。
镜像内部结构(进入容器可见):
$ docker compose exec server ls /api
CHANGELOG.md Gemfile Gemfile.lock Rakefile app bin config config.ru
db docker-entrypoint.sh judge0.conf lib log public scripts tmp vendor
应用根是 /api,judge0.conf 被复制到 /api/judge0.conf 并在启动时被 scripts/load-config source。isolate_job.rb 位于 /api/app/jobs/,本文的适配补丁就打在它上面。
2.2 镜像拉取的现实问题
直连 Docker Hub 在本环境多次超时,docker pull judge0/judge0:1.13.1 挂在 Pulling fs layer 阶段数十分钟无进展。可用的替代路径是先用 crane 走代理把镜像导出为 tar,再 docker load:
HTTPS_PROXY=http://192.168.1.62:7897 ./crane pull judge0/judge0:1.13.1 judge0-1.13.1.tar --platform linux/amd64
docker load -i judge0-1.13.1.tar
Loaded image: judge0/judge0:1.13.1
$ docker images judge0/judge0:1.13.1
REPOSITORY TAG IMAGE ID CREATED SIZE
judge0/judge0 1.13.1 ... 14.2GB
镜像解压后 14.2GB,主要来自预置的几十种语言运行时(gcc、clang、JDK、Node、Ruby、PHP、.NET 等)。磁盘规划至少留 40GB 给镜像层、容器可写层与数据库。
三、前置条件
3.1 Docker 与 Compose
$ docker version --format '{{.Server.Version}}'
29.9.2
$ docker compose version
Docker Compose version v5.6.1
Compose v2 的 name: 顶层字段需要 docker compose(插件形式),老式 docker-compose 二进制不识别该字段,会退化成目录名作为项目名。
3.2 cgroup 与特权模式
Judge0 的每个提交都由 isolate 创建独立命名空间与 cgroup,因此 server 与 workers 两个服务都必须 privileged: true。去掉特权能力后的实测报错:
$ docker run --rm --cap-drop=ALL --entrypoint isolate judge0/judge0:1.13.1 --cg -b 0 --init
Cannot switch to root group: Operation not permitted
isolate 需要切换到内部 boxN 用户与组,缺失能力时在第一步就失败。
同时要确认宿主的 cgroup 形态,这决定第五章补丁是否必须:
$ stat -fc %T /sys/fs/cgroup
cgroup2fs
3.3 端口与网络槽
API 对外端口选 8138,落入本文约定的 8128–8152 段;自定义网络子网选 10.89.60.0/24,位于 10.89.56–75 槽内。选之前必须确认没有同网段网络已存在,否则 up 阶段直接失败(见 11.3)。
$ docker network ls --format '{{.Name}} {{.Driver}}'
bridge bridge
dittofeed-v0.24.0-alpha.21-net bridge
hoppscotch-v2026-9-stack_hoppscotch-net bridge
judge0-1.13.1-net bridge
kibitzr-8.0.0-net bridge
laminar-1.4-net bridge
四、部署步骤
4.1 准备目录与配置文件
mkdir -p /opt/data/quzuo/tmp/judge0/compose
cd /opt/data/quzuo/tmp/judge0/compose
judge0.conf 从官方仓库复制一份,改动三组值:数据库主机名、Redis 主机名、两个密码。容器内主机名由服务名解析,因此写 db 与 redis:
REDIS_HOST=redis
REDIS_PORT=
REDIS_PASSWORD=judge0_redis_pwd_2026
POSTGRES_HOST=db
POSTGRES_PORT=
POSTGRES_DB=judge0
POSTGRES_USER=judge0
POSTGRES_PASSWORD=judge0_pg_pwd_2026
与裸机部署不同,REDIS_PORT、POSTGRES_PORT、RAILS_MAX_THREADS、INTERVAL 这些项可以留空:容器启动时 scripts/load-config 会 source 配置并补齐默认值(INTERVAL=0.1、RAILS_MAX_THREADS=$(nproc) 等)。这一点是容器路径和裸机路径最容易混淆的地方。
db 与 redis 两个服务用 env_file: judge0.conf 直接读取同一份文件,因此 POSTGRES_DB/POSTGRES_USER/POSTGRES_PASSWORD 与 REDIS_PASSWORD 必须与 server/workers 完全一致,否则会出现「API 能连接但 worker 连不上」的半可用状态。
4.2 compose 文件
name: judge0-1.13.1-stack
x-logging: &default-logging
logging:
driver: json-file
options:
max-size: 100M
services:
server:
image: judge0/judge0:1.13.1
volumes:
- ./judge0.conf:/judge0.conf:ro
- ./isolate_job.rb:/api/app/jobs/isolate_job.rb:ro
ports:
- "8138:2358"
privileged: true
<<: *default-logging
restart: always
workers:
image: judge0/judge0:1.13.1
command: ["./scripts/workers"]
volumes:
- ./judge0.conf:/judge0.conf:ro
- ./isolate_job.rb:/api/app/jobs/isolate_job.rb:ro
privileged: true
<<: *default-logging
restart: always
db:
image: postgres:16.2
env_file: judge0.conf
volumes:
- ./data:/var/lib/postgresql/data
<<: *default-logging
restart: always
redis:
image: redis:7.2.4
command: [
"bash", "-c",
'docker-entrypoint.sh --appendonly no --requirepass "$$REDIS_PASSWORD"'
]
env_file: judge0.conf
<<: *default-logging
restart: always
networks:
default:
name: judge0-1.13.1-net
ipam:
config:
- subnet: 10.89.60.0/24
与官方 compose 的三处差异:
db的数据卷由命名卷data改为绑定./data,便于直接备份与迁移,见第五章。- 显式声明网络名与子网,避免与同机其他栈撞段。
- 两个应用服务都挂载了
isolate_job.rb补丁,原因见第八章。
4.3 启动
docker compose up -d
$ docker compose ps --format '{{.Name}} {{.Status}}'
judge0-1131-stack-db-1 Up 17 minutes
judge0-1131-stack-redis-1 Up 17 minutes
judge0-1131-stack-server-1 Up 16 minutes
judge0-1131-stack-workers-1 Up 17 minutes
server 的入口命令会在启动时执行 rails db:create db:migrate db:seed,首次启动比另外三个服务慢 10–20 秒,这段时间 /about 会返回 502/连接拒绝,属正常。
4.4 首次验证
$ curl -s http://127.0.0.1:8138/about
{"version":"1.13.1","homepage":"https://judge0.com","source_code":"https://github.com/judge0/judge0","maintainer":"Herman Zvonimir Došilović \u003chermanz.dosilovic@gmail.com\u003e"}
homepage / source_code / maintainer 有值,说明容器内已预置 JUDGE0_* 环境变量;裸机部署时这几个字段为 null。
$ curl -s http://127.0.0.1:8138/languages | python3 -c "import sys,json;print(len(json.load(sys.stdin)))"
47
此时不要急着认为装好了:提交大概率会返回 Internal Error,见第五章。
五、cgroup v2 适配与 isolate_job.rb 补丁
5.1 失败现象
$ curl -s -X POST "http://127.0.0.1:8138/submissions?wait=true" \
-H "Content-Type: application/json" \
-d '{"source_code":"print(\"hello judge0\")","language_id":71}' | python3 -m json.tool
{
"stdout": null,
"time": null,
"memory": null,
"token": "...",
"message": "No such file or directory @ rb_sysopen - /box/script.py",
"status": {
"id": 13,
"description": "Internal Error"
}
}
5.2 根因链
镜像内 isolate --version 为 1.8.1。在 cgroup2fs 主机上直接执行:
$ docker compose exec server isolate --cg -b 6 --init
Failed to create control group /sys/fs/cgroup/memory/box-6/: No such file or directory
isolate 1.8.1 只认 cgroup v1 的 memory 控制器目录,v2 统一层级下没有 /sys/fs/cgroup/memory/,--init 返回非零且无输出。Judge0 的 initialize_workdir 用反引号取 stdout 当作工作目录:
@workdir = `isolate #{cgroups} -b #{box_id} --init`.chomp
失败时 @workdir 为空字符串,后续 boxdir 退化成 /box,于是写源码时报 rb_sysopen - /box/script.py。这就是「Internal Error 消息里路径缺 /var/local/lib/isolate/<id> 前缀」的由来。
5.3 补丁内容
把容器内的 app/jobs/isolate_job.rb 复制出来,改一行:
docker compose cp server:/api/app/jobs/isolate_job.rb ./isolate_job.rb
# 原:
@cgroups = (!submission.enable_per_process_and_thread_time_limit || !submission.enable_per_process_and_thread_memory_limit) ? "--cg" : ""
# 改:
@cgroups = "" # cgroup v2 host: isolate 1.8.1 has no cgroup v2 support, run without --cg
去掉 --cg 后 isolate 退化为「命名空间 + rlimit」隔离,不再需要 cgroup,提交可以正常执行;代价是内存统计改取 max-rss、MAX_PROCESSES_AND_OR_THREADS 不再由 cgroup 强制约束、CPU 计时为进程级。这条补丁是降级适配,不是等价替换。
5.4 补丁必须挂到两个服务
这是本文最容易被忽略的一点。wait=true 的同步提交由 API 进程(server)内联执行 IsolateJob,不经过队列;只有异步提交才由 workers 消费。实测只把补丁挂到 workers 时:
| 提交方式 | 结果 |
|---|---|
?wait=true | Internal Error(server 仍用镜像内原版) |
不带 wait | Accepted(workers 用了补丁) |
因此 server 与 workers 两个服务都需要挂载同一个文件,挂载后重启:
docker compose restart server workers
$ curl -s -X POST "http://127.0.0.1:8138/submissions?base64_encoded=false&wait=true" \
-H "Content-Type: application/json" \
-d '{"source_code":"print(\"hello judge0\")","language_id":71}' | python3 -m json.tool
{
"stdout": "hello judge0\n",
"time": "0.019",
"memory": 7944,
"stderr": null,
"token": "4a7b4730-bbae-4ea1-8540-a139117135f7",
"compile_output": null,
"message": null,
"status": {
"id": 3,
"description": "Accepted"
}
}
异步路径的验证(不带 wait,随后轮询):
$ T=$(curl -s -X POST "http://127.0.0.1:8138/submissions" -H "Content-Type: application/json" \
-d '{"source_code":"print(6*7)","language_id":71}' | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
$ curl -s "http://127.0.0.1:8138/submissions/$T"
{"stdout":"42\n","time":"0.011","memory":7728,"stderr":null,"token":"b3b1972e-5a5f-4240-979b-24f7b4ddcdb1","compile_output":null,"message":null,"status":{"id":3,"description":"Accepted"}}
六、数据持久化说明
| 数据 | 位置 | 生命周期 |
|---|---|---|
| PostgreSQL 数据 | 宿主 ./data(容器内 /var/lib/postgresql/data) | 绑定目录,down 不删;rm -rf ./data 才清空 |
| Redis 数据 | 容器内 /data | --appendonly no,重启即丢;队列为空不影响已落库的提交 |
| 提交记录 | PostgreSQL submissions 表 | 与数据库同寿命 |
| 隔离工作目录 | 容器内 /var/local/lib/isolate/<box> | 每次提交 --init/--cleanup 创建与回收 |
| 配置文件 | 宿主 ./judge0.conf、./isolate_job.rb | 只读挂载,改完需重启对应服务 |
db 首次启动时,./data 目录会由 PostgreSQL 初始化;如果目录里已有旧集群数据,PostgreSQL 会跳过初始化并直接挂载,导致密码与 compose 中的 POSTGRES_PASSWORD 不一致。更换密码时必须进库 ALTER USER 或清空 ./data 重新初始化。
七、环境变量表
judge0.conf 同时承担三种角色:应用服务的配置源、db 服务的初始化参数、redis 服务的启动参数。关键项:
| 项 | 本文值 | 被谁读取 | 说明 |
|---|---|---|---|
REDIS_HOST | redis | server / workers | 服务名解析 |
REDIS_PASSWORD | 自定 | server / workers / redis 启动命令 | 通过 $$REDIS_PASSWORD 传入 --requirepass |
POSTGRES_HOST | db | server / workers | — |
POSTGRES_DB | judge0 | server / workers / db 初始化 | — |
POSTGRES_USER | judge0 | server / workers / db 初始化 | — |
POSTGRES_PASSWORD | 自定 | server / workers / db 初始化 | 三处必须一致 |
RAILS_ENV | 空(默认 production) | server / workers | 空值由 load-config 补 production |
INTERVAL | 空(默认 0.1) | workers | 空值由 load-config 补 0.1 |
COUNT | 空(默认 2*nproc) | workers | 决定并行消费者数量 |
MAX_QUEUE_SIZE | 空(默认 100) | server | 队列满时拒绝新提交 |
ENABLE_WAIT_RESULT | 空(默认 true) | server | 关闭后 wait=true 会返回 400 |
八、配置文件挂载
| 挂载 | 目标 | 模式 | 作用 |
|---|---|---|---|
./judge0.conf | /judge0.conf | ro | 应用配置;服务入口 scripts/load-config 优先读 /api/judge0.conf,其次 /judge0.conf |
./isolate_job.rb | /api/app/jobs/isolate_job.rb | ro | cgroup v2 降级补丁,server 与 workers 都要挂 |
./data | /var/lib/postgresql/data | rw | 数据库持久化 |
isolate_job.rb 是 Ruby 源码,改动后必须重启对应服务进程(docker compose restart server workers);只重启 server 会让异步提交继续失败,排查时容易误判为补丁无效。
九、等价 docker run
不用 Compose 时,最小可运行组合如下(网络与数据卷需先建):
docker network create judge0-1.13.1-net --subnet 10.89.60.0/24
docker run -d --name judge0-1131-db --network judge0-1.13.1-net \
--env-file judge0.conf -v "$PWD/data:/var/lib/postgresql/data" postgres:16.2
docker run -d --name judge0-1131-redis --network judge0-1.13.1-net \
--env-file judge0.conf redis:7.2.4 \
bash -c 'docker-entrypoint.sh --appendonly no --requirepass "$REDIS_PASSWORD"'
docker run -d --name judge0-1131-server --network judge0-1.13.1-net \
--privileged -p 8138:2358 \
-v "$PWD/judge0.conf:/judge0.conf:ro" \
-v "$PWD/isolate_job.rb:/api/app/jobs/isolate_job.rb:ro" \
judge0/judge0:1.13.1
docker run -d --name judge0-1131-workers --network judge0-1.13.1-net \
--privileged \
-v "$PWD/judge0.conf:/judge0.conf:ro" \
-v "$PWD/isolate_job.rb:/api/app/jobs/isolate_job.rb:ro" \
judge0/judge0:1.13.1 ./scripts/workers
注意 docker run 路径下必须自己保证四个容器的启动顺序:db/redis 先就绪,server 再启动(它会执行迁移),workers 最后。
十、常用运维命令
docker compose ps # 状态总览
docker compose logs -f server # 跟踪 API 日志
docker compose logs --tail=100 workers
docker compose exec server isolate --version
docker compose exec db psql -U judge0 -d judge0 -c "select count(*) from submissions;"
docker compose exec redis redis-cli -a "$REDIS_PASSWORD" llen 1.13.1
docker compose restart server workers
docker compose down # 停止并删除容器/网络,保留 ./data
docker compose down -v # 连命名卷一起删(本文用绑定目录,等价于保留数据)
队列积压查询依赖 JUDGE0_VERSION 作为队列名(镜像内预置 1.13.1):
$ docker compose exec -T redis redis-cli -a judge0_redis_pwd_2026 llen 1.13.1
0
$ docker compose exec -T redis redis-cli -a judge0_redis_pwd_2026 keys 'resque:worker:*' | head -3
resque:worker:8ccd316a2cb4:62:1.13.1:started
resque:worker:abaf0b6f6ed0:60:1.13.1:started
resque:worker:abaf0b6f6ed0:66:1.13.1:started
十一、常见问题
11.1 提交返回 Internal Error,消息里是 /box/script.py
{"message":"No such file or directory @ rb_sysopen - /box/script.py","status":{"id":13}}
宿主机是 cgroup v2,镜像内 isolate 1.8.1 的 --cg --init 失败,工作目录为空。按第五章打补丁,并把补丁同时挂到 server 与 workers。
11.2 项目名与容器名前缀不是 compose 里的名字
compose 文件声明 name: judge0-1.13.1-stack,实际项目名是 judge0-1131-stack:
$ docker inspect -f '{{index .Config.Labels "com.docker.compose.project"}}' judge0-1131-stack-server-1
judge0-1131-stack
Compose 会对项目名做规范化(只保留小写字母、数字与 -/_),1.13.1 中间的点被删掉。编写监控、日志采集或 docker compose -p 命令时应以 docker compose ps 显示的名称为准,不要用声明值拼容器名。
11.3 up 报地址池重叠
Error response from daemon: invalid pool request: Pool overlaps with other one on this address space
同机已有网络占用了目标子网。逐个检查后换一个槽位:
$ docker network inspect judge0-1.13.1-net -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
10.89.60.0/24
11.4 去掉 privileged 后 isolate 直接失败
Cannot switch to root group: Operation not permitted
isolate 要切换到 boxN 用户/组并创建挂载命名空间,需要完整能力集。server 与 workers 都必须 privileged: true。
11.5 更换数据库密码后 API 起不来
./data 里的旧集群仍使用旧密码,PostgreSQL 不会因 POSTGRES_PASSWORD 变化而改密。两种处理:进容器用旧密码执行 ALTER USER judge0 WITH PASSWORD '...',或停栈后清空 ./data 重新初始化(会丢历史提交)。
11.6 镜像拉取长时间停在 Pulling fs layer
见 2.2,改用离线导入路径。导入后 docker compose up -d 不会再次访问远端。
十二、参考链接
- Judge0 源码与 compose 原文:https://github.com/judge0/judge0/blob/master/docker-compose.yml
- Judge0 1.13.1 Release:https://github.com/judge0/judge0/releases/tag/v1.13.1
- isolate 源码仓库:https://github.com/ioi/isolate
- Compose 项目名规范化说明:https://docs.docker.com/compose/project-name/
- 站内:《Judge0 1.13.1 介绍与选型指南》
- 站内:《Ubuntu 22.04 安装 Judge0 1.13.1 指南》
- 站内:《Judge0 1.13.1 配置参数详解》
- 站内:《Judge0 1.13.1 REST API 使用详解》