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 拉取超时等五项排障实录与升级回滚方案。
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'
注意两点:
- 启用日志后,crontab 里的命令被包装成了
cron-log-wrapper.sh "<jobId>" ...,包装脚本路径是宿主机路径(/opt/data/quzuo/tmp/cronmaster/b2/data/...),不是容器内路径——这正是HOST_DATA_DIR的作用。 - 由于 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 迁移,升级风险主要在配置项变更。升级前建议:
- 备份
data/(日志、包装脚本、会话)与宿主机crontab -l输出。 - 记录当前镜像 digest:
docker inspect --format '{{index .RepoDigests 0}}' ghcr.io/fccview/cronmaster:2.4.1。 - 阅读目标版本的 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 接口的实测响应与边界。