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 镜像等实测现象与回显。

Judge0Judge0代码评测REST APIRubyPostgreSQL2026/10/27

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 Engine29.9.2
Docker Composev5.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 的三处差异:

  1. db 的数据卷由命名卷 data 改为绑定 ./data,便于直接备份与迁移,见第五章。
  2. 显式声明网络名与子网,避免与同机其他栈撞段。
  3. 两个应用服务都挂载了 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=trueInternal Error(server 仍用镜像内原版)
不带 waitAccepted(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_HOSTredisserver / workers服务名解析
REDIS_PASSWORD自定server / workers / redis 启动命令通过 $$REDIS_PASSWORD 传入 --requirepass
POSTGRES_HOSTdbserver / workers—
POSTGRES_DBjudge0server / workers / db 初始化—
POSTGRES_USERjudge0server / 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.confro应用配置;服务入口 scripts/load-config 优先读 /api/judge0.conf,其次 /judge0.conf
./isolate_job.rb/api/app/jobs/isolate_job.rbrocgroup v2 降级补丁,server 与 workers 都要挂
./data/var/lib/postgresql/datarw数据库持久化

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 不会再次访问远端。

十二、参考链接