Docker

Контейнерные задания по расписанию — без внедрения Kubernetes ради этого.

Если команда использует Docker, но не Kubernetes, запланированному контейнерному заданию попросту негде жить. Dagu — единственный бинарный файл, управляющий уже имеющимся демоном Docker и добавляющий контейнерам зависимости, повторы, логи и веб-интерфейс.

Не нужны ни 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
  • Podman поддерживается через свой Docker-совместимый API при установке DAGU_CONTAINER_RUNTIME=podman
  • Поскольку Dagu — один бинарный файл, самому планировщику не нужны ни кластер, ни база метаданных, ни брокер

Управляемые инстансы Dagu Cloud работают с изоляцией gVisor и не предоставляют сокет демона контейнеров, поэтому контейнерные шаги там недоступны. Используйте self-hosted Dagu или направьте процесс на self-hosted воркер.

06

Где Kubernetes по-прежнему правильный ответ

Эта страница отстаивает меньший инструмент в конкретной ситуации, а не отказ от Kubernetes вообще.

  • Если кластер уже есть, CronJob — разумное место для контейнеров по расписанию и не требует ничего нового
  • Если заданиям нужно планирование на уровне подов, автомасштабирование или упаковка по узлам — это работа кластера, а не оркестратора
  • Для процессов, которые отдают работу кластеру, оставляя оркестрацию снаружи, у Dagu есть шаг Kubernetes

FAQ

Practical questions before adopting

Нужен ли Kubernetes, чтобы запускать контейнерные задания по расписанию?

Нет. Dagu напрямую работает с Docker-совместимым демоном, поэтому достаточно одного хоста с установленным Docker. Kubernetes оправдан, когда нужны планирование и масштабирование на уровне кластера, а не просто запуск контейнера в три часа ночи.

Чем это отличается от Docker-оператора в Airflow?

Прежде всего стоимостью эксплуатации. Airflow требует планировщика, базы метаданных и Python-фреймворка DAG, прежде чем запустит контейнер. Dagu — один бинарный файл с состоянием в файлах, а контейнер задаётся полем процесса или шага, а не Python-оператором.

Могут ли шаги обмениваться файлами?

Да. При контейнере уровня процесса шаги работают в одном контейнере и напрямую разделяют его файловую систему. При разных образах по шагам смонтируйте общий путь хоста в каждый шаг.

Работает ли Podman?

Да, через Docker-совместимый API Podman. Установите DAGU_CONTAINER_RUNTIME=podman в self-hosted инсталляции, и контейнерные шаги будут вести себя так же.

Можно ли выполнить шаг внутри контейнера, поднятого 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.