Cron запускает команды по расписанию. Свое расписание правят командой crontab -e: одна строка равна одному заданию, в ней пять полей времени и сама команда. Описание ниже опирается на справку crontab(5) из документации Ubuntu и подходит для 22.04, 24.04 и 26.04.
Большинство «не работающих» заданий ломается в трех местах. Cron запускает команду в урезанном окружении, не читает ваш профиль. Он по-своему обращается с символом % и с комментариями в строке. А вывод задания никуда не попадает, если на сервере нет почты. Дальше по порядку: где лежат задания, как записать расписание, на чем спотыкаются и как найти причину, когда задание молчит.
Где cron хранит задания
У каждого пользователя свой crontab. Его открывают командой crontab -e, а сам файл лежит в /var/spool/cron/crontabs/. Руками туда лучше не лазить: через команду crontab файл попадает на место правильным путем, и она заодно проверяет синтаксис.
Системные задания записывают в /etc/crontab и в файлы каталога /etc/cron.d/. Формат почти тот же, только между временем и командой стоит имя пользователя, от которого запустится задание. В личном crontab этого поля нет, и это первая вещь, на которой путаются, когда копируют строку из системного файла в личный.
Есть и третий вариант: положить скрипт в /etc/cron.hourly, /etc/cron.daily, /etc/cron.weekly или /etc/cron.monthly. Время запуска для них задает система, указывать расписание не нужно.
| Куда писать | Поле пользователя | Когда подходит |
|---|---|---|
| crontab -e | нет | Задание одного пользователя |
| /etc/crontab, /etc/cron.d/ | да | Системное задание или задание от имени другого пользователя |
| /etc/cron.daily и соседние каталоги | не используется | Скрипт, которому хватает «раз в час, день, неделю или месяц» |
Как устроена строка расписания
Строка состоит из пяти полей времени, разделенных пробелами, и команды:
30 3 * * * /opt/scripts/backup.sh
Здесь скрипт запустится каждый день в 3:30. Поля идут в таком порядке:
| Поле | Допустимые значения |
|---|---|
| Минута | 0-59 |
| Час | 0-23 |
| День месяца | 1-31 |
| Месяц | 1-12 |
| День недели | 0-7, где 0 и 7 обозначают воскресенье |
Внутри полей работают четыре символа: * значит «любое значение», запятая перечисляет (1,15), дефис задает диапазон (9-18), косая черта задает шаг (*/10 означает каждые десять минут). Месяцы и дни недели можно писать тремя первыми буквами, например mon или jan, но только по одному: в диапазонах и списках имена справка не разрешает. Поэтому «с понедельника по пятницу» пишут как 1-5, а не mon-fri.
Вместо пяти полей бывают сокращения. @reboot запускает команду один раз при старте системы, @daily раз в сутки.
Где спотыкаются в синтаксисе
Комментарий в строке с командой. Справка прямо говорит: комментарий нельзя ставить в одну строку с командой или с переменной окружения, он станет частью команды. Для переменной это ломает значение: после PATH=/usr/bin # свой путь в PATH окажется и хвост. В строке с командой комментарий оболочка обычно проглотит, но если в нем встретится процент, cron оборвет команду раньше времени. Пишите комментарии отдельной строкой, которая начинается с #.
Символ процента. Cron считает первый неэкранированный % концом команды, а все, что после него, подает команде на стандартный ввод. Поэтому date +%F в crontab не работает, нужно date +\%F.
День месяца и день недели. Если заполнены оба поля, команда срабатывает при совпадении любого из них, а не обоих сразу. Строка 30 4 1,15 * 5 запустится в 4:30 первого и пятнадцатого числа и еще в каждую пятницу. Если нужно «только по пятницам, которые выпали на 1 или 15», условие придется проверять внутри самой команды.
Смена времени. Задания, попавшие в пропущенный при переводе часов интервал, сработают вскоре после него. Задания в повторяющемся часе выполнятся один раз, если только они и так не идут каждую минуту или каждый час. Если в часовом поясе сервера часы не переводят, этот пункт можно пропустить.
Как создать задание и проверить, что оно работает
Начните с проверки службы:
systemctl status cron
Если такой службы нет, cron не установлен. На минимальных образах Ubuntu так бывает, ставится он командой sudo apt install cron.
Дальше откройте свой crontab командой crontab -e. При первом запуске система предложит выбрать редактор. Допишите строку с заданием, сохраните файл. Перезапускать cron после правки не нужно, он подхватывает изменения сам. Список своих заданий показывает crontab -l, а чужой crontab открывают от root: sudo crontab -u deploy -e.
Прежде чем ставить настоящее расписание, прогоните тестовое. Добавьте строку, которая каждую минуту дописывает дату в файл:
* * * * * date >> /tmp/cron-test.log 2>&1
Подождите пару минут, прочитайте /tmp/cron-test.log и сразу удалите тестовую строку. Если записи появились, сам cron работает, а дальше вопрос только в вашей команде.
Пять примеров расписания
| Строка | Когда сработает | На что смотреть |
|---|---|---|
30 3 * * * /opt/scripts/backup.sh |
Каждый день в 3:30 | Скрипт должен быть исполняемым (chmod +x), путь полный |
*/15 * * * * /opt/scripts/check.sh |
Каждые 15 минут, в 0, 15, 30 и 45 минут часа | Шаг считается от начала часа |
0 9 * * 1-5 /opt/scripts/report.sh |
В 9:00 с понедельника по пятницу | Диапазон цифрами, не mon-fri |
30 4 1,15 * 5 /opt/scripts/clean.sh |
В 4:30 первого и пятнадцатого числа и по пятницам | Дни месяца и недели работают как «или» |
0 2 * * * tar -czf /backups/www-$(date +\%F).tar.gz /var/www |
Каждый день в 2:00, архив с датой в имени | Процент экранирован |
Окружение: что cron задает сам
Команда, которая спокойно работает в терминале, из cron часто падает. Причина почти всегда в окружении: cron не читает ~/.bashrc и профиль, а выдает заданию минимальный набор.
| Переменная | Значение по умолчанию |
|---|---|
| SHELL | /bin/sh |
| LOGNAME, HOME | Из записи владельца crontab в /etc/passwd |
| PATH | /usr/bin:/bin |
Отсюда два следствия. Первое: программы из /usr/local/bin, /sbin или из окружений вроде pyenv и nvm cron не найдет. Пишите полные пути, а если команд много, задайте PATH в начале crontab. Переменные действуют только на строки, которые стоят ниже них.
PATH=/usr/local/bin:/usr/bin:/bin
30 3 * * * backup.sh
Второе: на Ubuntu /bin/sh это dash, и конструкции вроде [[ ... ]] в строке crontab не сработают. Либо переписывайте их под обычный sh, либо добавьте в начало файла SHELL=/bin/bash.
Чтобы увидеть задание глазами cron, запустите его в похожем окружении:
env -i SHELL=/bin/sh HOME="$HOME" LOGNAME="$USER" PATH=/usr/bin:/bin /bin/sh -c '/opt/scripts/backup.sh'
Если скрипт падает здесь, а в обычном терминале нет, причина в окружении.
Куда уходит вывод и где искать логи
Все, что задание напечатало, cron отправляет письмом владельцу crontab. Адресата меняет переменная MAILTO, а MAILTO="" письма отключает. Подвох в том, что без почтовой системы на сервере письмо уйти не может: в журнале появляется запись No MTA installed, discarding output, и вывод пропадает вместе с текстом ошибки.
Поэтому вывод перенаправляют в файл сами:
*/10 * * * * /opt/scripts/sync.sh >> /var/log/sync.log 2>&1
Порядок важен: сначала >> в файл, потом 2>&1, чтобы ошибки шли туда же. Пользователь, от имени которого работает задание, должен иметь право писать в этот каталог.
Отдельно проверьте, запускал ли cron задание вообще:
journalctl -u cron.service --since today
grep CRON /var/log/syslog
В записи будет примерно такая строка: CRON[12345]: (root) CMD (/opt/scripts/backup.sh). Она говорит только о том, что cron запустил команду. Получилось ли у команды что-то сделать, видно лишь в вашем собственном логе.
Файла /var/log/cron, как в CentOS, в Ubuntu нет. Бывает и так, что нет /var/log/syslog: на минимальных образах не установлен пакет rsyslog. Тогда либо ставят его (sudo apt install rsyslog), либо читают журнал через journalctl, как выше. Отдельный cron.log по умолчанию тоже не создается: в стандартной настройке rsyslog строка для cron закомментирована, ее можно включить и перезапустить rsyslog.
Задание не работает: что проверить
Идите сверху вниз: первые строки отсекают самые частые причины.
| Что видите | Вероятная причина | Что проверить |
|---|---|---|
| Команда работает в терминале, из cron нет | Другое окружение: PATH, относительные пути, [[ ]] под sh | Полные пути, PATH и SHELL в crontab, запуск через env -i |
| В журнале нет записи о запуске | Строка не принята, служба не работает, задание в другом crontab | systemctl status cron, crontab -l, journalctl -u cron.service |
| Запись о запуске есть, результата нет | Ошибка в самой команде, вывод потерян | Перенаправление >> файл 2>&1, права на каталог лога |
| Запись с датой в имени файла не создается | Неэкранированный % | Замените % на \% |
| Сработало не в то время или не в те дни | Правило «или» для дней, часовой пояс, смена времени | Поля дня месяца и недели, timedatectl |
| Нет /var/log/syslog | Не установлен rsyslog | journalctl -u cron.service или установка rsyslog |
| Копии задания накладываются друг на друга | Задание дольше интервала | Блокировка через flock, см. ниже |
Как не запустить задание дважды
Если скрипт работает дольше интервала между запусками, cron стартует вторую копию, пока не закончилась первая. Для резервных копий и синхронизации это обычно плохо. Защищает утилита flock:
*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /opt/scripts/sync.sh
Ключ -n означает «не ждать». Если блокировка занята, flock сразу завершается с кодом 1, и вторая копия не стартует. Файл блокировки создается сам, если его нет. Держите его на локальном диске: на сетевых файловых системах вроде NFS и CIFS flock может не работать.
Итоги
Cron хорош для простого случая: одна команда в заданное время. Новое расписание сначала проверяйте на коротком интервале с записью в лог, и только потом ставьте настоящее. Когда нужно догнать запуск, пропущенный из-за простоя сервера, или дождаться другой службы, смотрите в сторону systemd timers: у них для этого есть свои средства.
Где отладить расписание, не трогая рабочий сервер
Если заданиями управляет компания, расписание удобнее сначала проверить на отдельной машине. В LanCloud для этого можно взять облачный сервер Cloud Server на Linux: для компаний первый месяц тестирования бесплатный.

