Dokku 定时任务(Scheduled Cron Tasks)完全指南:从 app.json 声明到 cron:run 实战
本篇技术指南围绕 Dokku 内置的定时任务(Scheduled Cron Tasks)能力展开,详细讲解如何通过 app.json 中的 cron 键为应用声明周期性执行的命令,如何用 cron:set、cron:list、cron:suspend、cron:resume、cron:run、cron:report 等命令管理任务生命周期,以及如何通过 vector 集成持久化任务输出。读完本文,你将能够为任何 Dokku 应用配置可靠的定时任务,理解其执行环境、超时回收机制与调度器差异,并掌握自管理 cron 的高级用法。
功能概述与命令速览
Dokku 从 0.23.0 版本开始提供内置的定时任务支持(cron 插件,位于 plugins/cron)。它把应用 app.json 中的 cron 声明转换为调度器可执行的定时任务:对使用宿主 crontab 的调度器(如 docker-local)写入 dokku 用户 crontab,对自带 cron 后端的调度器(如 k3s)则原生调度。所有命令如下:
cron:list <app> [--format json|stdout] # 列出应用的定时任务
cron:report [<app>] [<flag>] # 显示应用 cron 报告
cron:resume <app> <cron_id> # 恢复一个 cron 任务
cron:run <app> <cron_id> [--detach] [--ttl-seconds SECONDS] # 即时运行一个 cron 任务
cron:set [--global|<app>] <key> <value> # 设置或清除应用的 cron 属性
cron:suspend <app> <cron_id> # 挂起一个 cron 任务
Dokku 托管 Cron:通过 app.json 声明任务
Dokku 自动调度 dokku run 命令,其入口是应用 app.json 文件中的 cron 键。从源码看,app.json 由 plugins/app-json/appjson.go 解析,其中 AppJSON.Cron 是 CronTask 的列表,每个任务包含 command、maintenance、schedule、concurrency_policy 四个字段。
声明任务
以下 app.json 示例等效于每天执行一次 dokku run $APP npm run send-email:
{
"cron": [
{
"command": "npm run send-email",
"schedule": "@daily"
}
]
}
app.json 的默认搜索路径与部署方式相关;如需从 monorepo 等场景指定其他位置,可通过 app-json:set <app> appjson-path <path> 设置(值为相对于基础搜索目录的路径),详见 deployment-tasks.md。
任务属性说明
每个 cron 任务支持以下属性:
| 属性 | 说明 |
|---|---|
command |
在构建出的应用镜像内执行的命令,也可以直接引用 Procfile 条目 |
schedule |
cron 兼容的调度定义,决定命令何时运行。秒通常不支持 |
maintenance |
布尔值,决定该任务是否处于维护(不可执行)状态 |
concurrency_policy |
字符串(默认 allow),控制任务与自身是否可并发执行。合法值:allow(允许并发)、forbid(已有任务运行则新任务直接退出)、replace(终止已有任务并启动新任务) |
每个应用可以声明零个或多个 cron 任务。任务的验证发生在构建产物生成之后、应用部署之前;cron 调度表则在部署后阶段(post-deploy)更新。也就是说,一份非法的时间表或命令会在部署时直接报错,而不是等到调度触发时才暴露。
从 plugins/cron/cron.go 的实现可以看到,schedule 使用 robfig/cron/v3 解析器校验,其标志位组合为 Minute | Hour | Dom | Month | Dow | Descriptor——因此不支持秒字段,但支持 @daily、@hourly 等描述符。concurrency_policy 非 allow/forbid/replace 时会返回"Invalid cron concurrency policy"错误;command 与 schedule 为空时也会在部署阶段报错(WarnToFailure 模式)。
任务执行时长上限与回收
cron 任务最长可运行 24 小时,超过后会被系统回收:
docker-local调度器通过每 5 分钟运行一次的dokku ps:retire扫描回收超时任务,因此任务实际可能超时最多约 5 分钟;k3s调度器则直接通过 Job 的activeDeadlineSeconds强制执行期限。
源码中 DefaultTTLSeconds 常量定义为 86400(plugins/cron/cron.go),docker-local 会将其作为 com.dokku.active-deadline-seconds 标签盖印到容器上,k3s 则渲染为 CronJob 的 activeDeadlineSeconds。
任务执行环境须知
运行定时任务时有以下几点需要注意:
- 定时任务在应用运行时的环境中执行;如果应用镜像不存在,命令可能执行失败;
- 调度基于宿主服务器时区(通常为 UTC);
- 目前 cron 模板中只指定了
PATH与SHELL两个环境变量:MAILTO可通过cron:set设置;MAILFROM可通过cron:set设置;
- 每个定时任务都在一个一次性
run容器内执行,因此会继承为run容器配置的所有 docker-options;任务之间绝不共享资源; - 定时任务按调度器分别支持:使用宿主 crontab 的调度器(如
docker-local)会把app.json中的 cron 任务写入dokku用户 crontab;自己管理 cron 后端的调度器(如k3s)则原生调度; - 宿主 crontab 调度器(如
docker-local)管理的所有应用的任务都写入同一个、归属于dokku用户的 crontab 文件,该 crontab 应视为 Dokku 专用,不要手动写入其他条目; command会被分词(tokenize)后直接在容器内 exec,不会解释;、&&、|、>等 shell 特性。包含裸 shell 操作符的命令在部署时验证app.json会被拒绝,因此格式错误的 cron 命令会导致部署失败而非静默运行失败。如果需要 shell 语义,请显式包裹命令,例如"sh -c 'do-thing > /var/log/x.log'";- 任务输出写入容器的 stdout 和 stderr,可通过 Dokku 的 vector 集成持久化(见下文);
- cron 任务不能在
app.json中声明日志文件路径。写入dokku用户 crontab 的只有dokku cron:run <app> <cron_id>行,任何来自部署仓库的路径都不会被插值进去。
关于命令分词,plugins/cron/cron.go 的 ValidateCronCommand 使用 mvdan.cc/sh/v3/shell 的 shell.Fields 解析命令;cron:run 在派发时使用同一解析器,因此部署时能通过校验的命令一定可以执行。对应的单元测试见 plugins/cron/cron_test.go:"sh -c 'echo CRON_OK; echo hi > /tmp/x.txt'" 这类显式包裹的命令会被接受,而 "echo CRON_OK; echo hi > /tmp/x.txt"、"cmd1 && cmd2"、"cmd | other"、"cmd > file"、"cmd $(other)" 都会被拒绝。
持久化 Cron 任务输出
如果不做额外配置,任务输出只会投递到 cron 配置的 MAILTO 地址。要保留输出,可以通过 Dokku 的 vector 集成配置一个 sink,详见 logs.md 的 vector 日志投递章节。
为应用配置的任何 sink 都会与应用的其余日志一起收到 cron 任务输出:
dokku logs:set node-js-app vector-sink "console://?encoding[codec]=json"
若要单独保留 cron 输出,改用 vector-cron-sink,cron 输出就会被路由到这里而不是应用主 sink:
dokku logs:set node-js-app vector-cron-sink "console://?encoding[codec]=text"
要写入宿主机上的文件,可指向 /var/log/dokku/apps 目录(该目录已挂载进 vector 容器)。dokku_cron_id 字段可用于模板化,让每个任务拥有自己的日志文件:
dokku logs:set node-js-app vector-cron-sink "file://?path=/var/log/dokku/apps/node-js-app/cron-{{ dokku_cron_id }}.log&encoding[codec]=text"
需要注意的路由规则(logs.md):设置 cron sink 是移动而非复制 cron 输出——vector 把每行日志恰好路由到两个 sink 之一;仅设置 vector-cron-sink 时 cron 输出去 cron sink、其余输出无处可去;两者都设置时 cron 输出去 cron sink、其余去 vector-sink。cron 分支的事件额外带 dokku_app 与 dokku_cron_id 两个字段(只有它们保证存在,模板中引用其他字段可能导致日志被静默丢弃);对非常短命的任务还存在相关注意事项。
管理 cron 设置:cron:set
cron 插件提供若干可按应用管理的设置项。下表列出了本文其他章节未覆盖的属性:
| 名称 | 描述 | 级别 | 全局默认值 |
|---|---|---|---|
mailfrom |
在 cron 文件中设置 MAILFROM 变量,用于 cron 报告 |
仅全局 | 空字符串 |
maintenance |
是否让应用运行 cron | 应用与全局 | false |
mailto |
在 cron 文件中设置 MAILTO 变量,用于 cron 报告 |
仅全局 | 空字符串 |
所有设置都通过 cron:set 命令完成。以 maintenance 为例:
dokku cron:set node-js-app maintenance true
传入空值即可恢复默认值:
dokku cron:set node-js-app maintenance
如果属性可以全局设置(如 mailto),使用 --global 标志;应用未设置时,若全局值存在则生效:
dokku cron:set --global maintenance true
同样,传空值可恢复全局默认值:
dokku cron:set --global maintenance
从实现看(plugins/cron/subcommands.go),cron:set 在写入属性后还会触发 scheduler-cron-write 触发器(对 --global 只传调度器参数),让调度器重新生成 crontab;同时它也是 cron:suspend/cron:resume 的底层实现。属性定义见 plugins/cron/cron.go:默认属性包含 mailfrom、mailto、maintenance,其中 mailfrom 与 mailto 为全局属性。
列出 Cron 任务:cron:list
使用 cron:list 命令列出应用的 cron 任务,命令接收 app 参数:
dokku cron:list node-js-app
ID Schedule Command
cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5 @daily node index.js
cGhwPT09dHJ1ZT09PSogKiAqICogKg== * * * * * true
输出也支持 JSON 格式:
dokku cron:list node-js-app --format json
[{"id":"cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5","app":"node-js-app","command":"node index.js","schedule":"@daily"}]
获取全局任务,使用 --global 标志:
dokku cron:list --global
ID Schedule Command
5cruaotm4yzzpnjlsdunblj8qyjp @daily /bin/true
从源码看(plugins/cron/subcommands.go),stdout 格式的表格会额外展示 Concurrency 与 Maintenance 列;Maintenance 列对任务级挂起显示 true (task),对应用级维护显示 true (app)。--format 仅支持 stdout 与 json 两种值。任务 ID 由 GenerateCommandID 生成:对 appName + "===" + Command + "===" + Schedule 做 base36 编码(plugins/cron/cron.go),这也是为什么示例中同样的命令与时间表在不同应用会得到不同 ID。
挂起与恢复指定 Cron 任务
cron 任务可以临时挂起(暂停按计划执行),之后恢复,适用于维护或调试场景。
挂起指定任务,使用 cron:suspend 并带上应用名与 cron ID:
dokku cron:suspend node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5
被挂起的任务将不再按计划执行。可通过 cron:list 输出的 Maintenance 列确认任务已挂起——挂起的任务会显示 true (task)。
恢复挂起的任务,使用 cron:resume:
dokku cron:resume node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5
恢复后任务将重新按计划执行。cron ID 可从 cron:list 输出获取。
实现细节:cron:suspend 等价于 cron:set <app> maintenance.<cron_id> true,cron:resume 等价于 cron:set <app> maintenance.<cron_id>(清除该属性)(plugins/cron/subcommands.go 与 plugins/cron/subcommands.go)。属性前缀 maintenance. 定义于 plugins/cron/cron.go。任务级维护属性不能全局设置(cron:set --global maintenance.<id> 会报错),且仅当属性值为 true 时才会覆盖 app.json 中声明的 maintenance(见 FetchCronTasks 中的合并逻辑,plugins/cron/cron.go)。
即时执行 Cron 任务:cron:run
cron:run 命令可以即时调用 cron 任务,接收 app 参数和 cron ID(可从 cron:list 输出获取):
dokku cron:run node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5
默认情况下任务在附加(attached)容器中运行(视调度器支持而定)。要在后台分离容器中运行,指定 --detach 标志:
dokku cron:run node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5 --detach
即时调用默认也有 24 小时(86400 秒)的运行上限,与计划调度的任务相同。可用 --ttl-seconds 指定不同期限:
dokku cron:run node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5 --detach --ttl-seconds 600
该值只作用于本次调用——由调度计划启动的任务仍保持 24 小时默认值。所有一次性 cron 执行的容器在调用结束后都会被终止。
实现细节(plugins/cron/subcommands.go):cron:run 会先校验 --ttl-seconds 必须为正整数(validateTTLSeconds,plugins/cron/cron.go,对应测试见 plugins/cron/cron_test.go),校验任务 ID 存在,然后用 shell.Fields 对命令分词,设置 DOKKU_DETACH_CONTAINER、DOKKU_DISABLE_TTY(分离模式)、DOKKU_CONCURRENCY_POLICY、DOKKU_CRON_ID、DOKKU_RM_CONTAINER=1、DOKKU_RUN_TTL_SECONDS 等环境变量,最终通过 scheduler-run 触发器派发给应用的调度器执行。
查看 Cron 报告:cron:report
使用 cron:report 命令查看应用的 cron 配置报告:
dokku cron:report
=====> node-js-app cron information
Cron task count: 2
=====> python-sample cron information
Cron task count: 0
=====> ruby-sample cron information
Cron task count: 10
也可以只查看指定应用:
dokku cron:report node-js-app
=====> node-js-app cron information
Cron task count: 2
还可以传标志,只输出你关心的特定信息:
dokku cron:report node-js-app --cron-task-count
可设置的属性及其对应的 report 标志、JSON 键名详见下文"属性参考"一节(完整实现见 plugins/cron/report.go)。
属性参考
以下属性可通过 cron:set 设置,并通过 cron:report 查看:
[!NOTE]
Report flags列是cron:report接受的 CLI 参数名。cron:report --format json输出的 JSON 键为去掉--cron-前缀后的同名(如global-mailto、computed-mailto、maintenance)。带cron-前缀的旧键(如cron-global-mailto)在 0.38.x 弃用窗口期仍会输出,并将在未来大版本中移除。
| 属性 | 作用域 | 默认值 | Report flags | 描述 |
|---|---|---|---|---|
mailfrom |
仅全局 | 无 | --cron-global-mailfrom、--cron-computed-mailfrom |
cron 失败邮件使用的 From: 地址 |
mailto |
仅全局 | 无 | --cron-global-mailto、--cron-computed-mailto |
cron 失败邮件的收件地址;为空则禁用邮件 |
maintenance |
应用 + 全局 | false |
--cron-maintenance、--cron-global-maintenance、--cron-computed-maintenance |
为 true 时挂起应用(或全局)的所有 cron 任务 |
maintenance.<cron-id> |
仅应用 | false |
--cron-maintenance-<cron-id>(按任务动态生成) |
按计算出的 ID 挂起单个 cron 任务(每个任务一行);由 cron:suspend/cron:resume 写入 |
底层原理:crontab 生成与调度器协作
理解"谁能把任务写进 crontab"有助于排障。核心逻辑位于 plugins/cron/crontab.go:
usesHostCron通过scheduler-uses-host-cron触发器询问某调度器是否使用宿主 crontab(空调度器或未实现该触发器的视为false,crontab.go);generateCronTasks会收集所有使用宿主 crontab 的应用的app.json任务,再加上通过cron-entries触发器注入的任务(格式为$SCHEDULE;$COMMAND[;$LOGFILE]),并过滤掉处于维护状态的任务(crontab.go);writeCronTab每次都是全量重新生成dokku用户 crontab(先crontab -r -u dokku再写入),因此多个调度器共用宿主 crontab 时不会互相覆盖;任务列表为空时直接删除 crontab(crontab.go)。
最终渲染使用的模板为 plugins/cron/templates/cron.tmpl,内容大致为:
MAILFROM={{ .Mailfrom }}
MAILTO={{ .Mailto }}
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
{{ $task.Schedule }} {{ $task.DokkuRunCommand }}
DokkuRunCommand 对应用任务恒输出 dokku cron:run <app> <cron_id>(plugins/cron/cron.go),绝不把用户命令直接写进 crontab——测试 plugins/cron/cron_test.go 专门断言 crontab 行不包含 ;、>、|、&、`、$ 等 shell 元字符,且不会把用户命令泄漏到 crontab 行中。
自管理 Cron(高级用法)
[!WARNING] 自管理 cron 属于高级用法。虽然下文提供操作说明,但强烈建议优先使用内置定时任务支持,除非确有必要。
某些安装场景可能需要更细粒度的 cron 控制,以下是配置 cron 的高级指引。
使用 run 执行 cron 任务
可以随时使用一次性容器运行应用任务:
dokku run node-js-app some-command
对于不应被打断的任务,run 是处理 cron 任务的首选方式,因为即使发生部署或扩缩容事件,容器也会继续运行。代价是多个并发任务同时运行时内存占用会增加。
使用 enter 执行 cron 任务
在 Procfile 中加入以下条目:
cron: sleep infinity
将 cron 进程扩到 1:
dokku ps:scale node-js-app cron=1
然后即可在该容器中运行所有命令:
dokku enter node-js-app cron some-command
注意也可以同时运行多个命令以减少内存占用,但这可能会污染容器环境。
对于需要正确恢复的任务,应当使用上述方式——因为部署和扩缩容事件会中断正在运行的任务,且后续命令始终运行在最新容器中。注意如果把 cron 容器缩容,可能会中断任务正常运行。
通用 cron 建议
定期任务在 Dokku 上需要一些额外注意,以下通用建议有助于保证任务成功运行:
- 在 cron 任务中使用
dokku用户;- 否则
dokku二进制会尝试用sudo执行,cron 运行会失败并报sudo: no tty present and no askpass program specified;
- 否则
- 添加
MAILTO环境变量,把 cron 邮件发给自己; - 添加
PATH环境变量,或指定宿主机上二进制文件的完整路径; - 添加
SHELL环境变量,运行命令时指定 Bash; - 让 cron 任务按时间排序存放;
- 保持服务器时间为 UTC,读取 cronfile 时无需换算夏令时;
- 尽量在流量最低的时段运行任务;
- 用 cron 来触发任务,而不是运行任务本体——用 rabbitmq 之类的真实队列系统处理实际任务;
- 尽量让任务保持安静,只在出错时发邮件;
- 不要屏蔽标准错误或标准输出:屏蔽前者会错过失败信息;屏蔽后者意味着你其实应该通过修改应用来调整日志级别;
- 使用 Dead Man's Snitch 之类的服务验证 cron 任务是否成功完成;
- 在 cronfile 中写大量注释,说明每个任务在做什么,免得日后花时间解读文件;
- 将 cronfile 放在如
/etc/cron.d/APP的模式路径下; - 不要在 cronfile 文件名中使用非 ASCII 字符,cron 对此很挑剔;
- 记得 cronfile 末尾要有换行符,cron 同样很挑剔。
以下是一份可直接参考的应用 cronfile 示例:
# server cron jobs
MAILTO="mail@dokku.me"
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
# m h dom mon dow username command
# * * * * * dokku command to be executed
# - - - - -
# | | | | |
# | | | | +----- day of week (0 - 6) (Sunday=0)
# | | | +------- month (1 - 12)
# | | +--------- day of month (1 - 31)
# | +----------- hour (0 - 23)
# +----------- min (0 - 59)
### HIGH TRAFFIC TIME IS B/W 00:00 - 04:00 AND 14:00 - 23:59
### RUN YOUR TASKS FROM 04:00 - 14:00
### KEEP SORTED IN TIME ORDER
### PLACE ALL CRON TASKS BELOW
# removes unresponsive users from the subscriber list to decrease bounce rates
0 0 * * * dokku dokku run node-js-app some-command
# sends out our email alerts to users
0 1 * * * dokku dokku ps:scale node-js-app cron=1 && dokku enter node-js-app cron some-other-command && dokku ps:scale node-js-app cron=0
### PLACE ALL CRON TASKS ABOVE, DO NOT REMOVE THE WHITESPACE AFTER THIS LINE
小结
Dokku 的定时任务体系由"声明(app.json 的 cron 键)— 验证(部署期校验时间表与命令分词)— 调度(宿主 crontab 或调度器原生后端)— 执行(一次性 run 容器)— 回收(24 小时 TTL)"五段组成。日常使用推荐完全走内置方案:用 app.json 声明、用 cron:list/cron:report 观察、用 cron:suspend/cron:resume 维护、用 cron:run 应急触发,并配合 vector-cron-sink 持久化输出;只有在需要细粒度控制或特殊约束时才考虑自管理 cron。相关可进一步阅读的仓库资料包括 cron 插件源码、cron 单元测试、app.json 格式定义 与 vector 日志集成文档。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.2 K634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown300
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java101
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java60
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript60
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python280