Настройка cron-задания: расписание, права и частые ошибки

Опубликовано 17 мин. чтения

Задание отрабатывает в shell, но не в cron. Это руководство разбирает пять полей времени, разницу между пользовательской crontab и /etc/cron.d, ловушку PATH и то, как доказать, что задание действительно выполнилось.

Cron-задание настраивается за пять минут, а потом нередко отнимает часы. Команда прекрасно отрабатывает в shell, из cron не происходит ничего, а в логе в лучшем случае написано, что cron что-то запустил. Эта статья начинается ровно там, где обычные руководства заканчиваются: реальные сообщения об ошибках, различия между дистрибутивами и вопрос о том, по каким признакам видно, что задание действительно доработало до конца, а не просто стартовало.

Все сведения относятся к Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS и Ubuntu 22.04 LTS. Все четыре системы используют один и тот же cron (вариант Vixie Cron от Debian), а не cronie, как Red Hat и Fedora. Дальше, в разделе про часовой пояс, это различие окажется решающим.

Запущена ли вообще служба cron?

В полных установках cron присутствует. В минимальных облачных образах, базовых образах контейнеров и урезанных netinst-установках пакета регулярно нет. Это первый вопрос, который стоит выяснить, прежде чем что-то отлаживать.

command -v crontab
systemctl status cron

Намеренно command -v crontab, а не command -v cron: сама служба лежит в /usr/sbin, а этот каталог в Debian входит только в путь поиска пользователя root. Обычный пользователь просто не получит там никакого вывода, хотя cron установлен. В Ubuntu 24.04 запрос работает и без привилегий. crontab, наоборот, везде лежит в /usr/bin и виден каждому, а systemctl status cron и без того отвечает на более важный вопрос: работает ли служба.

Если службы нет, доустановите её:

apt-get update
apt-get install -y cron
systemctl enable --now cron

В Debian и Ubuntu служба называется cron, а не crond. Тот, кто привык к Red Hat, получит здесь Unit crond.service could not be found. и будет искать не в том месте.

В обычной работе перезапускать ничего не требуется: cron в Debian следит за каталогами crontab через inotify и сам подхватывает изменения. То есть после правки crontab перезапуск не нужен. Исключения: смена системного часового пояса и изменения в /etc/default/cron.

Пять полей и ловушка в пятом

Каждая строка начинается с пяти полей времени, за ними следует команда.

ПолеДиапазонПримечание
Минутаот 0 до 59
Часот 0 до 2324-часовой формат, без часового пояса
День месяцаот 1 до 31
Месяцот 1 до 12также от jan до dec
День неделиот 0 до 70 и 7 оба обозначают воскресенье, также от sun до sat
30 4 * * *      /usr/local/bin/backup.sh      # ежедневно в 04:30
*/10 * * * *    /usr/local/bin/check.sh       # каждые 10 минут
0 2 * * 0       /usr/local/bin/weekly.sh      # по воскресеньям в 02:00
15 3 1 * *      /usr/local/bin/monthly.sh     # 1-го числа в 03:15
0 9-17 * * 1-5  /usr/local/bin/business.sh    # по будням ежечасно с 9 до 17

Две детали почти всегда понимают неправильно.

День месяца и день недели связаны через ИЛИ, а не через И. Как только оба поля ограничены, то есть ни в одном из них не стоит звёздочка, задание выполняется, если подходит хотя бы одно из двух. 0 3 13 * 5 означает не «пятницу 13-го», а «каждое 13-е число и вдобавок каждую пятницу». Если вам нужна именно пятница 13-го, проверяйте это внутри самого скрипта.

Шаг делит интервал неравномерно. */7 * * * * срабатывает на минутах 0, 7, 14, 21, 28, 35, 42, 49 и 56, после чего счётчик переходит к следующему часу. Между 56 и 0 остаётся всего четыре минуты. Это касается любого интервала, который не делит нацело 60 или, соответственно, 24. Для «каждые 90 минут» аккуратной записи в cron не существует, здесь лучше взять systemd-таймер.

Как правильно пользоваться crontab -e

