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

Полный гид по Cron и crontab в Linux: от базового расписания до скрытых ловушек окружения

В системном администрировании и DevOps есть проверенное временем правило: если ты выполняешь какую-то рутинную операцию руками чаще двух раз, её пора автоматизировать. Снять ночной дамп базы данных, подчистить временные каталоги в /tmp, обновить TLS-сертификат или отправить сводный утренний отчет в корпоративный чат — всё это типовые задачи, которые операционная система должна делать сама, пока ты спокойно спишь.

И хотя современный стек пестрит сложными оркестраторами, операторами Kubernetes и продвинутыми облачными планировщиками, фундаментом автоматизации в мире Linux уже несколько десятилетий остаётся классическая подсистема Cron. Она работает на миллионах серверов по всему миру: от крошечных виртуалок за пару долларов до огромных банковских кластеров.

Но парадокс в том, что при кажущейся простоте синтаксиса звёздочек cron остаётся источником бесконечных ночных инцидентов. Сколько раз ты слышал (или говорил сам): «Скрипт идеально выполняется в терминале, запускаю руками — всё работает, но из cron он молча падает или вообще ничего не делает»? Знакомо?

Я не раз разбирал подобные факапы и знаю, сколько нервов съедают скрытые нюансы изоляции среды cron. Поэтому в этом руководстве мы вместе разложим всю подсистему по полочкам: от синтаксиса расписаний по официальному стандарту crontab(5) и практическим рекомендациям в руководстве по cron до ловушек переменных окружения, подавления паразитной почты, блокировок от наложения задач и развилки с современными systemd timers.

Анатомия подсистемы: в чём разница между cron, crond и crontab

Когда новички начинают работать с планировщиком, у них часто возникает каша в терминологии: cron, crond, crontab. Кажется, что это одно и то же слово, написанное с опечатками. Давай наведём идеальный порядок в понятиях:

  1. Демон Cron (в процессах crond или cron): это долгоживущая системная служба, которая запускается на самом раннем этапе загрузки ОС и непрерывно висит в оперативной памяти. Демон устроен предельно аскетично: ровно раз в минуту (на нулевой секунде каждой минуты) он просыпается, сканирует системные и пользовательские каталоги с конфигурациями, сравнивает текущее системное время с заданными правилами и порождает (fork) дочерние процессы для выполнения тех команд, чьё расписание совпало с текущей минутой.
  2. Файл Crontab (сокращение от cron table): это простой текстовый файл строгого формата, в котором построчно записаны временные интервалы и команды shell, которые нужно исполнить.
  3. Утилита crontab: это исполняемый бинарник командной строки (часто с установленным SUID-битом), который служит безопасным интерфейсом для чтения, редактирования и установки твоих личных расписаний без прямого лазания руками в служебные каталоги спулера.

В большинстве современных дистрибутивов Linux используются две основные реализации: Vixie Cron (наследник классического демона Пола Викси в Debian и Ubuntu) или cronie (развитие проекта от команды Red Hat в RHEL, Rocky Linux, CentOS, AlmaLinux и Fedora).

Управляется демон через стандартный менеджер служб:

# Проверка активного статуса службы в Debian и Ubuntu
sudo systemctl status cron

# Проверка статуса в дистрибутивах семейства Red Hat (RHEL / Rocky / Fedora)
sudo systemctl status crond

Если по какой-то причине служба выключена, запустить её и включить автозагрузку можно одной командой:

# Включение и немедленный запуск
sudo systemctl enable --now cron   # для Debian/Ubuntu
sudo systemctl enable --now crond  # для RHEL-дистрибутивов

Синтаксис расписания: пять полей времени, спецсимволы и готовые псевдонимы

Каждая рабочая строчка в файле пользовательского crontab состоит из двух смысловых частей: пяти временных полей и команды для запуска.

Классическая схема выглядит так:

