Cron-задачи в Docker: запуск и мониторинг
Контейнеры рассчитаны на один долгоживущий процесс, а cron-задачи короткие и периодические. Из этого несоответствия растёт большинство проблем: задачи не видят переменные окружения, работают не в том часовом поясе, пишут ошибки в никуда или тихо перестают запускаться, когда контейнер заменили. Вот три распространённые схемы и как сделать каждую надёжной.
Три способа запускать задачи по расписанию
1. Cron на хосте запускает команду в контейнере
Crontab на сервере вызывает docker exec в работающем контейнере или запускает разовый контейнер через docker run --rm. Это самый простой вариант, образ менять не нужно:
# команда в работающем сервисе app 17 3 * * * docker compose -f /srv/app/compose.yml exec -T app php artisan report:send # новый контейнер на каждый запуск */10 * * * * docker run --rm --env-file /srv/app/.env registry.example.com/sync:latest
-T отключает псевдотерминал, которого у cron нет; без этого флага docker compose exec падает. Минус схемы — расписание живёт на сервере, а не в репозитории.
2. Отдельный контейнер-планировщик
Запустите из того же образа отдельный контейнер, единственный процесс которого — планировщик. Подойдёт и классический cron, но для контейнеров удобнее supercronic: он читает обычный crontab, передаёт задачам переменные окружения контейнера и пишет их вывод в stdout, где его видит docker logs.
# compose.yml
services:
app:
image: registry.example.com/app:1.8
scheduler:
image: registry.example.com/app:1.8
command: ["supercronic", "/app/crontab"]
env_file: .env
Файл crontab лежит в репозитории вместе с кодом, поэтому расписания проходят ревью и выкатываются как всё остальное. Устанавливайте supercronic из релизов на GitHub и проверяйте опубликованную контрольную сумму в Dockerfile.
3. Cron внутри контейнера с приложением
Так можно, но понадобится менеджер процессов, чтобы запускать и cron, и приложение, а падение одного может уронить другое. Лучше выбрать вариант 2.
Ловушки
- Переменные окружения. Классический cron запускает задачи почти с пустым окружением, поэтому переменные из
docker run -eилиenv_fileвнутри задачи не видны. Используйте supercronic или сохраняйте окружение в файл при старте контейнера и подгружайте его в задаче. - Часовой пояс. Контейнеры работают в UTC, если не указано иное. Пишите расписание в UTC или задайте
TZ=Europe/Moscow; в Alpine ещё нужен пакетtzdata, иначе переменная игнорируется. - Логи. Cron отправляет вывод задач по почте, а почты в контейнере нет. Направьте вывод в вывод контейнера: supercronic делает это сам, а для классического cron от root допишите
>> /proc/1/fd/1 2>&1. - Знак процента. В строке crontab
%означает перевод строки. Пишитеdate +\%Fвместоdate +%F. - Наложение запусков. Если запуск может длиться дольше интервала, оберните его в
flock -n /tmp/sync.lock, чтобы вторая копия не стартовала.
Убедитесь, что каждый запуск состоялся
Когда контейнер-планировщик остановился, выпал из нового деплоя или потерял crontab, ничего не запускается и никто не сообщает об ошибке. Добавьте пинг в каждую задачу, чтобы тишина превращалась в алерт. В образах Alpine есть wget из BusyBox; curl можно поставить через apk add curl.
# пинг только после успешного запуска */10 * * * * /app/sync.sh && wget -q -T 10 -O /dev/null https://ping.heartcron.com/<uuid> # передать код выхода: 0 — успех, остальное сразу отправляет алерт */10 * * * * /app/sync.sh; wget -q -T 10 -O /dev/null https://ping.heartcron.com/<uuid>/$?
В Heartcron задайте проверке то же расписание и часовой пояс, что в crontab, и запас времени чуть больше обычной длительности задачи.