Пользовательскую crontab никогда не правят напрямую в /var/spool/cron/crontabs/, только через штатную утилиту:

crontab -e

При первом вызове программа сообщает no crontab for root - using an empty one. На свежеустановленных системах дальше предлагается выбрать редактор. Если редактора нет вообще, например в урезанном образе, вызов прерывается сообщением вроде /usr/bin/sensible-editor: 25: editor: not found. Решение: установить редактор или явно задать нужный.

apt-get install -y nano
EDITOR=nano crontab -e

Главное преимущество crontab -e перед прямой записью файла: проверка при сохранении. Если строка сломана, вы увидите:

"/tmp/crontab.7hK2mn/crontab":3: bad minute
errors in crontab file, can't install.
Do you want to retry the same edit? (y/n)

Номер строки указан верно, а вот поле в сообщении не всегда: bad minute появляется и тогда, когда просто пропущено одно поле, потому что парсер читает всё со сдвигом влево. При успехе процесс завершается строкой crontab: installing new crontab. Только она означает, что изменение принято.

Ещё несколько команд, которые стоит знать:

crontab -l                         # показать
crontab -l > /root/crontab.bak     # сохранить
crontab -u www-data -l             # прочитать чужую crontab (от root)
crontab -i -r                      # удалить, с подтверждением (только в терминале)

Замечание про резервную копию: crontab -l > /root/crontab.bak выводит no crontab for root и возвращает код 1 до тех пор, пока у пользователя вообще нет crontab. Целевой файл при этом всё равно создаётся, размером 0 байт. При чтении статьи по порядку это незаметно, а вот в скрипте, который проверяет код возврата или перезаписывает более старую копию, очень даже заметно.

crontab -r без -i удаляет всю crontab сразу и без вопросов. Клавиши r и e на клавиатуре расположены рядом, так что потеря данных здесь вполне реальна. Приучите себя в терминале к crontab -i -r и делайте копию заранее через crontab -l.

Важно здесь слово «терминал». crontab -i -r — сугубо интерактивная команда. Если стандартный ввод не подключён к терминалу, то есть в скрипте, в cron-задании или в однострочнике вроде ssh host "crontab -i -r", запрос уходит в бесконечный круг: crontab: really delete root's crontab? (y/n) Please enter Y or N: Please enter Y or N: ... повторяется без остановки и за секунды пишет сотни килобайт вывода, а прервать вызов можно уже только извне. Поэтому для всего автоматического берите неинтерактивный вариант и сохраняйте копию заранее:

crontab -l > /root/crontab.bak     # сохранить перед удалением
crontab -r                         # удалить без подтверждения
printf "y\n" | crontab -i -r       # оставить подтверждение, подставить ответ

Если существуют /etc/cron.allow или /etc/cron.deny, именно они решают, кому вообще разрешено создавать crontab. Затронутые пользователи получают: You (username) are not allowed to use this program (crontab). Если файл /etc/cron.allow есть, он действует эксклюзивно: любой не перечисленный в нём пользователь заблокирован.

Пользовательская crontab, /etc/crontab и /etc/cron.d

Расписания могут храниться в четырёх местах, и форматы у них разные. Именно здесь возникают ошибки, которые труднее всего найти.

Пользовательская crontab: пять полей

Ведётся через crontab -e, выполняется от имени того пользователя, которому принадлежит, и поля пользователя в ней нет. Если всё же вписать его туда, cron попытается выполнить имя пользователя как команду, и в письме или в логе окажется:

/bin/sh: 1: root: not found

/etc/cron.d: шесть полей

Файлы в /etc/cron.d/ — системные crontab, и между полями времени и командой у них стоит поле пользователя. Это правильное место для заданий, относящихся к приложению или к системе управления конфигурацией, потому что каждый файл можно заменять по отдельности.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

0 3 * * * root /usr/local/bin/backup.sh >> /var/log/kh-backup.log 2>&1

Если забыть поле пользователя, cron примет первое слово команды за имя пользователя, и строка молча провалится, потому что такого пользователя не существует.

