Плановые задачи на Linux часто начинают с одной строки в crontab. Для короткого скрипта этого достаточно, но на сервере быстро появляются требования, которые cron решает неудобно: отдельный пользователь, ограничения ресурсов, понятный журнал, запуск пропущенной задачи после простоя, зависимость от сети и защита файловой системы. В таких сценариях полезно перенести расписание в systemd timer, а саму работу оформить как обычный .service.
В этом руководстве мы создадим ежедневную резервную копию каталога приложения, проверим календарное выражение, запустим задачу вручную, посмотрим журнал и добавим базовые ограничения безопасности. Команды подходят для современных дистрибутивов с systemd, включая Ubuntu Server 24.04 LTS и более новые выпуски. Перед применением на production проверьте версии systemd и доступные параметры через systemd --version и локальные man-страницы.
Чем systemd timer отличается от cron
Таймер не выполняет команду сам. Он активирует другой unit, обычно сервис с тем же именем. Например, myapp-backup.timer запускает myapp-backup.service. Расписание и команда разделены, поэтому их проще проверять независимо.
- Журнал. Стандартный вывод и ошибки сервиса попадают в journald и читаются через
journalctl. - Права. В service unit явно задаются
User=,Group=, рабочий каталог и разрешённые пути. - Пропущенный запуск.
Persistent=trueпозволяет выполнить календарную задачу после включения сервера, если её время было пропущено. - Зависимости. Сервис можно запускать после сети, монтирования диска или другого unit.
- Ограничения. Доступны sandbox-параметры systemd, лимиты CPU и ввода-вывода.
- Диагностика.
systemctl list-timersпоказывает предыдущий и следующий запуск.
Cron остаётся нормальным выбором для простых пользовательских задач. Systemd timer удобнее, когда расписание является частью эксплуатации сервиса и должно проверяться так же, как остальная конфигурация сервера.
Для сайтов, ботов и pet-проектов можно использовать Beget: виртуальный хостинг от 420 ₽/мес., VPS от 330 ₽/мес. и тестовый период.
Как устроена пара service и timer
В примере будут три файла:
/usr/local/sbin/myapp-backup— исполняемый скрипт;/etc/systemd/system/myapp-backup.service— описание одного запуска;/etc/systemd/system/myapp-backup.timer— расписание.
Такое разделение важно. Сервис можно запустить вручную и отладить без ожидания нужного времени, а расписание можно проверить до активации.
Подготовка отдельного пользователя и каталогов
Не запускайте резервное копирование от root только потому, что так проще. Сначала определите, какие файлы действительно должен читать процесс и куда он должен писать. Создадим системного пользователя без интерактивного входа:
sudo useradd --system
--home-dir /var/lib/myapp-backup
--create-home
--shell /usr/sbin/nologin
myapp-backup
sudo install -d -o myapp-backup -g myapp-backup -m 0750
/var/backups/myapp
Пользователю нужен доступ на чтение к исходному каталогу. Лучше выдать минимальное ACL или добавить его в подходящую группу, чем менять владельца всего приложения. Пример с ACL:
sudo setfacl -R -m u:myapp-backup:rX /srv/myapp
sudo setfacl -R -d -m u:myapp-backup:rX /srv/myapp
Команды setfacl и getfacl входят в пакет acl; при необходимости установите его через sudo apt install acl. Второй параметр задаёт ACL по умолчанию для новых объектов внутри каталога. Проверьте результат командой getfacl /srv/myapp. Если приложение хранит секреты, базы данных или сокеты, не выдавайте доступ ко всему дереву без разбора.
Скрипт резервного копирования
Создайте /usr/local/sbin/myapp-backup:
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
SOURCE_DIR="/srv/myapp"
BACKUP_DIR="/var/backups/myapp"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
ARCHIVE="${BACKUP_DIR}/myapp-${STAMP}.tar.gz"
test -d "${SOURCE_DIR}"
install -d -m 0750 "${BACKUP_DIR}"
/usr/bin/tar
--create
--gzip
--file "${ARCHIVE}"
--directory "${SOURCE_DIR}"
--one-file-system
.
/usr/bin/find "${BACKUP_DIR}"
-type f
( -name 'myapp-*.tar.gz' -o -name 'myapp-*.tar.gz.sha256' )
-mtime +14
-delete
/usr/bin/test -s "${ARCHIVE}"
/usr/bin/sha256sum "${ARCHIVE}" > "${ARCHIVE}.sha256"
printf 'Created %sn' "${ARCHIVE}"
Затем назначьте владельца и права:
sudo chown root:root /usr/local/sbin/myapp-backup
sudo chmod 0755 /usr/local/sbin/myapp-backup
Скрипт завершится с ошибкой при необъявленной переменной, неудачной команде или ошибке в pipeline. umask 077 закрывает новые архивы от других пользователей. Проверка test -s подтверждает только наличие непустого файла, но не доказывает, что приложение можно восстановить. Для настоящего бэкапа нужен регулярный restore-тест на отдельной машине.
Архивирование работающей базы данных простым tar небезопасно. PostgreSQL, MySQL и другие СУБД копируют штатными инструментами, например pg_dump или mysqldump, либо используют согласованный snapshot. Пароли не передают в командной строке: они могут попасть в список процессов и журналы.
Создание service unit
Файл /etc/systemd/system/myapp-backup.service:
[Unit]
Description=Backup myapp files
Documentation=https://prizraks.ru/
ConditionPathIsDirectory=/srv/myapp
[Service]
Type=oneshot
User=myapp-backup
Group=myapp-backup
ExecStart=/usr/local/sbin/myapp-backup
TimeoutStartSec=2h
Nice=10
IOSchedulingClass=idle
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/srv/myapp
ReadWritePaths=/var/backups/myapp
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
Type=oneshot означает, что сервис выполняет одну операцию и завершается. Пока unit активен, повторная активация тем же таймером не создаёт второй параллельный экземпляр. Это снижает риск наложения запусков, но не защищает от ручного запуска другого скрипта, который пишет в тот же каталог.
Параметры sandbox следует проверять на конкретной задаче. Например, ProtectHome=true закроет доступ к /home, /root и /run/user. Если источник находится там, сервис завершится ошибкой. Не копируйте hardening-настройки вслепую: добавляйте их группами и после каждой группы выполняйте тестовый запуск.
Создание timer unit
Файл /etc/systemd/system/myapp-backup.timer:
[Unit]
Description=Run myapp backup every day
[Timer]
OnCalendar=*-*-* 03:20:00 UTC
Persistent=true
RandomizedDelaySec=15m
AccuracySec=1m
Unit=myapp-backup.service
[Install]
WantedBy=timers.target
OnCalendar задаёт календарное расписание. Здесь запуск планируется ежедневно на 03:20 UTC. Явная зона времени избавляет от сюрпризов при смене локальной зоны сервера. Перед использованием проверьте выражение:
systemd-analyze calendar '*-*-* 03:20:00 UTC'
systemd-analyze calendar --iterations=5 '*-*-* 03:20:00 UTC'
RandomizedDelaySec=15m добавляет случайную задержку до 15 минут. Это полезно, когда много серверов не должны одновременно нагружать хранилище или API. Если операция обязана начаться почти точно, уменьшите или уберите задержку. AccuracySec задаёт допустимое окно точности и решает другую задачу: systemd может объединять пробуждения таймеров ради эффективности.
Что делает Persistent
При Persistent=true systemd хранит время последнего срабатывания календарного таймера. Если сервер был выключен в 03:20, задача может запуститься после следующей загрузки. Несколько пропущенных периодов не превращаются в очередь из множества запусков: обычно выполняется одна догоняющая активация.
Для тяжёлого backup это поведение нужно учитывать. Сервер может одновременно загружаться, обновлять пакеты и начинать резервное копирование. Добавляйте случайную задержку, условия доступности хранилища и мониторинг свободного места. Если догоняющий запуск не нужен, используйте Persistent=false.
Активация и первая проверка
sudo systemctl daemon-reload
sudo systemd-analyze verify
/etc/systemd/system/myapp-backup.service
/etc/systemd/system/myapp-backup.timer
sudo systemctl enable --now myapp-backup.timer
systemctl status myapp-backup.timer
systemctl list-timers --all myapp-backup.timer
enable --now включает таймер для следующих загрузок и запускает его сейчас как механизм расписания. Команда не обязана немедленно выполнить backup. Для проверки сервиса запустите его отдельно:
sudo systemctl start myapp-backup.service
systemctl status myapp-backup.service
sudo journalctl -u myapp-backup.service --since today
sudo -u myapp-backup ls -lh /var/backups/myapp
Успешный статус сервиса ещё не подтверждает восстановление. Распакуйте архив во временный каталог и проверьте ожидаемые файлы:
RESTORE_DIR="$(sudo -u myapp-backup mktemp -d)"
sudo -u myapp-backup tar -xzf
/var/backups/myapp/myapp-YYYYMMDDTHHMMSSZ.tar.gz
-C "${RESTORE_DIR}"
find "${RESTORE_DIR}" -maxdepth 2 -type f | head
Для production восстановление выполняют на отдельном тестовом сервере. Не распаковывайте непроверенный архив поверх работающего приложения.
Как читать журналы и искать причину ошибки
Таймер сообщает, когда должен запускаться unit, а результат работы находится в журнале сервиса:
systemctl list-timers --all
systemctl show myapp-backup.timer
-p LastTriggerUSec
-p NextElapseUSecRealtime
journalctl -u myapp-backup.timer --since '24 hours ago'
journalctl -u myapp-backup.service -n 100 --no-pager
journalctl -u myapp-backup.service -p warning..alert
Частая ошибка — смотреть только status timer. Таймер может сработать корректно, а сервис завершиться с ненулевым кодом из-за прав, заполненного диска или недоступного каталога.
Для системного аудита полезно связать эти журналы с настройкой auditd и journald. Тогда видно не только ошибку скрипта, но и изменения unit-файлов и критичных каталогов.
Монотонные таймеры без календаря
Не каждую задачу нужно привязывать к часам. Для запуска через несколько минут после загрузки и затем через одинаковый интервал используются монотонные параметры:
[Timer]
OnBootSec=10m
OnUnitActiveSec=6h
RandomizedDelaySec=10m
Unit=myapp-maintenance.service
OnBootSec отсчитывает время от загрузки, а OnUnitActiveSec — от последней активации unit. Такой вариант подходит для обслуживания кеша, синхронизации и проверок, которым не важно точное время суток.
Пользовательские timers
Unit можно разместить в ~/.config/systemd/user/ и управлять им без sudo:
systemctl --user daemon-reload
systemctl --user enable --now report.timer
systemctl --user list-timers
journalctl --user -u report.service
По умолчанию user manager связан с пользовательской сессией. Для работы после выхода иногда включают linger:
sudo loginctl enable-linger username
Это меняет модель работы пользователя и запускает его manager без активного входа. Включайте linger только осознанно и проверяйте, какие пользовательские units получат возможность работать постоянно.
Типичные ошибки при переходе с cron
Команда использует shell синтаксис
ExecStart не является строкой shell. Конструкции >, |, &&, подстановка $(...) и wildcard работают не так, как в crontab. Сложную последовательность помещайте в проверяемый скрипт. Не используйте /bin/sh -c только ради удобства: это усложняет экранирование и расширяет поверхность ошибок.
Используются относительные пути
У cron и systemd отличается окружение от интерактивной оболочки. Указывайте абсолютные пути к скриптам и важным утилитам, задавайте WorkingDirectory=, если программа зависит от текущего каталога.
Таймер включили но не запустили
systemctl enable name.timer создаёт привязку к загрузке, но не всегда запускает unit в текущей сессии. Используйте enable --now или отдельный start.
Редактирование не применилось
После изменения unit-файла выполняйте systemctl daemon-reload. Затем перезапустите timer и проверьте новое расписание через list-timers.
Задача зависит от сети
After=network-online.target задаёт порядок, но само по себе не гарантирует доступность конкретного API или хранилища. Скрипт должен иметь timeout, ограниченные повторы и корректный код завершения.
Архивы заполнили диск
Retention нужно тестировать отдельно. Добавьте мониторинг свободного места и внешний backup. Копия, расположенная только на том же VPS, не защищает от отказа диска или удаления сервера.
Как безопасно изменить готовый unit
Unit из пакета не редактируют напрямую в /usr/lib/systemd/system или /lib/systemd/system: обновление может стереть изменения. Используйте drop-in:
sudo systemctl edit apt-daily-upgrade.timer
Например, чтобы отключить догоняющий запуск конкретного таймера:
[Timer]
Persistent=false
Посмотрите итоговую конфигурацию:
systemctl cat apt-daily-upgrade.timer
systemctl show apt-daily-upgrade.timer -p FragmentPath -p DropInPaths
Ubuntu использует systemd timers для apt-daily.timer и apt-daily-upgrade.timer, поэтому неожиданный запуск обновлений после загрузки часто связан именно с Persistent и случайной задержкой, а не с cron.
Минимальный чеклист перед production
- календарное выражение проверено через
systemd-analyze calendar; - service unit успешно запускается вручную;
- процесс работает от отдельного пользователя с минимальными правами;
- секреты не находятся в unit-файле и командной строке;
- таймауты и повторы сетевых операций ограничены;
- логи читаются через
journalctlи имеют понятные ошибки; - настроены retention и контроль свободного места;
- резервная копия хранится вне исходного сервера;
- восстановление проверено на отдельном стенде;
- после изменения unit выполнены
daemon-reloadи повторный тест.
Для общей защиты сервера используйте этот механизм вместе с чеклистом hardening Ubuntu и безопасной настройкой SSH. Таймер делает запуск наблюдаемым, но не исправляет слабые права, отсутствие резервного восстановления или небезопасный скрипт.
Нужен отдельный VPS для безопасного теста? Проверяйте таймеры, права и восстановление на некритичном сервере до переноса в production. Можно использовать любой подходящий VPS; один из вариантов — Beget. Партнёрская ссылка не увеличивает стоимость для читателя.
Когда cron всё же лучше
Оставьте cron, если задача короткая, пользовательская, не требует отдельного журнала и понятна всем администраторам проекта. Не переносите расписание ради самого переноса. Systemd timer оправдан, когда важны права, зависимости, missed runs, ресурсные ограничения и единая диагностика через systemctl.
Для контейнерных платформ выбор другой: периодическую задачу обычно описывают средствами оркестратора, например Kubernetes CronJob. Запуск системного timer внутри временного контейнера часто ломает модель одного процесса и усложняет наблюдение.
FAQ
Нужно ли удалять запись из crontab сразу
Нет. Сначала отключите cron-задачу контролируемо, запустите service вручную и убедитесь, что timer активен. Не держите оба расписания включёнными: получите дублирующие операции.
Выполнит ли Persistent все пропущенные запуски
Нет. Для календарного таймера после простоя обычно выполняется одна догоняющая активация, даже если было пропущено несколько периодов.
Как запустить задачу прямо сейчас
Запускайте service, а не timer: sudo systemctl start myapp-backup.service. Затем проверьте статус и журнал.
Где смотреть следующий запуск
Используйте systemctl list-timers --all myapp-backup.timer. Для проверки будущих дат календарного выражения подходит systemd-analyze calendar --iterations=5 'выражение'.
Можно ли хранить пароль в EnvironmentFile
Файл с правами 0600 лучше секрета прямо в unit, но переменные окружения не являются полноценным хранилищем секретов. Для production используйте возможности конкретного приложения или менеджер секретов, ограничьте чтение файла и не выводите значения в журнал.
Итог
Systemd timers удобны не из-за более красивого синтаксиса расписания, а потому что плановая операция становится обычным управляемым сервисом. Её можно запустить вручную, ограничить правами, проверить через systemd-analyze, увидеть в list-timers и разобрать по журналу. Начните с одной некритичной задачи, проверьте восстановление и только затем переносите остальные расписания.
Официальная документация
- systemd.timer — параметры timer unit, включая
OnCalendar,Persistent,AccuracySecиRandomizedDelaySec. - systemd.time — синтаксис календарных выражений.
- systemctl — управление units и команда
list-timers. - Ubuntu Server documentation — работа
apt-daily.timer, случайной задержки иPersistent.
Подборка онлайн-курсов для разработчиков — Python, Java, веб и DevOps.
14 практических страниц: SSH, firewall, обновления, резервные копии, WordPress, Docker и действия при признаках взлома.
Проверим SSH, firewall, обновления, резервные копии, Docker и журналы. Аудит — от 9 900 ₽.
Статья может содержать партнёрские ссылки. Это не влияет на стоимость для вас, но помогает развивать блог.

