Полная опись автозапуска: что поднимется при следующей загрузке
Сервер достался вам «в наследство», или вы просто год ничего не трогали — и теперь на вопрос «что вообще стартует при загрузке этой машины» честный ответ один: не знаю. systemctl list-units --type=service покажет только то, что живёт под systemd, и это лишь часть картины: рядом может быть строка @reboot в чьём-то персональном crontab, забытый скрипт в /etc/init.d/, который никто не переносил на юниты, и /etc/rc.local с командой пятилетней давности. Разберём, как собрать полную опись всех легитимных механизмов автозапуска — не догадками, а конкретными командами по каждому источнику — и получить таблицу того, что реально поднимется при следующей перезагрузке.
Содержание
- Почему `systemctl list-units` — это не вся картина
- Systemd: `enabled` — это не то же самое, что «запустится»
- Cron: `@reboot` — строка, которую `systemctl` никогда не покажет
- SysV init-скрипты: наследие `/etc/init.d`, которое никуда не делось
- `rc.local` и другие точки входа, которые легко упустить
- Сводная таблица: собираем всё в одном месте
- Как проверить опись без реальной перезагрузки продакшена
Почему `systemctl list-units` — это не вся картина
Systemd — основной, но не единственный механизм автозапуска на линукс-сервере. Он честно расскажет о самом себе: какие юниты включены, какие запущены, какие подтягиваются как зависимость. Но у него нет обязанности знать про cron, про старые SysV-скрипты, которые технически ему не принадлежат (даже если он их и оборачивает), и уж тем более про /etc/rc.local, который является отдельным необязательным механизмом совместимости.
Это принципиально другая задача, чем поиск процессов, запущенных вручную мимо systemd — тех, что живут в nohup или tmux-сессии и умирают безвозвратно при перезагрузке. Той теме посвящена отдельная статья про процессы, запущенные мимо systemd: там разбирается, как найти нелегитимный ручной запуск и перевести его под юнит. Здесь задача обратная и шире: собрать опись всего, что настроено на автозапуск легитимно — через recognised-механизмы системы — и понять, какие из них реально сработают при следующем ребуте, а какие числятся «включёнными» лишь формально.
Systemd: `enabled` — это не то же самое, что «запустится»
Базовая команда очевидна:
systemctl list-unit-files --type=service --state=enabled --no-legend
Она покажет юниты, у которых стоит симлинк из /etc/systemd/system/<target>.wants/ (или аналогичной директории) на файл юнита — именно эти симлинки создаёт systemctl enable. Но в выводе list-unit-files есть и другие статусы, которые часто путают:
| Статус | Что значит | Запустится при ребуте? |
|---|---|---|
enabled | Есть симлинк в .wants/ для целевого target | Да, если target активируется |
disabled | Юнит существует, симлинка нет | Нет, только вручную |
static | Нет секции [Install] — юнит нельзя enable/disable напрямую | Только если его подтянет как зависимость другой enabled-юнит |
masked | Симлинк на /dev/null — юнит невозможно запустить вообще | Нет, даже если что-то пытается его подтянуть |
generated | Юнит создан «на лету» генератором (например, из fstab или SysV-скрипта) | Зависит от источника |
Статус static — источник частых сюрпризов: сервис не значится «enabled», но исправно поднимается каждый раз, потому что его тянет за собой через Wants= или Requires= другой юнит. Проверить реальную цепочку зависимостей целевого target:
systemctl list-dependencies multi-user.target --no-pager
Это куда честнее, чем полагаться только на список enabled — здесь видно всё дерево того, что будет поднято при достижении multi-user.target (или graphical.target, если на сервере есть графическое окружение — редкость, но встречается на панельных VPS).
Отдельный слой — пользовательские (user-level) юниты, которые запускаются не системным менеджером, а per-user экземпляром systemd. Они переживут ребут только если для пользователя включён linger:
loginctl show-user <user> --property=Linger
Если Linger=no — все юниты этого пользователя (например, systemctl --user list-unit-files --state=enabled под ним) стартуют только после интерактивного логина, а не при загрузке сервера. Это частая причина, почему «сервис же был enabled» не помогает: он был enabled как user-юнит без linger, и жил только пока в системе была активна хоть одна сессия этого пользователя.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSCron: `@reboot` — строка, которую `systemctl` никогда не покажет
Специальное время @reboot в crontab — полноценный, легитимный механизм автозапуска, полностью независимый от systemd. Он живёт в нескольких местах одновременно, и проверять нужно все:
# системный crontab
cat /etc/crontab 2>/dev/null | grep -i reboot
# задачи по drop-in файлам
grep -rH "@reboot" /etc/cron.d/ 2>/dev/null
# личные crontab всех пользователей системы
for user in $(cut -f1 -d: /etc/passwd); do
crontab -l -u "$user" 2>/dev/null | grep -H "@reboot" | sed "s/^/user=$user: /"
done
Третья команда важна отдельно: crontab -l без -u показывает только crontab текущего пользователя, а личные crontab лежат в /var/spool/cron/crontabs/ (Debian/Ubuntu) или /var/spool/cron/ (RHEL-семейство) под именами пользователей — и там вполне может обнаружиться @reboot у аккаунта, про который все забыли, включая тех, что технически не имеют интерактивного логина.
@reboot — популярный способ поднять что-то простое без написания unit-файла: скрипт бэкапа, разовую очистку временных файлов, запуск VPN-клиента. Проблема в том, что у @reboot-задачи нет ни Restart=on-failure, ни ограничений ресурсов, ни интеграции с journalctl — она просто выполняется один раз при старте cron-демона, и если она упадёт или зависнет, узнать об этом можно только по логам самого скрипта (если он их вообще ведёт). Про то, где ещё, кроме crontab -l, могут прятаться периодические задачи (включая непрямые способы попасть в автозапуск), подробно разобрано в статье чужая задача в cron: где ещё смотреть, кроме crontab. А к чему приводит неаккуратная @reboot-строка, которая задваивается при каждом деплое — в разборе инцидента @reboot в crontab поднял 30 копий сервиса: полезно прочитать перед тем, как класть в @reboot что-то долгоживущее, а не разовый скрипт.
SysV init-скрипты: наследие `/etc/init.d`, которое никуда не делось
На современных дистрибутивах с systemd классические init-скрипты формально не нужны, но пакеты, которым много лет (некоторые VPN-клиенты, старые агенты мониторинга, самописные скрипты, унаследованные от прошлого админа), до сих пор могут ставить файл в /etc/init.d/ вместо unit-файла. Systemd не игнорирует их — он оборачивает такие скрипты через systemd-sysv-generator и представляет их как обычные .service-юниты, поэтому systemctl status <имя> для них может честно показывать Loaded: loaded (/etc/init.d/<имя>; generated).
Проверить, что вообще лежит в /etc/init.d/:
ls /etc/init.d/
А включён ли конкретный скрипт в текущий runlevel — смотреть символические ссылки. Systemd транслирует классические runlevel в свои target (runlevel 3 → multi-user.target, runlevel 5 → graphical.target), и для большинства headless-серверов актуален именно runlevel 3/multi-user.target:
systemctl get-default
ls /etc/rc3.d/ 2>/dev/null | grep '^S'
Символические ссылки вида S01имя — это «start»-скрипты, которые будут выполнены при входе в runlevel; K-ссылки — «kill», выполняются при выходе. Управление такими ссылками исторически делалось через update-rc.d (Debian/Ubuntu) или chkconfig (RHEL-семейство). Практический нюанс: если скрипт был подключён через update-rc.d <имя> defaults пять лет назад и с тех пор не трогался, systemctl is-enabled <имя> для него часто вернёт enabled — compat-слой честно транслирует наличие rcN.d-симлинков в понятный systemd-статус. С точки зрения «переживёт ли ребут» это не хуже нативного unit-файла, но с точки зрения диагностики (нет ограничений ресурсов, логи не всегда идут в journal) это легаси, от которого стоит избавляться — переписывая в нормальный unit, тем же подходом, что и при переводе процесса из ручного запуска в юнит.
`rc.local` и другие точки входа, которые легко упустить
/etc/rc.local — ещё один механизм совместимости, отдельный и от cron, и от SysV runlevel-ссылок. В современных дистрибутивах он не запускается автоматически сам по себе: за него отвечает юнит rc-local.service, и этот юнит по умолчанию содержит условие ConditionFileIsExecutable=/etc/rc.local. Из-за этого возможна классическая грабля: файл /etc/rc.local существует, в нём лежит нужная команда, но у файла нет бита на выполнение (например, его скопировали scp без сохранения прав, или отредактировали через какой-то инструмент, который права сбросил) — юнит молча пропускает запуск, без ошибки в логах, потому что условие Condition* не выполнилось, а это не считается сбоем самого юнита.
Проверка:
ls -l /etc/rc.local
systemctl status rc-local.service
Если файла /etc/rc.local в системе вообще нет — юнит rc-local.service может отсутствовать или быть замаскирован, и это нормально: пустое место здесь ничего не значит, это не обязательный механизм.
Кроме rc.local, в опись стоит добавить ещё одну точку, которую легко упустить: автозапуск контейнеров. Если на сервере используется Docker или Podman, флаг перезапуска (--restart=always или --restart=unless-stopped) хранится в собственной конфигурации контейнерного движка, а не в systemd напрямую. От systemd здесь зависит только сам демон (docker.service/podman.socket) — если он не enabled, ни один контейнер с любым restart-policy не поднимется, потому что поднимать их будет некому.
Сводная таблица: собираем всё в одном месте
Смысл описи не в том, чтобы держать в голове пять разных команд, а в том, чтобы иметь один скрипт, который прогоняет их все и складывает результат в файл — тогда его можно хранить в git и сравнивать снимки со временем, замечая, что появилось нового.
#!/bin/bash
# audit-autostart.sh — снимок всех легитимных механизмов автозапуска
echo "=== systemd: enabled unit-файлы ==="
systemctl list-unit-files --type=service --state=enabled --no-legend
echo -e "\n=== systemd: что реально тянется в multi-user.target ==="
systemctl list-dependencies multi-user.target --no-pager
echo -e "\n=== cron: @reboot по всем пользователям и /etc/cron.d ==="
grep -rH "@reboot" /etc/crontab /etc/cron.d/ 2>/dev/null
for user in $(cut -f1 -d: /etc/passwd); do
crontab -l -u "$user" 2>/dev/null | grep -H "@reboot" | sed "s/^/user=$user: /"
done
echo -e "\n=== SysV: активные start-скрипты в текущем runlevel ==="
case "$(systemctl get-default)" in
graphical.target) rc_dir=/etc/rc5.d ;;
multi-user.target) rc_dir=/etc/rc3.d ;;
esac
[ -n "$rc_dir" ] && ls "$rc_dir" 2>/dev/null | grep '^S'
echo -e "\n=== rc.local ==="
[ -f /etc/rc.local ] && ls -l /etc/rc.local && systemctl is-enabled rc-local.service 2>/dev/null
echo -e "\n=== linger для пользовательских юнитов ==="
loginctl list-users --no-legend | awk '{print $2}' | while read -r u; do
loginctl show-user "$u" --property=Linger 2>/dev/null | sed "s/^/user=$u: /"
done
Итог такого прогона удобно свести в общую таблицу — вот как выглядит типичный результат по реальному серверу:
| Механизм | Источник конфигурации | Переживёт ребут? |
|---|---|---|
nginx.service | enabled unit-файл | Да |
myapp.service | static, тянется через Wants= из app-stack.target | Да, косвенно |
| Бэкап-скрипт | @reboot в личном crontab пользователя backup | Да, но без рестарта при падении |
legacy-agent | /etc/init.d/legacy-agent, симлинк в /etc/rc3.d/S20 | Да, через SysV-совместимость |
| Скрипт правки iptables | /etc/rc.local, файл без +x | Нет — условие юнита не выполняется |
debug-server.py | запущен вручную через nohup, юнита/crontab/init.d нет | Нет, см. статью про процессы мимо systemd |
Именно такая таблица — конечная цель описи: не абстрактный список команд, а конкретный ответ по каждому пункту «да, поднимется» или «нет, и вот почему».
Как проверить опись без реальной перезагрузки продакшена
Полностью безопасного способа проверить @reboot-задачи и SysV-скрипты, кроме фактической перезагрузки, не существует — это их ограничение по сравнению с systemd-юнитами. Но снизить риск можно:
- Для unit-файлов —
systemd-analyze verify <имя>.serviceпроверит синтаксис и явные ошибки конфигурации без запуска сервиса. - Для цепочки зависимостей target —
systemctl list-dependencies --plain multi-user.targetбез реального ребута покажет, что дерево вообще валидно и не содержит юнитов с ошибками загрузки (systemctl --failedпосле этого стоит проверить отдельно). - Для cron и rc.local — надёжной альтернативы тестовой перезагрузке на клоне/staging-копии сервера нет. Если сервис критичный, стоит держать отдельную staging-машину именно для проверки такого рода изменений перед тем, как переносить их в прод.
- Держать снимок описи в git — тогда при следующем инциденте («сервис не поднялся после планового ребута») не нужно гадать заново: достаточно посмотреть diff между последним снимком и текущим состоянием, чтобы увидеть, что именно изменилось в конфигурации автозапуска.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Чем enabled отличается от static в выводе systemctl list-unit-files?
У enabled-юнита есть прямой симлинк в .wants/-директории целевого target, поставленный командой systemctl enable. У static-юнита нет секции [Install], поэтому его нельзя enable/disable напрямую — он запускается только если его явно тянет через Wants=/Requires= другой юнит. Оба варианта одинаково переживут перезагрузку, если цепочка зависимостей не нарушена.
Нужно ли переживать из-за SysV init-скриптов на современном Debian/Ubuntu?
Как правило, нет — большинство пакетов давно перешли на unit-файлы. Но конкретные старые пакеты (некоторые VPN-клиенты, самописные легаси-скрипты) могут до сих пор ставить файл в /etc/init.d/. Он продолжит работать через compat-слой systemd-sysv-generator, но лучше при первой возможности переписать его в нормальный unit — так проще диагностировать сбои и задавать ограничения ресурсов.
Почему rc.local есть в системе, но команда из него не выполняется после обновления?
Чаще всего дело в правах: у файла пропал бит на выполнение, а юнит rc-local.service содержит условие ConditionFileIsExecutable=/etc/rc.local — если оно не выполняется, юнит молча пропускает запуск без ошибки в логах. Проверяйте ls -l /etc/rc.local в первую очередь.
А что с автозапуском Docker-контейнеров — это отдельный механизм?
Да. Restart-policy контейнера (--restart=always и т.п.) хранится в конфигурации самого контейнерного движка, а не в systemd. От systemd там зависит только демон движка — если docker.service не enabled, контейнеры не поднимутся вне зависимости от их собственной restart-policy, потому что поднимать их будет некому.
Как проверить полную опись без реальной перезагрузки продакшн-сервера?
Для unit-файлов поможет systemd-analyze verify и разбор systemctl list-dependencies без остановки сервисов. Для @reboot-задач и SysV-скриптов полноценной безопасной альтернативы реальному ребуту нет — единственный практичный вариант — проверять изменения такого рода на staging-копии сервера перед тем, как вносить их в прод.
Если сервис виден в systemctl list-units как running, значит он точно enabled и переживёт ребут?
Нет, и это частая ошибка. list-units показывает то, что запущено *сейчас* — включая юниты, поднятые вручную через systemctl start без enable. Только list-unit-files --state=enabled (плюс проверка зависимостей для static-юнитов) отвечает на вопрос «поднимется ли это само после ребута».
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →