Короткий ответ: cron запускает команды по расписанию, и запись задания состоит из пяти полей времени и самой команды. Главные ошибки — не указан абсолютный путь к программе, не задано окружение, и вывод команды никуда не пишется, из-за чего о сбое никто не узнаёт. Надёжнее запускать через cron не длинную команду, а скрипт с ключом set -euo pipefail и журналированием.
Этот материал продолжает цикл про настройку Ubuntu 24.04 на VPS.
Формат записи
минута час день_месяца месяц день_недели команда
0-59 0-23 1-31 1-12 0-7 (0 и 7 — воскресенье)
| Запись | Когда выполняется |
|---|---|
0 3 * * * |
Каждый день в 03:00 |
*/15 * * * * |
Каждые 15 минут |
0 */6 * * * |
Каждые 6 часов |
30 2 * * 1 |
По понедельникам в 02:30 |
0 4 1 * * |
Первого числа каждого месяца в 04:00 |
0 9-18 * * 1-5 |
Каждый час с 9 до 18 по рабочим дням |
Символ */15 означает «каждые 15 единиц», 1-5 — диапазон, 1,15 — перечисление. Символ * — любое значение.
Управление заданиями
crontab -l # показать задания текущего пользователя
crontab -e # редактировать
crontab -r # удалить ВСЕ задания (без подтверждения!)
crontab -u www-data -l # задания другого пользователя (нужен root)
Команда crontab -r удаляет весь список без вопроса о подтверждении. Перед правками полезно сохранить вывод crontab -l в файл.
Системные задания лежат отдельно:
| Путь | Назначение |
|---|---|
/etc/crontab |
Системный файл, содержит поле пользователя |
/etc/cron.d/ |
Файлы от пакетов, тоже с полем пользователя |
/etc/cron.daily/ |
Скрипты ежедневного запуска |
/etc/cron.hourly/ |
Скрипты ежечасного запуска |
/var/spool/cron/crontabs/ |
Файлы пользовательских заданий |
В файлах /etc/crontab и /etc/cron.d/* есть дополнительное поле — пользователь, от имени которого выполняется команда. В пользовательских заданиях этого поля нет.
Окружение — главный источник проблем
Cron запускает команды с минимальным окружением: другой PATH, нет переменных из профиля оболочки. Поэтому команда, которая работает в терминале, может не работать в cron.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SHELL=/bin/bash
MAILTO=admin@example.com
0 3 * * * /usr/bin/php /var/www/app/artisan schedule:run >> /var/log/cron-app.log 2>&1
| Строка | Зачем |
|---|---|
PATH |
Чтобы находились программы без полного пути |
SHELL |
Явно указать оболочку |
MAILTO |
Куда отправлять вывод при сбое |
Надёжнее всё же указывать полный путь к программе: /usr/bin/php, /usr/bin/python3, /usr/local/bin/backup.sh. Узнать путь: which php.
Журналирование — обязательная часть
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Конструкция >> файл 2>&1 направляет в лог и обычный вывод, и ошибки. Без неё вывод уходит в почту, которая на сервере обычно не настроена, — и о падении задания никто не узнает.
Чтобы лог не рос бесконечно, добавляют ротацию. Файл /etc/logrotate.d/cron-app:
/var/log/backup.log {
weekly
rotate 8
compress
missingok
notifempty
}
Правильный скрипт для cron
#!/usr/bin/env bash
set -euo pipefail
exec 2>&1
echo "[$(date '+%F %T')] старт"
cd /var/www/app || exit 1
/usr/bin/php artisan queue:work --stop-when-empty
echo "[$(date '+%F %T')] готово"
| Строка | Смысл |
|---|---|
set -euo pipefail |
Остановиться при ошибке, неопределённой переменной или сбое в конвейере |
exec 2>&1 |
Ошибки идут туда же, куда обычный вывод |
cd /var/www/app |
Cron запускает команду из домашнего каталога, а не из каталога проекта |
| Отметки времени | По логу видно, когда задание началось и когда закончилось |
Без set -e ошибка на середине скрипта не остановит выполнение, и скрипт завершится с кодом успеха, не сделав работу. Это самая коварная ошибка в заданиях по расписанию.
Что можно и чего нельзя
Cron подходит для регулярных задач: копирование, очистка, отправка отчётов, обработка очереди. Не подходит для задач, которые должны выполняться строго один раз в определённый момент, и для длительных процессов, которые могут пересечься сами с собой.
Защита от наложения запусков:
*/10 * * * * /usr/bin/flock -n /tmp/task.lock /usr/local/bin/task.sh
Команда flock -n не даст запустить второй экземпляр, пока работает первый. Без этой защиты долгая задача, запускаемая каждые 10 минут, накопит параллельные процессы и исчерпает ресурсы сервера.
systemd-таймеры как альтернатива
В современных системах расписание можно задавать через таймеры systemd. Их преимущества: журналирование через journalctl автоматически, зависимость от других служб, запуск пропущенного задания после включения сервера.
# /etc/systemd/system/backup.timer
[Unit]
Description=Ежедневное резервное копирование
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
systemctl enable --now backup.timer
systemctl list-timers
Параметр Persistent=true — то, чего нет у cron: если сервер был выключен в момент запуска, задание выполнится после включения.
Диагностика
| Команда | Что показывает |
|---|---|
journalctl -u cron --since today |
Что запускал cron |
grep CRON /var/log/syslog |
Записи о запуске заданий |
crontab -l |
Текущий список заданий |
systemctl status cron |
Работает ли сама служба |
Задание, которое не запускается вовсе, обычно означает, что служба cron остановлена или в записи опечатка в поле времени. Задание, которое запускается и не работает, почти всегда связано с окружением или путями.