┌───────────── минута (от 0 до 59)
│ ┌───────────── час (от 0 до 23, 24-часовой формат)
│ │ ┌───────────── день месяца (от 1 до 31)
│ │ │ ┌───────────── номер месяца (от 1 до 12 или имена JAN-DEC)
│ │ │ │ ┌───────────── день недели (от 0 до 6 или SUN-SAT, где 0 и 7 — воскресенье)
│ │ │ │ │
* * * * * /путь/к/твоей/команде аргументы

Чтобы гибко задавать расписание, в каждом поле используются специальные символы:

  • Звёздочка (*): означает «любое возможное значение». Если звёздочка стоит в поле минут, команда будет запускаться каждую минуту. Если во всех пяти полях стоят звёздочки (* * * * *), скрипт будет молотить каждую минуту круглые сутки.
  • Запятая (,): задаёт дискретный список значений. Например, 0,15,30,45 * * * * запустит команду каждые пятнадцать минут (в 00, 15, 30 и 45 минут каждого часа).
  • Дефис (-): задаёт непрерывный диапазон. Например, 0 9-18 * * 1-5 запустит задачу в начале каждого часа с 9:00 до 18:00 строго с понедельника по пятницу.
  • Слэш (/): задаёт шаг интервала. Запись */10 * * * * означает «каждые 10 минут». Можно комбинировать с диапазонами: 0 8-20/2 * * * запустит команду каждые 2 часа в интервале с 8 утра до 8 вечера (в 8, 10, 12, 14, 16, 18, 20 часов).

В синтаксисе есть одна тонкая логическая деталь, на которой спотыкаются даже опытные инженеры: взаимодействие дня месяца (поле 3) и дня недели (поле 5). Если в обоих полях указаны конкретные значения (не звёздочки), cron применяет к ним логическое ИЛИ (OR), а не И (AND)!

То есть запись:

0 12 1,15 * 1 /usr/local/bin/report.sh

выполнит скрипт 1-го и 15-го числа месяца, плюс каждый понедельник, независимо от даты! Если тебе нужно выполнить задачу строго в первый понедельник месяца, стандартными пятью полями этого не выразить: придётся запускать скрипт каждый понедельник с 1 по 7 число и внутри команды проверять день через команду test $(date +\%u) -eq 1. Кстати, если тебе когда-либо требовалось переводить человеческие фразы в подобные выражения, рекомендую посмотреть наш разбор инструмента cronsense: трансляция живой речи в выражения cron, RRULE и systemd.

Чтобы не высчитывать поля для частых типовых интервалов, создатели Vixie cron добавили удобные готовые макросы:

  • @reboot: запускает команду ровно один раз при старте операционной системы сразу после инициализации демона.
  • @hourly: аналог 0 * * * * (в начале каждого часа).
  • @daily или @midnight: аналог 0 0 * * * (каждую полночь).
  • @weekly: аналог 0 0 * * 0 (в полночь с субботы на воскресенье).
  • @monthly: аналог 0 0 1 * * (в полночь первого числа каждого месяца).
  • @yearly или @annually: аналог 0 0 1 1 * (в полночь первого января).

Пользовательские таблицы против системных: где искать /etc/crontab и /etc/cron.d/

Архитектурно в Linux существует два принципиально разных слоя таблиц расписания: пользовательские и системные.

Пользовательские crontab: У каждого пользователя в системе (включая пользователя root) есть собственный персональный файл расписания. Физически эти файлы хранятся в защищённом системном спул-каталоге:

  • В Debian / Ubuntu: /var/spool/cron/crontabs/<username>
  • В RHEL / CentOS: /var/spool/cron/<username>

Ни в коем случае не редактируй эти файлы напрямую в текстовом редакторе! Каталог спулера защищён правами доступа, а демон кэширует таблицы в памяти. Редактировать расписание нужно строго через утилиту crontab -e. Она создаёт временную копию файла, валидирует синтаксис при сохранении и отправляет сигнал демону немедленно перечитать конфигурацию.

Команды управления пользовательским расписанием:

# Открыть расписание текущего пользователя на редактирование
crontab -e

# Вывести текущее расписание в терминал без редактирования
crontab -l

# Полностью удалить всё своё расписание (осторожно, без подтверждения!)
crontab -r

