SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Почему не выполняется задача в cron: 5 частых причин

Ваша задача cron завершается с ошибкой сразу после запуска. Узнайте, как исправить минимальный PATH, экранировать знак процента и где искать логи, если вывод уходит в почту.

Почему ваша задача cron не выполняется

Если задача cron «никогда не выполняется», почти всегда она на самом деле запускается. Она выполняется в окружении, которое отличается от вашей оболочки, завершается с ошибкой в первую же секунду, а сообщение об этом отправляется туда, где вы его не читаете. Почти все подобные случаи объясняются пятью причинами: путь поиска (search path), знак процента, неверный файл crontab, вывод, направленный в почту, и скрипт, ожидающий сеанс входа в систему.

cron — это демон (фоновая служба), который считывает файлы crontab и запускает команды по расписанию. Он не читает ваш .bashrc, не открывает терминал, не запускает оболочку входа (login shell) и не уведомляет вас о сбоях команд. Каждая из причин ниже вытекает из этих четырех фактов.

Разбирайте их по порядку, начав с вопроса, который лежит в основе всех остальных: сработал ли cron вообще? «cron не запустил задачу» и «задача запустилась и завершилась» — это разные проблемы, у которых нет ничего общего, поэтому сначала ответьте на этот вопрос.

Сработал ли cron вообще?

Демон имеет разное имя юнита в зависимости от семейства дистрибутивов. Проверьте оба варианта, а затем изучите лог.

systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"

В Debian и Ubuntu этот юнит называется cron. В Fedora, Rocky и Alma он называется crond. На конкретной машине существует только одно из этих имен, поэтому сообщение об ошибке для одного из них — это нормальное поведение, а не неисправность.

Изучите записи, созданные вашей системой. Не ищите строку, скопированную из руководства, так как формулировки различаются в зависимости от реализации cron и настроек логирования. Вы проверяете только два факта: есть ли запись в минуту, указанную в вашем расписании, и содержит ли эта запись вашу команду. Наличие записи с вашей командой означает, что cron выполнил свою часть работы, а ошибка кроется внутри самой команды. Отсутствие записи означает, что cron не получил ваше расписание, что соответствует причине 3 ниже.

Некоторые образы направляют сообщения cron через rsyslog в файл, а не в journal. Проверьте /var/log на наличие файла с именем cron или syslog, а затем прочитайте его содержимое в конце.

ls -l /var/log
sudo tail -n 50 /var/log/syslog

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

dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron

Причина 1: у cron отсутствует ваш PATH

Ваша интерактивная оболочка формирует PATH на основе /etc/profile, ~/.profile, ~/.bashrc и всех файлов, которые они подключают. Ни один из них не выполняется при запуске задания cron. cron запускает команду в собственной ограниченной среде, поэтому программа, расположенная вне стандартных системных директорий, не будет найдена. Это касается всего, что находится в /usr/local/bin, /opt, менеджеров версий языков программирования, виртуальных окружений Python или рабочих пространств Go. Задание завершается ошибкой на первой же строке, а оболочка выводит сообщение о том, что команда не найдена; точная формулировка зависит от используемой оболочки.

Найдите полный путь к каждой команде, которую использует ваше задание.

command -v docker
command -v node
readlink -f "$(command -v node)"

Затем либо укажите эти абсолютные пути непосредственно в задании, либо один раз задайте PATH в начале crontab.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool run

Получите этот список на своей машине с помощью echo "$PATH" и удалите всё, что существует только в рамках интерактивной сессии. Важно помнить одно правило: cron не раскрывает переменные в этих строках присваивания. PATH=$PATH:/usr/local/bin сохранит буквальный текст $PATH:/usr/local/bin, поэтому в итоге задание получит путь поиска, не содержащий ни одной рабочей директории. Указывайте весь список целиком.

Менеджеру версий недостаточно просто пути. nvm, pyenv, rbenv и asdf устанавливают функцию оболочки или директорию с прослойками (shims) из вашего .bashrc, а задание cron никогда не читает этот файл. Вызывайте версию бинарного файла по абсолютному пути или подключайте скрипт инициализации менеджера первой строкой вашего собственного скрипта.

Причина 2: знак процента завершает команду

В поле команды crontab символ % не является обычным символом. Первый неэкранированный % завершает команду. Всё, что следует за ним, передаётся команде в качестве стандартного ввода, а каждый последующий % превращается в символ новой строки. Это штатная функция cron для передачи коротких данных в программу, и именно поэтому запись с датой в имени файла — классический пример нерабочей строки crontab.

