Cronmaster 2.4.1 Docker Compose 部署指南

部署篇:用 Docker Compose 部署 Cronmaster 2.4.1,含 name: cronmaster-2.4.1-stack 完整编排、root/host PID/privileged 与 Docker Socket 权限模型、卷与端口规划、Nginx SSE 反代、登录建任务与执行日志实测,以及镜像体积 406MB、GHCR 拉取超时等五项排障实录与升级回滚方案。

Cronmastercron定时调度任务管理2026/10/27

Cronmaster 2.4.1 Docker Compose 部署指南

目标:用 Docker Compose 在一台 Linux 宿主机上部署 Cronmaster 2.4.1,把宿主机 crontab 交给浏览器管理,并验证登录、建任务、执行日志三条链路。本篇的 compose 文件、镜像大小、任务 ID、日志内容均来自测试机真实执行结果。

本篇是唯一允许出现 Docker 相关术语的篇幅;如果希望不引入 Docker、直接用 systemd 托管,请看本专栏《Ubuntu 22.04 安装 Cronmaster 2.4.1 指南》。

一、为什么 Cronmaster 必须给足权限

Cronmaster 的 Docker 形态不是「随便起个 Web 容器」那么简单。它要代表宿主机操作 crontab,因此官方硬性要求三件事,缺一不可:

配置为什么必须
user: "root"读写 /var/spool/cron/crontabs/<user> 需要 root 权限,普通用户无法保存任务
pid: "host"「立即执行」要通过 nsenter 进入宿主机命名空间,共享 PID 命名空间是前提
privileged: true配合 nsenter 与宿主挂载读取(磁盘/inode 统计走 /proc/1/root)

再加一条:挂载 Docker Socket(/var/run/docker.sock)。启用执行日志时,应用要靠它 docker inspect 反查 /app/data 在宿主机上的真实路径,才能把正确的包装脚本路径写进 crontab。

换句话说,这个容器实质上拿到了宿主机 root 能力。只应部署在可信内网,并配合反向代理 + HTTPS + 强密码/SSO。

二、测试环境

项目实测值
镜像ghcr.io/fccview/cronmaster:2.4.1
镜像体积406 MB(docker images 口径)
宿主机端口8131 → 容器 3000
数据目录/opt/data/quzuo/tmp/cronmaster/b2/{scripts,data,snippets}
容器名cronmaster-2.4.1-b2
网络cronmaster-2.4.1-net-b2,10.89.64.0/24
登录密码q1w2e3r4

三、compose 文件

创建 /opt/cronmaster/docker-compose.yml(本次实验路径为 /opt/data/quzuo/tmp/cronmaster/b2/docker-compose.yml):

name: cronmaster-2.4.1-stack

services:
  cronmaster:
    image: ghcr.io/fccview/cronmaster:2.4.1
    container_name: cronmaster-2.4.1-b2
    user: "root"
    ports:
      - "8131:3000"
    environment:
      NODE_ENV: production
      AUTH_PASSWORD: q1w2e3r4
      HOST_CRONTAB_USER: root
      LOCALE: zh
      API_KEY: cronmaster-api-key-2026
      HOST_DATA_DIR: /opt/data/quzuo/tmp/cronmaster/b2/data
      HOST_SCRIPTS_DIR: /opt/data/quzuo/tmp/cronmaster/b2/scripts
      MAX_LOG_AGE_DAYS: 30
      MAX_LOGS_PER_JOB: 50
      DISABLE_SYSTEM_STATS: "false"
      DISABLE_DISK_STATS: "false"
      LIVE_UPDATES: "true"
      SESSION_MAX_AGE_DAYS: 30
      TZ: Asia/Shanghai
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /opt/data/quzuo/tmp/cronmaster/b2/scripts:/app/scripts
      - /opt/data/quzuo/tmp/cronmaster/b2/data:/app/data
      - /opt/data/quzuo/tmp/cronmaster/b2/snippets:/app/snippets
    pid: "host"
    privileged: true
    restart: unless-stopped
    init: true
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      start_period: 30s
      retries: 3
    networks:
      - cronmaster-net