Три правила для /etc/cron.d/ нарушают регулярно:

  • В имени файла не должно быть точки. Разрешены исключительно буквы, цифры, подчёркивания и дефисы. backup.cron или kh-backup.sh игнорируются без единого сообщения. Причина в управлении пакетами: так остатки вроде .dpkg-dist или .dpkg-old никогда не выполнятся. Назовите файл просто kh-backup.
  • Права и владелец должны быть верными. Ожидаются root:root и режим 0644. Иначе в логе появляются сообщения вроде (*system*) WRONG FILE OWNER, (*system*) BAD FILE MODE или указание на небезопасный режим, доступный на запись группе или всем остальным. Задание в этом случае не выполняется.
  • Файл должен заканчиваться переводом строки. cron требует, чтобы каждая запись завершалась символом newline. Если последняя строка обрывается без перевода, она пропускается, причём без единого сообщения: в контрольной проверке с точно таким же файлом, но без завершающего перевода строки, задание не отработало ни разу, ни ошибки, ни строки в логе. Тот, кто создаёт файл через echo -n, через printf без завершающего \n или из шаблона без пустой строки в конце, теряет ровно последнее задание.
chown root:root /etc/cron.d/kh-backup
chmod 0644 /etc/cron.d/kh-backup

/etc/crontab и каталоги cron.*

У /etc/crontab тоже шесть полей, и этот файл принадлежит дистрибутиву. Меняйте его, только если понимаете зачем. Через run-parts он вызывает каталоги /etc/cron.hourly, cron.daily, cron.weekly и cron.monthly. Скриптам в них нужен бит выполнения, и точки в имени у них тоже быть не должно. backup.sh в /etc/cron.daily/ не выполнится никогда, а backup выполнится. Проверить это можно без запуска:

run-parts --test /etc/cron.daily

Выводятся только те скрипты, которые run-parts действительно запустил бы. Если вашего в списке нет, дело в имени или в бите выполнения.

Почему PATH в cron другой

Это с большим отрывом самая частая причина ситуации «в shell работает, а в cron нет». cron не запускает login-shell. Не читаются ни ~/.bashrc, ни ~/.profile, ни /etc/profile. Для пользовательских crontab cron в Debian задаёт минимальный путь поиска:

PATH=/usr/bin:/bin

В нём нет /usr/local/bin и /usr/sbin. В Debian 12, Debian 13, Ubuntu 22.04 и Ubuntu 24.04 /usr/bin и /usr/sbin по-прежнему остаются разными каталогами, usr-merge затронул только /bin и /sbin. Всё, что вы сами положили в /usr/local/bin, всё из pip install, всё из node под nvm и системные утилиты вроде ufw или iptables для cron попросту не существуют. Сообщение об ошибке будет таким:

/bin/sh: 1: backup.sh: not found

Вторая часть ловушки: cron задаёт SHELL=/bin/sh. В Debian и Ubuntu /bin/sh — ссылка на dash, а не на bash. Любая конструкция bash в заголовке скрипта или прямо в строке crontab ломается:

