# 服务器(oj.xuyue.cc)。数据库在这里,机房那套连的就是这里的 5445。 # # docker/deploy.sh # 平时走这个(自检 → 构建 → 迁移 → 起栈 → 冒烟) # # 构建上下文是仓库根,所以 build.context 写 ..(相对本文件)。 # # **服务器上的全部容器都由这一个文件起**,包括 postgres 和 redis。以前库和 redis 是旧栈 # 那份 /root/OJDeploy/docker-compose.yml 起的、这里通过 DB_HOST 外接过去,2026-10 起收了进来。 # # ⚠️ `DATA_DIR` 默认值 `../data` 解析出来是 **`OJ2/data`**,不是部署目录的 `data/`。 # 不设它的话 postgres 会在空目录上初始化一个全新的空库,测试点和题面图片也全都不在, # 而且**不报错**。`deploy.sh` 为此专门设了自检。 services: oj-postgres: image: postgres:18-alpine container_name: oj-postgres restart: always # 18 起镜像的数据目录是 /var/lib/postgresql/18/docker,挂载点要是上一级, # 还挂 .../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: - "5445:5432" # 这台机器 2 核 7.5G、没有 swap,和 gitea、judge0 等二十多个别的服务挤在一起(2026-10 实测 # 可用 3.5G)。所以这里的取舍是「别被挤死」而不是「吃满内存」: # - 全库 249MB,shared_buffers 512MB 能整库常驻,work_mem 16MB 让统计页的聚合排序不落盘; # - oom_score_adj 压低:内存真满了,内核先杀别的,不先杀库; # - cpu_shares 加倍:上课一个班同时交题时,查库和网页比判题、比别的服务先拿到 CPU。 # 这是按比例分、不是封顶,闲的时候判题照样用满两个核; # - 两个超时防机房断网:事务开着不动超过 60 秒就断,锁不会一直挂着; # TCP 保活把断掉的远端连接从默认两小时缩到两分钟左右才发现。 command: - postgres - -c - shared_buffers=512MB - -c - effective_cache_size=2GB - -c - work_mem=16MB - -c - maintenance_work_mem=128MB - -c - idle_in_transaction_session_timeout=60s - -c - tcp_keepalives_idle=60 - -c - tcp_keepalives_interval=10 - -c - tcp_keepalives_count=6 # 并行查询的工作区走 /dev/shm,docker 默认只给 64MB,大一点的并行哈希会直接报错 shm_size: 256m oom_score_adj: -500 cpu_shares: 2048 healthcheck: test: ["CMD-SHELL", "pg_isready -U onlinejudge -d onlinejudge"] interval: 5s timeout: 3s retries: 10 oj-redis: image: redis:8-alpine container_name: oj-redis restart: always # 会话存在这里:谁能连上它,谁就能写一条 session: 冒充超管。所以必须带密码, # 端口也只发布在本机(api / worker 走 compose 网络连 oj-redis,用不着宿主机端口)。 command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?}"] environment: # redis-cli 认这个变量,healthcheck 和 docker exec 进来的 redis-cli 都不用再带 -a REDISCLI_AUTH: ${REDIS_PASSWORD:?} volumes: - ${DATA_DIR:-../data}/redis:/data ports: - "127.0.0.1:5446:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 oj-judge: # 自己构建的判题沙箱(上游 JudgeServer 1.6.1 + 新工具链,见 docker/judge/)。 # 上游停更在 2024-04,registry 上的 1.6.1 == latest,没有新版可拉。 # ⚠️ 这个 tag 不在任何 registry 上:本机 docker/judge/build.sh 构建, # 服务器/机房用 docker load 装。镜像不在本地时 compose 会去 pull 然后报找不到。 image: oj2-judge-2 container_name: oj-judge restart: always read_only: true cap_drop: - SETPCAP - MKNOD - NET_BIND_SERVICE - SYS_CHROOT - SETFCAP - FSETID tmpfs: - /tmp volumes: - ${DATA_DIR:-../data}/backend/test_case:/test_case:ro # 判题机的运行状态目录,默认跟着 DATA_DIR 走 - ${JUDGE_STATE_DIR:-${DATA_DIR:-../data}/judge_server}/log:/log - ${JUDGE_STATE_DIR:-${DATA_DIR:-../data}/judge_server}/run:/judger environment: SERVICE_URL: http://oj-judge:8080 # 心跳路径跟着新后端改了:judge_server_heartbeat/ → judge-server/heartbeat BACKEND_URL: http://oj-api:3000/api/judge-server/heartbeat TOKEN: ${OJ2_JUDGE_TOKEN:?请在 docker/.env 里设置 OJ2_JUDGE_TOKEN} # 同一时刻最多六份在编译(正式判题 2、试运行 2、提交前编译检查 2),g++ 编一份 iostream # 峰值约 70MB、bits/stdc++.h 约 195MB,再加学生程序本身。512m 不够撑满,超了是容器里 # 随机杀进程、学生看到系统错误 mem_limit: 1g oj-api: build: &build context: .. dockerfile: docker/Dockerfile target: api # 产物来源,见 docker/Dockerfile 顶部。默认 build(服务器自己编); # CI 传 ARTIFACTS=prebuilt 用 runner 编好的产物。 args: ARTIFACTS: ${ARTIFACTS:-build} image: oj2-api:latest container_name: oj-api restart: always depends_on: oj-postgres: condition: service_healthy oj-redis: condition: service_healthy volumes: - ${DATA_DIR:-../data}/backend:/data environment: &api-env 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}@oj-redis:6379 JUDGE_SERVER_URL: http://oj-judge:8080 JUDGE_SERVER_TOKEN: ${OJ2_JUDGE_TOKEN:?} JUDGE_CONCURRENCY: ${JUDGE_CONCURRENCY:-2} TRIAL_CONCURRENCY: ${TRIAL_CONCURRENCY:-2} AI_KEY: ${AI_KEY:-} AI_HINT_DIAGNOSE: ${AI_HINT_DIAGNOSE:-} healthcheck: test: ["CMD", "oj2-api", "healthcheck"] interval: 30s timeout: 3s retries: 3 start_period: 10s mem_limit: 512m # 理由同 oj-postgres:上课时学生翻页、看结果要先于判题拿到 CPU cpu_shares: 2048 oj-worker: build: *build image: oj2-api:latest container_name: oj-worker restart: always depends_on: - oj-api volumes: - ${DATA_DIR:-../data}/backend:/data environment: *api-env # 同一个镜像,换个子命令就是判题消费者 command: ["oj2-api", "worker"] # SQL 判题的子进程各允许 512MB 数据段(judge/sql 的 ulimit -d),两个并发就能顶满 512m mem_limit: 1g oj-web: build: context: .. dockerfile: docker/Dockerfile target: web args: ARTIFACTS: ${ARTIFACTS:-build} image: oj2-web:latest container_name: oj-web restart: always depends_on: - oj-api volumes: # Caddy 只往这里写访问日志,静态资源在镜像里 - ${DATA_DIR:-../data}/backend/log:/data/log ports: - "0.0.0.0:${WEB_PORT:-8080}:8000" mem_limit: 256m