Напишите 0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site, и tar никогда не увидит отформатированную дату. cron обрезает строку на первом %, поэтому оболочка получает незавершённую подстановку команды, а остальная часть строки поступает как стандартный ввод. Экранируйте каждый знак процента обратной косой чертой.

0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site

Эту строку последовательно обрабатывают два уровня. \% — это правило cron, применяемое до запуска чего-либо. $(date +\%F) — это подстановка команды, применяемая позже оболочкой, которую запускает cron. Понимание того, какой уровень отвечает за конкретный символ, — это и есть главный секрет.

Более безопасная практика — полностью выносить логику из crontab. Поместите её в скрипт, где знак процента не имеет специального значения.

#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site

Тогда строка crontab будет содержать только путь и перенаправление. Crontab, который легко читается с первого взгляда, — это crontab, который легко отлаживать.

Cause 3: which crontab did you edit?

There is no single crontab. There are several files, with different owners and different field counts, and a job written into the wrong one is invisible.

  • crontab -e edits the crontab of the user who runs the command. sudo crontab -e edits root's. Two people debugging the same box often end up reading two different files.
  • sudo crontab -l -u deploy lists another user's crontab, which is how you confirm what is actually installed for the account that should run the job.
  • /etc/crontab and every file in /etc/cron.d carry one extra field between the schedule and the command: the user to run as. Paste a five-field user crontab line into /etc/cron.d and the first word of your command is read as a username.
  • Files in /etc/cron.d must be named with letters, digits, underscores and hyphens. A file called backup.sh or site.conf is skipped because of its name alone. Rename it to backup and check your log again.
  • Files in /etc/cron.d should be owned by root, and must not be writable by group or others. ls -l /etc/cron.d shows you both facts at once.
  • Scripts dropped into /etc/cron.daily and its siblings follow the same naming rule and must also carry the execute bit. A missing execute bit is a silent skip.
  • /etc/cron.allow and /etc/cron.deny decide who may install a crontab at all. If either exists on your box, read it before assuming your user is allowed one.

Install a user crontab with the crontab command instead of editing the spool file by hand, because crontab parses the file before installing it. When you save, read what the command prints back to you. If it refuses the file, the previous version stays live and your change never took effect, which looks exactly like cron ignoring you.

The owner also decides permissions. A job in root's crontab creates root-owned files that the application reading them may not be able to write. A job in a normal user's crontab cannot read a root-only directory. Match the owner to the work: application maintenance belongs to the application's own account, which is the reasoning behind replacing WordPress wp-cron with a system cron job. The mode of the files your job creates comes from the umask it inherits, and that is another value which is not your shell's, so how umask sets file permissions is worth reading if a job's output lands unreadable.

Причина 4: вывод отправляется на почту, которую никто не читает

cron собирает всё, что задача записывает в стандартный вывод (stdout) и стандартный поток ошибок (stderr). Если задача вывела хоть что-то, cron передает этот текст локальной почтовой системе, адресуя его владельцу crontab или тому, что указано в MAILTO. На минималистичных VPS обычно нет установленного MTA (mail transfer agent), поэтому сообщение никуда не доставляется. Ваша ошибка существовала мгновение и исчезла. Именно поэтому неисправная задача выглядит так, будто она ничего не делает.

Перенаправьте вывод в файл, который вы контролируете.

0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

>> добавляет стандартный вывод в конец файла. 2>&1 направляет стандартный поток ошибок туда же, куда в данный момент направлен стандартный вывод, поэтому этот параметр должен идти после перенаправления. Если записать это в обратном порядке, как 2>&1 >> file, стандартный поток ошибок сохранит свое исходное назначение, и ошибка, которую вы ищете, как раз окажется той частью, которая никогда не попадет в файл.

Журнал (journal) — еще одна подходящая цель. logger записывает данные в syslog с выбранным вами тегом.

0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-site

Прочитайте его с помощью journalctl -t backup-site. Это позволяет хранить вывод задачи рядом с записями cron, что упрощает отслеживание хронологии событий. Если вам также нужен журнал того, кто и какую команду запускал на сервере, это задача другой системы, и аудит команд пользователей на вашем сервере описывает её.