/bin/sh: 1: [[: not found
/bin/sh: 1: source: not found
/bin/sh: 1: Syntax error: "(" unexpected

Три меры противодействия, именно в таком порядке:

  1. Использовать абсолютные пути. /usr/local/bin/backup.sh вместо backup.sh, /usr/bin/php вместо php. Это самый надёжный вариант, потому что он не зависит ни от одной переменной окружения. Но путь именно считывайте, а не набирайте по памяти: command -v date в Debian 13, Debian 12, Ubuntu 24.04 и Ubuntu 22.04 выдаёт /usr/bin/date, а в более старых системах вроде Debian 11 наоборот /bin/date. Строку с неверным путём crontab принимает молча и без предупреждения, она провалится только во время выполнения и опять же тихо. Для контроля выполните один раз crontab -l и один раз env -i /bin/sh -c "/usr/bin/date": неверный путь сразу отзовётся сообщением not found.
  2. Задать PATH и SHELL в начале crontab. Присваивания действуют на все последующие строки. Важно: cron при этом не разворачивает переменные, PATH=$PATH:/opt/bin не работает, пишите путь целиком.
  3. Использовать скрипт-обёртку. Строка crontab вызывает только скрипт, а собственное окружение скрипт настраивает себе сам.
#!/bin/bash
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd /srv/app
exec ./do-the-work.sh

Если хотите узнать, что cron действительно передаёт вашему заданию, попросите его это показать. Впишите на одну минуту:

* * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1

Только этот путь показывает окружение, которое cron задаёт на самом деле. Оболочка, собранная через env -i, лишь приближается к нему, потому что приносит с собой собственный путь по умолчанию. После этого сравните /tmp/cron-env.txt со своим интерактивным окружением. В глаза бросаются обычно не только PATH и SHELL, но и отсутствующие переменные локали. Без LANG всё работает в локали C, а это меняет порядок сортировки, форматы даты и вывод символов вне ASCII. Скрипты, рассчитанные на LANG=de_DE.UTF-8, ведут себя в cron иначе. Отсутствует и ssh-agent, поэтому заданиям с доступом по SSH нужен ключ без парольной фразы и явное -i.

Последняя мелкая подлость в этой категории: знак процента. В строке crontab неэкранированный % превращается в перевод строки, а всё, что идёт после него, попадает команде на стандартный ввод. Значит, форматы даты нужно экранировать.

0 2 * * * /usr/bin/tar -czf /backup/web-$(date +\%F).tar.gz /var/www

Перенаправление вывода, письма и «No MTA installed»

По умолчанию всё, что задание выводит в stdout или stderr, cron отправляет письмом владельцу crontab. На сервере без почтовой системы это оседает в логе:

(CRON) info (No MTA installed, discarding output)

Это не ошибка задания. Строка означает лишь, что вывод появился, а принять его было некому. Молчаливое задание такой строки не порождает. Поэтому она даже полезна как сигнал: если она возникла внезапно, значит, задание начало что-то выводить, чаще всего сообщение об ошибке.

Для перенаправления есть три разумных шаблона:

# всё в лог-файл, включая ошибки
0 3 * * * /usr/local/bin/backup.sh >> /var/log/kh-backup.log 2>&1

# обычный вывод отбросить, ошибки по-прежнему письмом
0 3 * * * /usr/local/bin/backup.sh > /dev/null

# всё в journal, с понятной меткой
0 3 * * * /usr/local/bin/backup.sh 2>&1 | /usr/bin/logger -t kh-backup

Вариант > /dev/null 2>&1 популярен и опасен: он выбрасывает и все сообщения об ошибках. Задание, которое падает уже четыре месяца, выглядит тогда точно так же, как исправно работающее. Если письма вам не нужны, лучше поставьте MAILTO="" в начале crontab и пишите вывод в файл. Если письма нужны на конкретный адрес, задайте MAILTO=alerts@example.org, но для этого потребуется установленная почтовая система.

Собственные лог-файлы в /var/log/ растут без ограничений. Заведите для них небольшое правило в /etc/logrotate.d/, иначе самое успешное cron-задание однажды заполнит накопитель.

По каким признакам видно, что задание действительно отработало

Здесь быстрая инструкция расходится с надёжной проверкой. cron записывает запуск задания. Ни завершение, ни код возврата он не записывает. Строка CMD в логе доказывает только то, что cron запустил оболочку, а не то, что ваш скрипт отработал успешно.

Логи читаются по-разному в зависимости от системы:

journalctl -u cron --since "30 min ago"
journalctl -t CRON --since today

В Debian 12 и Debian 13 это единственный путь, потому что начиная с bookworm rsyslog по умолчанию больше не устанавливается. Файла /var/log/syslog там, как правило, уже нет. Тот, кто ищет его и не находит, ошибочно считает, что задание не запускалось.

В Ubuntu 22.04 и 24.04 rsyslog обычно присутствует, там дополнительно работает:

grep CRON /var/log/syslog

Отдельного файла /var/log/cron.log ни в одной из четырёх версий из коробки нет. Соответствующее правило в /etc/rsyslog.d/50-default.conf закомментировано. Не ищите этот файл, он существует, только если кто-то включил правило сам.

Поэтому чистое подтверждение приходит из самого задания. Пусть скрипт в конце запишет отметку времени и код возврата:

#!/bin/bash
set -euo pipefail
trap 'echo "$(date -Is) kh-backup завершён, exit $?" >> /var/log/kh-backup.log' EXIT
# основная работа

Так у вас появляются три доказательства вместо одного: строка CRON в journal (cron запустил задание), финальная строка в вашем лог-файле (скрипт дошёл до конца) и код возврата (завершился корректно). Задание работает только тогда, когда сходятся все три.

Если задание может выполняться дольше собственного интервала, дополнительно защитите его от наложения. Иначе однажды параллельно стартуют десять экземпляров и уронят сервер:

*/5 * * * * /usr/bin/flock -n /var/lock/kh-sync.lock /usr/local/bin/sync.sh

flock -n завершается сразу, если один экземпляр уже работает. Утилита входит в util-linux и есть во всех четырёх системах.

@reboot и почему systemd чаще всего лучше

С помощью @reboot команду можно выполнить при старте системы. Рядом с ним есть @daily, @hourly, @weekly, @monthly и @yearly, каждый из которых заменяет собой все пять полей времени.

@reboot /usr/local/bin/start-app.sh

Выглядит удобно, но имеет три серьёзные слабости:

  • Момент срабатывания — это не «после загрузки», а «когда стартует cron». Будут ли к этому моменту готовы сеть, база данных или точка монтирования, зависит исключительно от везения. Обычный костыль: поставить перед командой sleep 30, но так проблема лишь откладывается.
  • Перезапуск службы cron заново запускает @reboot. Команда systemctl restart cron, например после обновления пакетов, запустит ваше приложение во второй раз, хотя первый экземпляр ещё работает.
  • Никакого контроля нет. Ни кода возврата, ни перезапуска после сбоя, ни статуса.

Для всего, что должно работать постоянно, нужен systemd-сервис, а не cron-задание. Для периодических задач лучше подходит таймер. Достаточно двух файлов:

[Unit]
Description=KernelHost Backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
[Unit]
Description=KernelHost Backup ежедневно

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=900

[Install]
WantedBy=timers.target

Активируется таймер, а не сервис:

systemctl daemon-reload
systemctl enable --now kh-backup.timer
systemctl list-timers kh-backup.timer

Три решающих преимущества перед cron: Persistent=true догоняет пропущенный запуск после следующей загрузки, а cron молча его пропускает. Код возврата попадает в systemctl status kh-backup.service, то есть результат виден без собственного логирования. А полный вывод доступен через journalctl -u kh-backup.service, без перенаправления и без писем. Путь поиска у systemd, кстати, щедрее, чем у cron, и по умолчанию содержит в том числе /usr/local/bin, но совсем без собственных добавок он всё равно остаётся скудным.

Для задач при старте системы замените @reboot сервисом с явными зависимостями:

[Unit]
After=network-online.target
Wants=network-online.target

Так ваше задание гарантированно стартует только тогда, когда сеть действительно поднялась. Подробнее о юнитах и их устройстве написано в статье создание systemd-сервиса.

Часовой пояс, UTC и переход на летнее время

cron всегда считает по системному часовому поясу из /etc/localtime. На многих серверах и почти во всех облачных образах это UTC. Задание 0 3 * * * отработает тогда летом в 05:00 по центральноевропейскому летнему времени, а не в 03:00. Сначала выясните, с чем вы имеете дело:

readlink -f /etc/localtime
timedatectl show -p Timezone --value
date
date -u

readlink -f /etc/localtime называет путь в /usr/share/zoneinfo, а значит и фактически действующую зону, например /usr/share/zoneinfo/Etc/UTC или /usr/share/zoneinfo/Europe/Vienna. Команда работает во всех четырёх версиях, в том числе там, где systemd не запущен. timedatectl show -p Timezone --value выдаёт ту же информацию коротко и в машиночитаемом виде, но требует systemd.

От чего стоит отвыкнуть, так это от заглядывания в /etc/timezone. Debian 13 этот файл больше не поставляет, cat /etc/timezone заканчивается там сообщением cat: /etc/timezone: No such file or directory, и установка tzdata его не возвращает. В Debian 12, Ubuntu 24.04 и Ubuntu 22.04 он ещё есть, так что ответ различается от версии к версии. Решающей в любом случае остаётся только символьная ссылка /etc/localtime: значение, вписанное вручную в /etc/timezone, не меняет ни системный часовой пояс, ни, соответственно, момент срабатывания ваших заданий.

Системный часовой пояс можно сменить в любой момент, после этого стоит перезапустить cron, чтобы служба гарантированно подхватила изменение:

timedatectl set-timezone Europe/Vienna
systemctl restart cron

На системах без работающего systemd вместо этого задайте символьную ссылку напрямую, результат тот же и сразу проверяется командой readlink -f /etc/localtime:

ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime

Теперь пункт, который во многих руководствах изложен неверно: cron в Debian и Ubuntu не знает переменной CRON_TZ. Она пришла из cronie, то есть из cron в Red Hat, Fedora и AlmaLinux. В Debian 13, Debian 12, Ubuntu 24.04 и Ubuntu 22.04 часового пояса на пользователя или на отдельную crontab не существует. Если вписать там в crontab TZ=Europe/Vienna, это подействует исключительно на окружение выполняемых команд, то есть date внутри скрипта покажет венское время. Сам момент срабатывания это не затрагивает вообще никак, он по-прежнему определяется системным часовым поясом. Кто этого не замечает, получает задание, которое выглядит настроенным правильно и всё равно срабатывает на два часа мимо.

Остаётся три чистых пути: выставить подходящий системный часовой пояс, пересчитать время в UTC самостоятельно или перейти на systemd-таймер, который принимает часовой пояс прямо в расписании. Проверить это можно без риска:

systemd-analyze calendar "Mon..Fri 03:00 Europe/Vienna"

Вывод называет ближайшее срабатывание конкретной датой. Это самая надёжная проверка, которую можно сделать до активации.

И практический совет про летнее время: не планируйте задания между 02:00 и 03:00. В марте этого часа не существует, в октябре он существует дважды. В зависимости от задания это означает пропущенный или сдвоенный запуск, и то и другое случается раз в год и плохо воспроизводится. 01:30 или 03:30 — спокойные альтернативы. В systemd-таймерах дополнительно помогает Persistent=true, тогда пропущенный запуск будет наверстан.

Быстрая диагностика за семь шагов

Если cron-задание не работает, пройдите по этому списку по порядку. Он покрывает практически все случаи.

  1. Служба работает? systemctl status cron. Нет службы, нет и задания.
  2. Запись вообще сохранилась? crontab -l для пользовательских заданий, иначе посмотрите файл в /etc/cron.d/. Проверьте имя файла без точки, режим 0644, владельца root и завершающий перевод строки.
  3. cron его вообще запускал? journalctl -t CRON --since today. Если строки CMD нет, проблема в расписании или в файле, а не в скрипте.
  4. Число полей верное? Пять полей в пользовательской crontab, шесть в /etc/cron.d и /etc/crontab.
  5. Абсолютные пути? Каждую команду и каждый скрипт вписывайте с полным путём, а сам путь определяйте заранее через command -v, а не набирайте по памяти.
  6. Окружение проверено? Один раз впишите * * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1, подождите минуту, сравните.
  7. Тестировать в контексте cron. Запускайте скрипт не в своей оболочке, а с пустым окружением: env -i PATH=/usr/bin:/bin /bin/sh -c '/usr/local/bin/backup.sh'. Путь поиска здесь указывается намеренно. Один только env -i /bin/sh окружение действительно очищает, но dash затем выставляет себе собственный путь по умолчанию /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, а он щедрее, чем у cron. Именно те ошибки, о которых здесь идёт речь, остались бы при этом незамеченными. Если вызов провалится, причина найдена, и ждать следующего срабатывания не придётся.

Последний пункт самый ценный. Почти каждое cron-задание, которое «необъяснимо» не работает, воспроизводимо падает, стоит запустить его с пустым окружением. Так загадка, которая проявляется раз в 24 часа, превращается в ошибку, устранимую за десять секунд.

Тем, кто регулярно снимает резервные копии через cron-задание, стоит дополнительно защитить и целевую сторону. Подходящие рекомендации собраны в нашей статье о защите Linux-сервера.

Частые вопросы

Почему скрипт работает в shell, но не работает как cron-задание?
В подавляющем большинстве случаев дело в окружении. cron не запускает login-shell, то есть не читает ни ~/.bashrc, ни /etc/profile, и для пользовательских crontab задаёт только PATH=/usr/bin:/bin. В нём нет /usr/local/bin и /usr/sbin. Кроме того, SHELL установлен в /bin/sh, а это в Debian и Ubuntu dash, который не понимает синтаксис bash. Протестируйте задание ровно в таком окружении: env -i PATH=/usr/bin:/bin /bin/sh -c, а следом путь к скрипту в одинарных кавычках ('/путь/к/скрипту.sh'), и оно упадёт сразу и воспроизводимо. Путь поиска указывайте явно, потому что просто env -i /bin/sh подставит более щедрый путь по умолчанию от dash и скроет как раз эту ошибку.
Что означает сообщение No MTA installed, discarding output?
Ваше задание что-то вывело в stdout или stderr, cron хотел доставить это письмом, но почтовая система не установлена. На само задание это не влияет, оно вполне могло отработать успешно. Либо перенаправьте вывод в лог-файл, либо поставьте MAILTO="" в начале crontab. Внимание: сообщение часто появляется именно тогда, когда до сих пор молчавшее задание начинает выдавать ошибки.
Поддерживают ли Debian и Ubuntu переменную CRON_TZ?
Нет. CRON_TZ пришла из cronie, то есть из cron в Red Hat и Fedora. Debian 13, Debian 12, Ubuntu 24.04 и Ubuntu 22.04 используют вариант Vixie Cron от Debian и не знают часового пояса на отдельную crontab. Запись TZ= в crontab действует только на окружение команд, но не на момент срабатывания. Либо задайте системный часовой пояс через timedatectl, либо пересчитайте время в UTC, либо возьмите systemd-таймер, который принимает часовой пояс в выражении OnCalendar.
Почему мой файл в /etc/cron.d игнорируется?
Почти всегда из-за имени файла. Разрешены только буквы, цифры, подчёркивания и дефисы, а точка делает файл невидимым для cron. То есть backup.cron игнорируется, а backup нет. Другие причины: неверные права (нужны root:root и 0644), пропущенное поле пользователя между полями времени и командой или последняя строка без завершающего перевода строки.
Где найти логи cron в Debian 12 и 13?
Через journalctl, потому что начиная с Debian 12 rsyslog по умолчанию не устанавливается и файла /var/log/syslog там чаще всего нет. Используйте journalctl -u cron или journalctl -t CRON. В Ubuntu 22.04 и 24.04 дополнительно работает grep CRON /var/log/syslog. Отдельного файла /var/log/cron.log ни в одной из четырёх систем из коробки нет.
Доказывает ли строка CMD в логе, что задание отработало успешно?
Нет. cron записывает только запуск задания, но ни завершение, ни код возврата. Скрипт мог упасть секундой позже, а строка в логе выглядела бы точно так же. Для настоящего подтверждения пусть скрипт сам пишет отметку времени и код возврата в лог-файл, например через trap на EXIT, либо перейдите на systemd-таймер, где systemctl status показывает код возврата.
Что выбрать: @reboot или systemd-сервис?
Почти во всех случаях systemd-сервис. @reboot выполняется не после загрузки, а при старте службы cron, без гарантии, что сеть или база данных уже готовы. Кроме того, он срабатывает заново при каждом systemctl restart cron, например после обновления пакетов. Сервис с After=network-online.target и Wants=network-online.target решает обе проблемы и вдобавок даёт статус и код возврата.

Cron Crontab systemd Администрирование Linux Debian Ubuntu Автоматизация Управление сервером