# Администраторский просмотр или правка расписания другого пользователя
sudo crontab -u nginx -l
sudo crontab -u nginx -e

Системные файлы расписания: Второй слой — это системные таблицы, которыми управляет администратор ОС или пакетные менеджеры (apt, dnf). Они располагаются в каталоге /etc:

  • Файл /etc/crontab
  • Дополнительные модульные файлы внутри каталога /etc/cron.d/

Главное архитектурное отличие системного crontab от пользовательского: в системных файлах появляется шестое поле — имя пользователя, от лица которого должна исполняться команда!

Сравни форматы:

# Пользовательский crontab (5 полей времени + команда):
0 4 * * * /home/app/backup.sh

# Системный /etc/crontab (5 полей времени + USER + команда):
0 4 * * * root /usr/local/bin/system-backup.sh
0 5 * * * postgres /usr/bin/vacuumdb -a -z

Если ты по привычке скопируешь строчку из /etc/crontab в личный crontab -e, cron попытается выполнить команду с именем пользователя в качестве первого бинарника и выдаст ошибку root: command not found.

Пакетный запуск и фоновые каталоги: cron.daily, anacron и run-parts

Если ты заглянешь в каталог /etc, то увидишь четыре характерные папки:

  • /etc/cron.hourly/
  • /etc/cron.daily/
  • /etc/cron.weekly/
  • /etc/cron.monthly/

Это механизм модульной автоматизации пакетов. Тебе не нужно вручную прописывать строки в crontab: достаточно положить исполняемый bash-скрипт с правами chmod +x внутрь папки /etc/cron.daily/, и система будет запускать его каждый день.

Под капотом за запуск этих каталогов отвечает утилита run-parts. В ней зашито строгое соглашение об именовании файлов: имена скриптов не должны содержать точек, дефисов и расширений! Скрипт с именем my-backup.sh внутри /etc/cron.daily/ будет молча проигнорирован run-parts. Правильное имя — mybackup без всяких .sh. Проверить, какие скрипты подхватит утилита, можно тестовым запуском:

# Тестовый опрос каталога без запуска самих скриптов
run-parts --test /etc/cron.daily

А что произойдёт, если сервер или твой рабочий ноутбук выключается на ночь? Классический cron просто пропустит момент времени 06:25 утра, и ежедневный скрипт не выполнится вовсе.

Чтобы решить эту проблему, в Linux используется вспомогательный планировщик anacron (anachronistic cron). Он не привязан к конкретным минутам и часам: anacron проверяет дату последней отметки выполнения (файлы-маркеры в /var/spool/anacron/). Если при включении сервера выясняется, что вчерашняя ежедневная задача не запускалась, anacron с заданной задержкой выполнит её в фоновом режиме.

Главная ловушка новичка: минимальное окружение, урезанный PATH и /bin/sh

А вот теперь переходим к тому самому «айсбергу», о который разбиваются сотни скриптов. Почему код, который работает в твоём терминале, стабильно падает в cron?

Инженерная инфографическая схема Blueprint, сопоставляющая интерактивную пользовательскую оболочку bash и изолированную среду cron с урезанным PATH.

Причина кроется в глубокой изоляции среды исполнения.

Когда ты заходишь на сервер по SSH, запускается так называемая интерактивная сессия входа (login shell). Твой терминал считывает конфигурационные файлы профиля: /etc/profile, ~/.bash_profile, ~/.bashrc. В них подгружаются переменные окружения, пути к утилитам node, nvm, pyenv, виртуальным окружениям Python, токенам доступа и базам данных. В твоём шелле переменная PATH может содержать десяток каталогов.