MAILTO="" в начале crontab отключает отправку почты для всех задач ниже. Установка MAILTO на реальный адрес поможет только при наличии работающего MTA, поэтому убедитесь, что почта действительно уходит с сервера, прежде чем полагаться на этот метод.

Одно правило при отладке: никогда не добавляйте > /dev/null 2>&1. Это самая популярная строка в любом crontab, и она уничтожает единственные улики, которые у вас есть. Верните её позже, когда задача будет работать корректно.

Причина 5: скрипт ожидает переменные окружения, которые cron не предоставляет

После того как команда найдена и её вывод получен, остаются все остальные параметры, которые ваша сессия предоставляет автоматически.

  • Оболочка (shell) может быть не bash. Проверьте это с помощью ls -l /bin/sh. В Debian и Ubuntu она указывает на dash, поэтому проверка с двойными скобками, массивы и source завершаются с синтаксической ошибкой. Добавьте в скрипт строку #!/bin/bash и вызывайте его через неё или установите SHELL в начале crontab.
  • Рабочая директория отличается от той, в которой вы находились. Используйте везде абсолютные пути или переходите в нужную директорию с помощью cd в первой строке скрипта. Относительный путь — самая частая причина, по которой задание «работает, когда я запускаю его вручную».
  • Локаль отличается от локали вашей сессии. Любая команда, форматирующая дату, число или сортирующая текст, может выдавать другой результат при ином значении LANG. Если последующий этап парсит этот вывод, установите локаль прямо в скрипте, вместо того чтобы полагаться на настройки по умолчанию.
  • Отсутствует TTY (терминал). Команда, запрашивающая подтверждение, открывающая редактор или отображающая индикатор выполнения, может зависнуть или завершиться с ошибкой. Добавьте флаг неинтерактивного режима, если утилита его поддерживает.
  • Отсутствует SSH-агент. SSH_AUTH_SOCK отсутствует в окружении cron, поэтому команды ssh или rsync, которые работали благодаря загруженному агенту, теперь не проходят аутентификацию. Создайте для задания отдельный ключ, владельцем которого является пользователь, выполняющий задание.
  • Отсутствует шина пользовательской сессии, поэтому systemctl --user из cron завершается ошибкой, пока не будет установлена переменная XDG_RUNTIME_DIR. Использование системного юнита — более правильное решение.

В Fedora, Rocky и Alma есть еще один подозреваемый. SELinux ограничивает работу cron-заданий, поэтому при обращении к пути с неожиданной меткой доступ будет запрещен, даже если права доступа к файлу выглядят корректно. Проверьте наличие отказов в доступе с помощью sudo ausearch -m avc -ts recent и ознакомьтесь с основами SELinux для сервера, прежде чем что-либо отключать.

Минутная проверка для вывода окружения cron

Перестаньте гадать, какие переменные окружения доступны в cron, и просто прочитайте их. Создайте скрипт, который выводит все параметры, запланируйте его выполнение каждую минуту, подождите, а затем прочитайте файл.

cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.sh

Добавьте одну строку в crontab пользователя, от имени которого выполняется целевая задача. Используйте абсолютные пути для всех элементов.

* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1

Подождите одну минуту, затем прочитайте /home/deploy/cron-probe.log и сравните результат с выводом тех же команд в вашей текущей оболочке. Строка PATH, рабочий каталог и локаль обычно сами по себе объясняют причину сбоя. Обратите внимание на две детали настройки: знаки процента находятся внутри скрипта, где правила cron не действуют, а путь к лог-файлу доступен для записи пользователю, от имени которого запускается задача.

Удалите эту строку из crontab сразу после получения данных. Задача, которая выполняется каждую минуту и дописывает данные в файл, быстро заполнит небольшой диск, причем сделает это незаметно.

Это тот график, который вы имели в виду?

Строка в пользовательском crontab состоит из пяти полей: минута, час, день месяца, месяц, день недели. Два из них взаимодействуют способом, который часто удивляет пользователей.

Когда и день месяца, и день недели ограничены, то есть ни одно из них не является *, cron запускает задачу, когда совпадает любое из этих полей. 0 0 13 * 5 — это не «пятница 13-е». Задача будет выполняться в полночь 13-го числа каждого месяца, а также в полночь каждую пятницу. Чтобы выбрать один конкретный день, оставьте одно из двух полей как *, а проверку второго выполните внутри самого скрипта.

