В системном администрировании и DevOps есть проверенное временем правило: если ты выполняешь какую-то рутинную операцию руками чаще двух раз, её пора автоматизировать. Снять ночной дамп базы данных, подчистить временные каталоги в /tmp, обновить TLS-сертификат или отправить сводный утренний отчет в корпоративный чат — всё это типовые задачи, которые операционная система должна делать сама, пока ты спокойно спишь.
И хотя современный стек пестрит сложными оркестраторами, операторами Kubernetes и продвинутыми облачными планировщиками, фундаментом автоматизации в мире Linux уже несколько десятилетий остаётся классическая подсистема Cron. Она работает на миллионах серверов по всему миру: от крошечных виртуалок за пару долларов до огромных банковских кластеров.
Но парадокс в том, что при кажущейся простоте синтаксиса звёздочек cron остаётся источником бесконечных ночных инцидентов. Сколько раз ты слышал (или говорил сам): «Скрипт идеально выполняется в терминале, запускаю руками — всё работает, но из cron он молча падает или вообще ничего не делает»? Знакомо?
Я не раз разбирал подобные факапы и знаю, сколько нервов съедают скрытые нюансы изоляции среды cron. Поэтому в этом руководстве мы вместе разложим всю подсистему по полочкам: от синтаксиса расписаний по официальному стандарту crontab(5) и практическим рекомендациям в руководстве по cron до ловушек переменных окружения, подавления паразитной почты, блокировок от наложения задач и развилки с современными systemd timers.
Анатомия подсистемы: в чём разница между cron, crond и crontab
Когда новички начинают работать с планировщиком, у них часто возникает каша в терминологии: cron, crond, crontab. Кажется, что это одно и то же слово, написанное с опечатками. Давай наведём идеальный порядок в понятиях:
- Демон Cron (в процессах
crondилиcron): это долгоживущая системная служба, которая запускается на самом раннем этапе загрузки ОС и непрерывно висит в оперативной памяти. Демон устроен предельно аскетично: ровно раз в минуту (на нулевой секунде каждой минуты) он просыпается, сканирует системные и пользовательские каталоги с конфигурациями, сравнивает текущее системное время с заданными правилами и порождает (fork) дочерние процессы для выполнения тех команд, чьё расписание совпало с текущей минутой. - Файл Crontab (сокращение от cron table): это простой текстовый файл строгого формата, в котором построчно записаны временные интервалы и команды shell, которые нужно исполнить.
- Утилита
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?

Причина кроется в глубокой изоляции среды исполнения.
Когда ты заходишь на сервер по SSH, запускается так называемая интерактивная сессия входа (login shell). Твой терминал считывает конфигурационные файлы профиля: /etc/profile, ~/.bash_profile, ~/.bashrc. В них подгружаются переменные окружения, пути к утилитам node, nvm, pyenv, виртуальным окружениям Python, токенам доступа и базам данных. В твоём шелле переменная PATH может содержать десяток каталогов.
А как запускает команду cron?
- Неинтерактивная оболочка (non-login shell): cron принципиально не читает твои файлы
~/.bashrcили~/.profile! Никаких пользовательских переменных среды в момент запуска команды внутри cron просто не существует. - Обрезанный PATH: по умолчанию в реализации Vixie cron переменная путей жёстко захардкожена всего двумя системными каталогами:
PATH=/usr/bin:/binЕсли твой скрипт вызывает утилиты из/usr/local/bin,/opt/,~/.cargo/bin,docker,docker-composeили системные бинарники из/sbin, cron выплюнет ошибкуcommand not foundи завершит работу. - Оболочка по умолчанию —
/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) указывает утилите: если предыдущий экземпляр всё ещё работает и файл блокировки удерживается, немедленно завершить работу без ожидания и не плодить дубликаты.
Практический регламент: настройка задач, безопасное тестирование и аудит
Давай соберём правильный пошаговый алгоритм: как добавить новую периодическую задачу на боевой сервер без сюрпризов.
- Задаём удобный редактор:
По умолчанию система может открыть непривычный редактор. Задай
nanoв переменной сессии:
export EDITOR="nano"
export VISUAL="nano"
- Открываем crontab на редактирование:
crontab -e
- Оформляем безопасную структуру:
# Указываем предсказуемый шелл и пути
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
- Проверяем права доступа к скрипту: Убедись, что скрипт исполняемый:
chmod +x /opt/app/scripts/backup.sh
- Тестируем выполнение в чистом окружении (Zero Environment Test):
Прежде чем ждать наступления ночи, проверь, как твой скрипт поведёт себя в условиях, максимально приближенных к суровой изоляции cron. Для этого используется утилита
env -i:
# Запуск команды с полностью очищенным окружением и минимальным PATH
env -i HOME="$HOME" PATH="/usr/bin:/bin" /bin/sh /opt/app/scripts/backup.sh
Если скрипт упал здесь — он гарантированно упал бы и в cron. Исправляй пути к утилитам прямо сейчас.
- Аудит реального выполнения в системных логах: Сам факт срабатывания демона логируется системным журналом. Проверить, проснулся ли 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 проектировался в другую технологическую эпоху. В современной серверной инфраструктуре у него есть три фундаментальных ограничения:
- Квант времени — строго одна минута: ты физически не можешь штатными средствами cron запустить задачу раз в 10 секунд или раз в 500 миллисекунд.
- Слепой запуск без учета зависимостей: cron ничего не знает о состоянии сети, монтировании дисков или готовности СУБД.
- Отсутствие контроля ресурсов: процессы 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