networks:
  cronmaster-net:
    name: cronmaster-2.4.1-net-b2
    ipam:
      config:
        - subnet: 10.89.64.0/24

3.1 逐项说明

  • name: cronmaster-2.4.1-stack:顶层项目名,决定 docker compose ps 里的 PROJECT 列与默认网络前缀。显式写死可以避免因目录改名导致网络/容器名漂移。
  • image 固定小版本:用 2.4.1,不要用 latest。2.4.1 是安全补丁版本,固定 tag 便于回滚与审计。
  • 端口:容器内固定 3000,宿主机映射到 8131。宿主机端口冲突是本篇踩到的第一个坑(详见第六节)。
  • HOST_DATA_DIR / HOST_SCRIPTS_DIR:宿主机绝对路径,必须与 volumes 左侧完全一致。这两个变量明确给出后,应用不再依赖 docker inspect 反查,日志包装脚本的路径更可靠。建议生产环境一律显式设置。
  • API_KEY:只保护 /api/*,不保护 Web 界面。界面保护靠 AUTH_PASSWORD 或 SSO。
  • TZ:日志文件名按宿主机本地时间生成。容器与宿主机时区不一致会让日志「年龄」算错,触达 MAX_LOG_AGE_DAYS 边界时可能误删。固定 Asia/Shanghai 对齐。
  • init: true:注入 tini 回收僵尸进程。cron 任务会 fork 子进程,没有 init 容易堆积。
  • networks + 显式 subnet:避免与宿主机已有网段冲突。起网络前应先 docker network ls 与 ip route 确认 10.89.64.0/24 未被占用。

四、启动与验证

4.1 拉取与启动

docker compose up -d
docker compose ps

回显:

NAME                  IMAGE                              SERVICE      STATUS                    PORTS
cronmaster-2.4.1-b2   ghcr.io/fccview/cronmaster:2.4.1   cronmaster   Up (healthy)              0.0.0.0:8131->3000/tcp

健康检查通过后,看启动自检日志:

docker logs cronmaster-2.4.1-b2 --tail 20

关键行:

Cr*nMaster starting {
  nodeEnv: 'production',
  locale: 'zh',
  localLogin: true,
  ssoMode: 'none',
  bearerAuth: true,
  https: false,
  hostCrontabUser: 'root',
  liveUpdates: true,
  systemStats: true,
  maxLogsPerJob: '50',
  maxLogAgeDays: '30'
}

bearerAuth: true 说明 API_KEY 生效,locale: 'zh' 说明中文界面已启用。

4.2 登录

curl -s -X POST http://127.0.0.1:8131/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"password":"q1w2e3r4"}'

回显:

{"success":true,"message":"Login successful"}

返回的 Set-Cookie 中 cronmaster-session 为 HttpOnly 会话,默认有效期 30 天(Max-Age=2592000)。

4.3 建任务并开启日志

在界面「新建任务」中填写:

字段填写值
调度0 3 * * *
命令echo b2-stream-start; sleep 5; echo b2-stream-end
备注B2 test job
启用日志勾选

保存后任务 ID 为 74r1-rp4h。此时宿主机 crontab 已被改写:

crontab -l
# B2 test job | logsEnabled: true | id: 74r1-rp4h
0 3 * * * /opt/data/quzuo/tmp/cronmaster/b2/data/cron-log-wrapper.sh "74r1-rp4h" sh -c 'echo b2-stream-start; sleep 5; echo b2-stream-end'

注意两点:

  1. 启用日志后,crontab 里的命令被包装成了 cron-log-wrapper.sh "<jobId>" ...,包装脚本路径是宿主机路径(/opt/data/quzuo/tmp/cronmaster/b2/data/...),不是容器内路径——这正是 HOST_DATA_DIR 的作用。
  2. 由于 crontab 里保存的是完整包装命令,即使容器停了,任务仍然会按计划执行,日志也会继续落到宿主机 data/logs/。

4.4 手动执行与日志验证

通过 API 手动触发(同步模式会等待执行完成):

curl -s -H "Authorization: Bearer cronmaster-api-key-2026" \
  "http://127.0.0.1:8131/api/cronjobs/74r1-rp4h/execute?runInBackground=false"

回显:

{"success":true,"message":"Cron job executed successfully","output":"Command executed successfully","mode":"sync"}

查看日志目录:

ls data/logs/74r1-rp4h/
2026-10-27_20-24-50.log

日志内容:

--- [ JOB START ] ----------------------------------------------------
Command   : sh -c echo b2-stream-start; sleep 5; echo b2-stream-end
Timestamp : 2026-10-27 20:24:50
Host      : master
User      : root
--- [ JOB OUTPUT ] ---------------------------------------------------

b2-stream-start
b2-stream-end

--- [ JOB SUMMARY ] --------------------------------------------------
Timestamp : 2026-10-27 20:24:55
Duration  : 5s
Exit Code : 0
Status    : SUCCESS
--- [ JOB END ] ------------------------------------------------------

开始时间、结束时间、耗时 5s、退出码 0、状态 SUCCESS 全部由包装脚本采集,不是 UI 推算。这份日志在界面上通过 SSE 实时回放,运行中的任务可以边跑边看。

4.5 鉴权验证

不带凭证请求 API:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8131/api/cronjobs

回显:

401
{"error":"Unauthorized","message":"Authentication required. Use session cookie or API key (Bearer token)."}

五、反向代理与 HTTPS

Cronmaster 前端使用 SSE(/api/events、/api/logs/stream),反向代理必须关闭缓冲,否则实时日志会「卡住不动」。Nginx 参考配置:

server {
    listen 443 ssl;
    server_name cron.example.com;

    ssl_certificate     /etc/nginx/certs/cron.crt;
    ssl_certificate_key /etc/nginx/certs/cron.key;

    location / {
        proxy_pass http://127.0.0.1:8131;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # SSE 必需
        proxy_buffering    off;
        proxy_cache        off;
        proxy_read_timeout 3600s;
    }
}

配套的环境变量:

  • APP_URL=https://cron.example.com:生成回调/跳转链接时使用。启用 OIDC 时必填。
  • HTTPS=true:强制 HTTPS-only Cookie。
  • AUTH_MAX_LOGIN_ATTEMPTS:启用暴力破解防护。它按 X-Forwarded-For 第一段识别客户端,因此必须在反代上设置该请求头,否则所有客户端会被算成同一来源。
  • 反向代理自身要放行长连接,proxy_read_timeout 建议不小于 1 小时。

六、排障实录

6.1 宿主机端口被占用

现象:docker compose up -d 后容器起不来,docker compose logs 或 docker 事件里出现:

Bind for 0.0.0.0:8129 failed: port is already allocated

原因:测试机 8128、8129 已被其他服务占用。Cronmaster 只是一个 Web 服务,宿主机端口必须挑空闲的。

解决:先用 docker ps --format '{{.Ports}}' 与 ss -lntp 核对,再改 compose 里的左侧端口(最终用 8131)。

6.2 GHCR 直连拉取超时

现象:docker pull ghcr.io/fccview/cronmaster:2.4.1 长时间停在 Pulling fs layer,无进展。

解决:改用镜像源拉取后重新打 tag:

docker pull ghcr.nju.edu.cn/fccview/cronmaster:2.4.1
docker tag  ghcr.nju.edu.cn/fccview/cronmaster:2.4.1 ghcr.io/fccview/cronmaster:2.4.1

compose 里的 image 保持官方名不变,避免后续升级时踩到本地 tag 不一致。

6.3 镜像体积与发布说明不符

现象:官方 Release Notes 宣称 2.0 之后镜像体积缩到 70~80 MB 量级,但实测 docker images 显示 406 MB。

原因:2.0 起虽然切到 Next.js standalone,但运行镜像仍包含完整 node_modules(含 Codemirror、React、systeminformation 等),并且基础层是完整 Node 镜像而非 slim/distroless。

影响:406 MB 对现代磁盘不算大,但在带宽受限的内网批量分发时值得提前规划。以实测 406 MB 作为容量规划依据,不要用发布说明里的数字。

6.4 启用日志时保存任务失败

现象:勾选「启用日志」保存,界面报错,crontab 未被修改。

原因:应用需要把 /app/data 映射成宿主机路径写进 crontab,解析顺序为 HOST_DATA_DIR → docker inspect(Docker Socket)→ HOST_PROJECT_DIR/data。三者都拿不到时,保存会主动失败并保持 crontab 原样(这是一个「宁可失败也不写错路径」的正确设计)。

解决:显式设置 HOST_DATA_DIR,或确认 /var/run/docker.sock 已挂载且当前用户可访问。

6.5 中文界面没生效

现象:LOCALE=zh-cn 时界面仍为英文,日志里有 Cannot find module '...zh-cn.json'。

解决:2.4.1 内置文件是 zh.json,写 LOCALE=zh。详见 B1 篇。

七、升级与回滚

7.1 升级

cd /opt/cronmaster
docker compose pull
docker compose up -d

Cronmaster 无数据库,没有 schema 迁移,升级风险主要在配置项变更。升级前建议:

  1. 备份 data/(日志、包装脚本、会话)与宿主机 crontab -l 输出。
  2. 记录当前镜像 digest:docker inspect --format '{{index .RepoDigests 0}}' ghcr.io/fccview/cronmaster:2.4.1。
  3. 阅读目标版本的 Release Notes,确认是否有环境变量被废弃。

7.2 回滚

只要 compose 里把 image 改回旧 tag 并 docker compose up -d 即可:

image: ghcr.io/fccview/cronmaster:2.4.0

注意:回滚后 data/ 里可能有新版本写入的结构(例如新版日志目录命名),旧版本通常能读取,但反向不保证。跨大版本回滚前先备份 data/。

7.3 已有任务的兼容性

启用了日志的任务,其 crontab 里保存的是包装脚本的宿主机绝对路径。升级/回滚只要不改宿主机上的 data/ 目录位置,已有任务不受影响,仍会继续执行并写日志。

八、安全清单

  • 只监听内网或只绑定 127.0.0.1:8131 由反代暴露。
  • 强 AUTH_PASSWORD + 独立 API_KEY;必要时改用 OIDC SSO 并限制 OIDC_USER_GROUPS。
  • 开启 AUTH_MAX_LOGIN_ATTEMPTS 并在反代设置 X-Forwarded-For。
  • 设置 FRAME_ANCESTORS=self 防止点击劫持(详见 C1 篇)。
  • 宿主机层面限制 Docker Socket 的暴露面:该容器等同 root,不要与其他不可信容器共用宿主机。
  • 定期审计 crontab:面板能写 crontab,等于能执行任意命令。

九、结论

Cronmaster 2.4.1 的 Compose 部署本身不复杂,真正的门槛是权限模型:root + pid: host + privileged + Docker Socket 四件套缺一不可,这决定了它只能待在可信内网。「启用日志」是体验分水岭——开启后 crontab 里保存的是宿主机绝对路径的包装脚本,任务执行与面板解耦,容器挂掉也不影响任务运行。

实测确认的两个「官方文档与实测不一致」点:镜像 406 MB(非发布说明的 70~80 MB)、LOCALE 必须写 zh(非 zh-cn)。规划容量与配置时以本文实测为准。

延伸阅读:本专栏《Cronmaster 2.4.1 配置参数详解》逐项拆解全部环境变量;《Cronmaster 2.4.1 API 详解》给出 REST 接口的实测响应与边界。