cron использует системный часовой пояс. Многие образы VPS поставляются с установленным UTC (всемирное координированное время), поэтому задача, запланированная на 03:00, запустится в 03:00 UTC, что может соответствовать середине вашего дня. timedatectl выводит часовой пояс, который фактически использует ваш сервер. Проверьте его, вместо того чтобы предполагать, что он совпадает с настройками вашего ноутбука.

Стоит знать еще о двух ловушках планировщика. @reboot срабатывает в момент запуска самого cron, что не совпадает с моментом готовности сети. Поэтому задача, требующая DNS или доступа к удаленному хосту, может завершиться ошибкой при загрузке, но успешно выполняться при каждом последующем ручном запуске. Кроме того, ничто не мешает медленной задаче запуститься снова, пока предыдущая копия еще выполняется. Оберните её в блокировку.

*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

flock -n немедленно завершает работу, если блокировка уже установлена, поэтому параллельные запуски прекращаются, вместо того чтобы накапливаться поверх первого процесса.

Когда systemd timer подходит лучше

cron хорош в одном: запуск команды в заданное время. В остальном он слаб. Таймер предоставляет логи в journal без перенаправления вывода, код завершения, который можно проверить позже, порядок запуска относительно network-online.target и случайную задержку, чтобы сотни серверов не начинали работу в одну и ту же секунду. Если вашей задаче требуется что-то из этого, настройка systemd service и timer на VPS потребует меньше усилий, чем поддержка строки в crontab. Логика повторных попыток также относится к таймерам, так как политики перезапуска systemd определяют действия после сбоя, в то время как у cron нет решения для подобных задач.

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

FAQ

Почему моя задача cron работает при ручном запуске, но не работает в cron?

Потому что окружение вашей оболочки и окружение cron различаются. Ваша интерактивная оболочка считывает /etc/profile и ~/.bashrc, которые устанавливают PATH, локаль и переменные агента. cron запускает команду без этих настроек, из другой рабочей директории и иногда с использованием другой оболочки. Используйте абсолютные пути для всех команд, задавайте необходимые переменные в начале crontab или внутри скрипта. Запланируйте тестовую задачу на одну минуту, которая записывает вывод env | sort, pwd и id в лог-файл: так вы увидите реальное окружение cron, вместо того чтобы гадать.

Как проверить, действительно ли cron выполнил мою задачу?

Изучите лог демона. Используйте journalctl -u cron в Debian и Ubuntu или journalctl -u crond в Fedora, Rocky и Alma; некоторые образы перенаправляют сообщения через rsyslog в файл в директории /var/log. Ищите запись, соответствующую минуте вашего расписания, и убедитесь, что в ней указана ваша команда. Отсутствие записи означает, что cron не получил расписание — проверьте, тот ли crontab вы редактировали. Запись без результата означает, что команда запустилась и завершилась с ошибкой; перенаправьте её вывод в файл для анализа.

Почему date +%Y вызывает ошибку внутри crontab?

cron воспринимает % как специальный символ в поле команды. Первый неэкранированный % завершает команду, всё, что следует за ним, передаётся в стандартный ввод, а каждый последующий % превращается в символ переноса строки. Поэтому имя файла с форматированием даты не доходит до программы. Экранируйте каждый знак процента как \% или вынесите команду в отдельный скрипт и вызывайте его из cron, так как внутри скрипта знак процента не имеет специального значения.

Куда уходит вывод моей задачи cron?

В локальную почтовую систему, на адрес владельца crontab или на адрес, указанный в MAILTO. В большинстве образов VPS почтовый агент (MTA) не установлен, поэтому сообщение отбрасывается, и кажется, что задача ничего не выводит. Перенаправьте вывод в файл с помощью >> /path/to/log 2>&1 (соблюдайте порядок, чтобы стандартный поток ошибок следовал за стандартным выводом) или передайте его через logger -t myjob и читайте с помощью journalctl -t myjob. Не используйте > /dev/null 2>&1, пока продолжаете отладку.

Что использовать: cron или systemd timer?

Используйте cron для простых команд, выполняемых в фиксированное время, особенно если задачу может потребоваться перенести на машину без systemd. Используйте таймер, если вам нужно видеть вывод в journal без перенаправлений, иметь возможность запросить статус завершения, настроить запуск после поднятия сети, добавить случайную задержку старта или политику повторных попыток при сбоях. Оба инструмента могут работать на одном сервере, поэтому вы можете переносить задачи по одной по мере необходимости.