Docker

要定时跑容器,不必先上 Kubernetes。

如果团队用 Docker 但没有 Kubernetes,定时容器作业其实无处安放。Dagu 是驱动你现有 Docker 守护进程的单一二进制文件,为容器补上依赖、重试、日志和 Web 界面。

无需 Kubernetes,也无需容器平台
所有步骤共享一个容器,或每步各用自己的镜像
可以 exec 进入 compose 已启动的容器
Podman 通过同一套 Docker 兼容 API 工作
01

docker run 与 Kubernetes CronJob 之间的空白

给容器加调度,往往就是选项用尽的地方。Kubernetes CronJob 是成熟答案,但前提是你已经有集群。在那条线之下,通常的做法是 cron 调用 docker run,除了触发之外什么也没有。

  • cron 加 docker run 没有重试、作业间依赖、运行历史,也无法查看昨晚为何失败
  • 为了一份夜间报表而引入 Kubernetes,平台的体量远超作业本身
  • 把容器视为一种步骤类型而非部署目标的通用编排器,正好落在这个中间地带
02

整个工作流共用一个容器

在工作流层声明容器,所有步骤都在同一个长期运行的容器中执行。步骤共享文件系统,因此某一步安装的软件包对下一步依然可用。

  • 依赖只需安装一次即可跨步骤复用,无需重建镜像或按任务重复安装
  • 用 volumes 挂载宿主路径,并为整个工作流一次性设置环境变量
  • 重试、依赖和失败处理仍是普通的工作流字段,不因运行在容器中而改变

该模式下步骤通过 docker exec 执行,因此镜像的 ENTRYPOINT 和 CMD 不会用于步骤命令。请把命令写在步骤里。

完全在一个镜像内完成的夜间作业
# nightly-report.yaml
schedule: "0 3 * * *"
max_active_runs: 1

container:
  image: python:3.12
  volumes:
    - ./data:/data
  env:
    - TZ=Asia/Tokyo

steps:
  - id: install
    run: pip install -r /data/requirements.txt

  - id: build_report
    run: python /data/build_report.py
    depends: install
    retry_policy:
      limit: 2
      interval_sec: 120

  - id: publish
    run: python /data/publish.py
    depends: build_report

mail_on:
  failure: true
03

在已经运行的容器里做维护

exec 模式把工作流指向一个已经运行的容器,例如由 Docker Compose 启动的容器。定时任务在真实运行的应用容器内执行,而不是在它的一个新副本里。

  • 数据库迁移、清理缓存和队列维护都针对真正在运行的服务执行
  • 配置就是把容器名写成一个字符串
  • 工作流依然拥有 compose 文件无法表达的依赖关系与失败通知
针对运行中的 compose 服务的定时维护
# app-maintenance.yaml
schedule: "0 4 * * *"
max_active_runs: 1

# docker compose で起動済みのコンテナに exec する
container: myapp-web

steps:
  - id: migrate
    run: php artisan migrate --force

  - id: prune_sessions
    run: php artisan session:prune
    depends: migrate

  - id: clear_cache
    run: php artisan cache:clear
    depends: prune_sessions

mail_on:
  failure: true
04

每个步骤一个镜像

当流水线跨越多种工具时,每一步都可以带上自己的镜像。工作流仍是一个文件、一份调度,而步骤保持独立。

  • 用数据库客户端镜像抽取、用语言镜像转换、再用另一个客户端装载,不需要一个包含全部三者的镜像
  • 步骤层的容器会覆盖工作流层的设置,因此大体统一的工作流仍可留出例外
  • 镜像拉取策略可按步骤配置,区分需要固定的镜像与频繁重建的镜像
一个工作流,三个镜像
# etl-pipeline.yaml
schedule: "0 2 * * *"
max_active_runs: 1

steps:
  - id: extract
    container:
      image: mysql:8
      volumes:
        - ./work:/work
    run: mysqldump --host db.internal -u svc app > /work/dump.sql

  - id: transform
    container:
      image: python:3.12
      volumes:
        - ./work:/work
    run: python /work/transform.py
    depends: extract

  - id: load
    container:
      image: postgres:16
      volumes:
        - ./work:/work
    run: psql -h dw.internal -f /work/out.sql
    depends: transform

handler_on:
  failure:
    run: ./scripts/notify-failure.sh

mail_on:
  failure: true
05

对宿主的要求

容器步骤需要与 Docker 兼容的 API 通信。这是唯一的要求,同时也是规划部署前值得知道的约束。

  • 本地 Docker 套接字或通过 DOCKER_HOST 的远程守护进程都可以
  • 设置 DAGU_CONTAINER_RUNTIME=podman 即可通过 Podman 的 Docker 兼容 API 使用
  • 由于 Dagu 是单一二进制文件,调度器本身不需要集群、元数据数据库或消息代理

Dagu Cloud 托管实例使用 gVisor 隔离,不暴露容器守护进程套接字,因此其中无法使用容器步骤。请使用自托管 Dagu,或把工作流路由到自托管 worker。

06

Kubernetes 仍然更合适的场景

本页主张的是在特定情况下使用更小的工具,而不是普遍地回避 Kubernetes。

  • 如果你已经在运行集群,CronJob 是放置定时容器的合理位置,且不需要新增任何东西
  • 如果作业需要 Pod 级调度、自动扩缩或跨节点装箱,那是集群的职责而非编排器的
  • 对于希望把编排留在集群之外、同时向集群提交任务的工作流,Dagu 提供了 Kubernetes 步骤

FAQ

Practical questions before adopting

定时运行容器化作业必须用 Kubernetes 吗?

不必。Dagu 直接与 Docker 兼容的守护进程通信,因此一台装有 Docker 的主机就够了。需要集群级调度与扩缩时 Kubernetes 才值得,而不是仅仅为了凌晨三点跑一个容器。

这与 Airflow 的 Docker operator 有何不同?

主要差别在运维成本。Airflow 在运行一个容器之前需要调度器、元数据数据库和 Python DAG 框架。Dagu 是状态存于文件的单一二进制文件,容器是工作流或步骤上的一个字段,而不是 Python operator。

步骤之间可以共享文件吗?

可以。使用工作流层容器时,步骤在同一容器中运行并直接共享文件系统。若每步使用不同镜像,则把同一宿主路径挂载到各步骤,上一步的输出下一步即可见。

Podman 能用吗?

能,通过 Podman 的 Docker 兼容 API。在自托管环境中设置 DAGU_CONTAINER_RUNTIME=podman,容器步骤的行为相同。

能在 Docker Compose 启动的容器里执行步骤吗?

可以,这就是 exec 模式。指定运行中的容器名,工作流的步骤便在其中执行。针对运行中的 compose 环境做定时迁移和缓存维护通常就是这样接的。

Next step

Start with one workflow.

Install Dagu, move one script that runs on cron today into YAML, and decide from a real run history.