А как запускает команду cron?

  1. Неинтерактивная оболочка (non-login shell): cron принципиально не читает твои файлы ~/.bashrc или ~/.profile! Никаких пользовательских переменных среды в момент запуска команды внутри cron просто не существует.
  2. Обрезанный PATH: по умолчанию в реализации Vixie cron переменная путей жёстко захардкожена всего двумя системными каталогами: PATH=/usr/bin:/bin Если твой скрипт вызывает утилиты из /usr/local/bin, /opt/, ~/.cargo/bin, docker, docker-compose или системные бинарники из /sbin, cron выплюнет ошибку command not found и завершит работу.
  3. Оболочка по умолчанию — /bin/sh: cron запускает команды через легковесный POSIX-совместимый шелл (в Ubuntu и Debian это dash), а не полноценный /bin/bash. Любые специфические конструкции Bash (так называемые bashisms: двойные квадратные скобки [[ ... ]], массивы, подстановки строк вида ${var//foo/bar}) вызовут синтаксическую ошибку syntax error: unexpected operand.

Как победить изоляцию окружения? Есть два надёжных инженерных правила:

Правило 1. Явно объявлять переменные в шапке crontab: Синтаксис crontab позволяет задавать переменные среды прямо в начале файла:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/home/deploy

0 3 * * * /home/deploy/scripts/backup.sh

Правило 2. Всегда использовать абсолютные пути в скриптах: Никогда не пиши python main.py или docker compose up. Пиши:

/home/deploy/app/venv/bin/python3 /home/deploy/app/main.py

Либо первой строчкой внутри своего bash-скрипта явно делай переход в рабочий каталог:

cd /home/deploy/app && ./run.sh

Управление выводом: отключение MAILTO, логирование в файл и защита через flock

По умолчанию в архитектуру cron заложена архаичная концепция из эпохи мейнфреймов 70-х годов: любой вывод, который порождает команда (будь то информационные сообщения в stdout или ошибки в stderr), демон перехватывает и пытается отправить локальным почтовым письмом текущему пользователю через почтовый сервер (MTA — sendmail или postfix).

В реальной жизни локальный MTA на сервере либо не настроен вовсе (и тогда системные логи /var/log/syslog захламляются бесконечными сообщениями об ошибках mail: cannot send message), либо почтовый ящик пользователя root втихую раздувается до сотен гигабайт.

Как правильно управлять выводом:

1. Отключение почты: В начале файла crontab задай пустую переменную MAILTO="". Это полностью выключит отправку локальных писем для всех задач:

MAILTO=""

2. Грамотное перенаправление в лог-файл: Если тебе важно сохранять историю выполнения и диагностировать сбои, перенаправляй потоки в файл с дозаписью (>>):

# Перенаправляем стандартный вывод в файл, а stderr (поток 2) склеиваем со stdout (поток 1)
30 2 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1

3. Подавление ненужного вывода: Если команда генерирует много служебного шума и логи не нужны, отправь весь вывод в виртуальную «чёрную дыру»:

0 * * * * /usr/local/bin/cleanup-temp.sh > /dev/null 2>&1

4. Защита от наложения задач (Race Conditions) через утилиту flock: Представь: ты настроил синхронизацию файлов или тяжелую обработку очереди каждые 5 минут:

*/5 * * * * /usr/local/bin/process-queue.sh

В обычных условиях скрипт отрабатывает за 40 секунд. Но однажды в очередь прилетела пачка тяжёлых данных, и скрипт завис на 15 минут. Что сделает cron? Ровно через 5 минут он вслепую запустит второй экземпляр того же скрипта! Ещё через 5 минут — третий! В результате несколько параллельных процессов начнут конкурировать за одну базу данных и забьют процессор сервера в полку.

Решение — стандартная утилита flock, создающая эксклюзивную файловую блокировку:

*/5 * * * * /usr/bin/flock -n /var/run/process-queue.lock /usr/local/bin/process-queue.sh >> /var/log/queue.log 2>&1

Флаг -n (non-blocking) указывает утилите: если предыдущий экземпляр всё ещё работает и файл блокировки удерживается, немедленно завершить работу без ожидания и не плодить дубликаты.

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

Давай соберём правильный пошаговый алгоритм: как добавить новую периодическую задачу на боевой сервер без сюрпризов.

  1. Задаём удобный редактор: По умолчанию система может открыть непривычный редактор. Задай nano в переменной сессии:
export EDITOR="nano"
export VISUAL="nano"
  1. Открываем crontab на редактирование:
crontab -e
  1. Оформляем безопасную структуру:
# Указываем предсказуемый шелл и пути
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

# Каждую ночь в 03:15: дамп базы с блокировкой и логированием
15 3 * * * /usr/bin/flock -n /tmp/backup.lock /opt/app/scripts/backup.sh >> /var/log/app/backup.log 2>&1

# Каждое воскресенье в 04:00: ротация старых архивов
0 4 * * 0 /usr/bin/find /opt/app/backups -type f -mtime +30 -delete > /dev/null 2>&1
  1. Проверяем права доступа к скрипту: Убедись, что скрипт исполняемый:
chmod +x /opt/app/scripts/backup.sh
  1. Тестируем выполнение в чистом окружении (Zero Environment Test): Прежде чем ждать наступления ночи, проверь, как твой скрипт поведёт себя в условиях, максимально приближенных к суровой изоляции cron. Для этого используется утилита env -i:
# Запуск команды с полностью очищенным окружением и минимальным PATH
env -i HOME="$HOME" PATH="/usr/bin:/bin" /bin/sh /opt/app/scripts/backup.sh

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

  1. Аудит реального выполнения в системных логах: Сам факт срабатывания демона логируется системным журналом. Проверить, проснулся ли cron в назначенную минуту, можно командами:
# В дистрибутивах с systemd
journalctl -u cron -n 20 --no-pager     # Debian/Ubuntu
journalctl -u crond -n 20 --no-pager    # RHEL/CentOS

# В классических логах syslog
grep CRON /var/log/syslog | tail -n 20

Когда cron упирается в потолок: сравнение с таймерами systemd

При всей своей гениальной простоте классический cron проектировался в другую технологическую эпоху. В современной серверной инфраструктуре у него есть три фундаментальных ограничения:

  1. Квант времени — строго одна минута: ты физически не можешь штатными средствами cron запустить задачу раз в 10 секунд или раз в 500 миллисекунд.
  2. Слепой запуск без учета зависимостей: cron ничего не знает о состоянии сети, монтировании дисков или готовности СУБД.
  3. Отсутствие контроля ресурсов: процессы cron запускаются вне изоляции cgroups; ты не можешь штатно ограничить лимит оперативной памяти или ядер процессора для задачи прямо в строке crontab.

Когда эти ограничения становятся критичными, на смену cron приходят таймеры systemd (systemd timers). Они строятся на связке двух файлов: служебного юнита .service (что запускать) и юнита таймера .timer (когда запускать):

# /etc/systemd/system/mytask.timer
[Unit]
Description=Периодическая синхронизация данных

[Timer]
OnBootSec=5min
OnUnitActiveSec=30s
AccuracySec=1ms
Persistent=true

[Install]
WantedBy=timers.target

Преимущества таймеров systemd очевидны: субсекундная точность, запуск относительно событий (например, через 5 минут после старта системы), автоматический запуск пропущенных задач (Persistent=true), строгие лимиты ресурсов в .service (MemoryMax=512M, CPUQuota=50%) и бесшовная интеграция с journalctl.

Но для 90% повседневных рутинных задач — бэкапов, очистки логов и периодических вызовов сервисных API — старый добрый cron остаётся самым быстрым, легковесным и надежным инструментом, который есть на любом Linux-сервере из коробки. Главное — помнить про изоляцию окружения, абсолютные пути и защиту от наложения процессов.

Чек-лист перед запуском новой задачи в cron

  • Переменная PATH и SHELL явно заданы в начале файла crontab
  • В самой команде и теле скрипта используются полные абсолютные пути ко всем бинарникам
  • Права на файл скрипта содержат бит исполнения (chmod +x)
  • Переменная MAILTO="" отключена, либо потоки вывода перенаправлены в лог-файл (>> /path/to/log 2>&1)
  • Для долгих и тяжёлых задач добавлена защита от параллельного запуска через flock
  • Скрипт успешно протестирован в очищенном окружении через команду env -i