Автозапуск Docker Compose при загрузке системы
Настройте автоматический запуск контейнеров после перезагрузки сервера. Узнайте, почему политика restart: on-failure не работает и когда для сервисов нужен systemd unit.
Краткий ответ
Сервисы Docker Compose запускаются при загрузке системы при одновременном выполнении двух условий. Docker daemon должен быть включен как системная служба, а для каждого сервиса в файле должна быть задана политика перезапуска unless-stopped или always. Добавьте restart: unless-stopped в каждый сервис и один раз выполните docker compose up -d — после перезагрузки контейнеры запустятся автоматически. В типичных случаях больше ничего не требуется.
Unit-файл systemd нужен только тогда, когда важен порядок запуска: например, если стек зависит от смонтированного диска, VPN-интерфейса или сетевого ресурса, которые еще не готовы в момент старта Docker daemon. Это реальная задача, и во второй части этого руководства она подробно разобрана. Если вы только начинаете разбираться с описанием сервисов и томами, начните с основ Docker Compose на VPS и вернитесь сюда позже.
Настройка политики перезапуска в compose.yaml
Эта политика задается одной строкой для каждого сервиса. Глобального переключателя не существует, поэтому сервис, для которого вы забыли указать политику, останется выключенным после перезагрузки, в то время как остальные компоненты стека запустятся.
services:
app:
image: nginx:1.27
restart: unless-stopped
ports:
- "8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change-me
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:Примените изменения, а затем проверьте политику запущенного контейнера:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)Команда выведет unless-stopped. Если выводится no, значит, файл был отредактирован, но контейнер не был пересоздан.
Это самая распространенная ошибка. Политика перезапуска сохраняется в самом контейнере, а не в YAML-файле. Редактирование compose.yaml никак не влияет на уже существующий контейнер. Команда docker compose restart также не поможет, так как она останавливает и запускает тот же объект контейнера, не затрагивая его конфигурацию. Только docker compose up -d сравнивает файл с запущенными контейнерами, обнаруживает изменение политики и пересоздает их.
Если вы не хотите пересоздавать контейнер прямо сейчас, измените политику «на лету»:
docker update --restart unless-stopped my-containerПри этом все равно внесите изменения в YAML-файл. Команда docker update изменит работающий контейнер, но при следующем вызове docker compose up -d система прочитает файл и вернет старое значение.
Что именно делает каждое значение restart
Docker определяет четыре значения, и разница между ними проявляется только при перезагрузке машины или перезапуске демона.
no— значение по умолчанию. Контейнер никогда не перезапускается автоматически ни при каких обстоятельствах.alwaysперезапускает контейнер каждый раз, когда он останавливается. Если вы остановили его вручную, он всё равно запустится при следующем старте демона Docker. Это часто становится неожиданностью: контейнер, который вы намеренно остановили на прошлой неделе, снова работает после перезагрузки.unless-stoppedведет себя так же, какalways, за исключением того, что контейнер, остановленный вручную, остается остановленным после перезапуска демона. Это значение подходит для сервиса, который вы периодически отключаете для обслуживания.on-failureперезапускает контейнер только в том случае, если он завершился с ненулевым кодом выхода. Вы можете ограничить количество попыток, как вrestart: on-failure:3.
Для стека, который должен просто работать, пока включен сервер, unless-stopped является правильным значением по умолчанию. Выбирайте always только в том случае, если вам нужен контейнер, который должен оставаться запущенным вопреки всему.
Почему restart: on-failure не работает после перезагрузки
Многие выбирают on-failure, так как это звучит надежно, но после первой же перезагрузки обнаруживают, что все контейнеры остановлены. Причина кроется в определении. on-failure реагирует только на одно событие: завершение процесса контейнера с кодом ошибки.
Перезагрузка не является ошибкой. Когда хост выключается, systemd останавливает docker.service, а демон намеренно останавливает каждый контейнер. Контейнер не завершился с ошибкой, поэтому политике не на что реагировать. При запуске системы демон проверяет, какие контейнеры нужно возобновить, но контейнер с on-failure, который был корректно остановлен, в этот список не входит. Он остается в состоянии exited.
Это можно увидеть на практике. Установите restart: on-failure для сервиса, выполните docker compose up -d, перезагрузитесь, а затем запустите:
docker compose ps -aСервис будет отображаться в состоянии Exited со статусом, похожим на Exited (0) 2 minutes ago. Ничего не сломано, и в логах нет ошибок, что и затрудняет диагностику. Политика сработала именно так, как описано.
on-failure по-прежнему полезен. Он подходит для контейнера, который выполняет задачу и может аварийно завершиться. Сам по себе restart: on-failure не устанавливает лимитов и будет бесконечно перезапускать задачу, которая постоянно падает, поэтому добавьте ограничение, например restart: on-failure:3, если хотите ограничить количество попыток и избежать цикла перезапусков. Это неподходящий инструмент для поддержания работоспособности долгоживущего сервиса после перезагрузок.
Политики перезапуска работают только при автозапуске Docker
Политики перезапуска применяются демоном Docker. Если демон не запущен, политики не действуют. Проверьте это:
systemctl is-enabled docker
systemctl is-enabled containerdОбе команды должны выводить enabled. Пакеты из официального репозитория Docker включают их при установке, поэтому на чистом сервере проверка обычно проходит успешно. Если какая-либо из команд выводит disabled, исправьте это:
sudo systemctl enable --now docker containerdЗдесь есть важный нюанс. В Ubuntu также поставляется docker.socket, который запускает демон по требованию при первом обращении к Docker API. Пользователи видят, что docker.socket включен, полагают, что демон защищен, и отключают docker.service для экономии памяти. При загрузке системы никто не обращается к API, сокет не активируется, демон не запускается, и ни один контейнер не поднимется, пока вы не введете первую команду docker. Активация через сокет не заменяет включенный docker.service.
Когда systemd unit является лучшим решением
Политики перезапуска (restart policies) не учитывают порядок запуска относительно остальной системы. Демон запускается и поднимает контейнеры при первой возможности. Если ваш стек использует bind-mount директории с отдельного тома, NFS (network file system) или зашифрованного диска, контейнеры могут запуститься до того, как этот путь станет доступен. Docker создаст пустую директорию в точке монтирования и запустит контейнер, в результате чего база данных поднимется без данных.
Создавайте systemd unit, если выполняется любое из этих условий. Стеку требуется предварительная готовность точки монтирования, VPN-интерфейса или другого юнита. Вы хотите, чтобы systemctl stop myapp и systemctl start myapp работали так же, как для любого другого сервиса на сервере. Или вы хотите, чтобы стек корректно завершал работу при выключении системы, а не принудительно прерывался вместе с демоном. Если вы не знакомы с systemd units, в руководстве написание systemd service и timer подробно описан формат файлов.
Написание юнита systemd
Разместите стек в фиксированном пути вне домашнего каталога пользователя. /srv/myapp — хороший выбор, так как юнит, который запускается до входа любого пользователя в систему, не должен обращаться к /home.
Создайте /etc/systemd/system/myapp.service:
[Unit]
Description=myapp docker compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
RequiresMountsFor=/srv/myapp/data
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.targetВключите и запустите его:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.serviceИсправный юнит показывает Active: active (exited). При первом просмотре это кажется ошибкой. На самом деле это верно: Type=oneshot с RemainAfterExit=yes означает, что юнит выполнил свою команду, команда завершилась, а systemd сохраняет юнит в состоянии active, чтобы ExecStop выполнился при выключении.
Каждая строка здесь важна. Requires=docker.service означает, что юнит быстро завершится с ошибкой, вместо того чтобы выполнять docker compose для нерабочего сокета. After= задает порядок, так как Requires= сам по себе этого не делает. RequiresMountsFor= заставляет systemd подтянуть юнит монтирования для этого пути и дождаться его готовности — это главная причина использовать юнит вместо политики перезапуска. TimeoutStartSec=0 не дает systemd прервать задачу запуска, пока еще идет загрузка большого образа.
Примечание о совмещении двух механизмов. Документация Docker не рекомендует сочетать политики перезапуска с системным менеджером процессов, утверждая, что это создает конфликты. Конфликт возникает, если менеджер процессов контролирует сам процесс контейнера и перезапускает его, пока демон пытается делать то же самое. Юнит Type=oneshot ничего не контролирует, поэтому использование restart: unless-stopped в файле compose вместе с этим юнитом допустимо и является правильным подходом. systemd управляет порядком запуска при загрузке, а демон обрабатывает контейнер, который аварийно завершился в три часа ночи.
Юнит выглядит иначе, если вы поддерживаете работу обычного долгоживущего процесса, а не стека, так как в этом случае под ним нет демона, и функции контроля берет на себя встроенный в systemd Restart=; запуск dsh в фоновом режиме через systemd — это пример такой конфигурации, включая использование выделенного пользователя и журнала.
Проверка с помощью реальной перезагрузки
Ничто не заменит реальное тестирование. systemctl restart docker не проверяет порядок монтирования, а docker compose down в сочетании с docker compose up -d вообще не затрагивает процесс загрузки.
sudo rebootДождитесь перезагрузки, переподключитесь и выполните проверку в следующем порядке:
uptime
systemctl is-active docker
docker compose psКоманда uptime подтверждает, что вы работаете с машиной, которая действительно была перезагружена. Команда docker compose ps, запущенная из директории стека, должна показать статус running для всех сервисов, а их время работы (uptime) должно быть близким к времени работы самой системы. Сервис со статусом Exited — это тот, который требует внимания.
Если какой-либо компонент не запустился, изучите логи демона за период загрузки:
journalctl -u docker.service -b --no-pager | tail -50Для стека, управляемого юнитом, команда journalctl -u myapp.service -b --no-pager выводит точные данные docker compose с момента загрузки, включая ошибки при вытягивании образа или отсутствие файла .env. Вы контролируете ту перезагрузку, которую запланировали, поэтому позвольте юниту сообщать о тех, которые вы пропустили: строка OnFailure=, настроенная на собственный сервер ntfy, превратит информацию о стеке, который не смог восстановиться, в push-уведомление, вместо того чтобы вы обнаружили проблему спустя несколько дней.
Факторы, незаметно нарушающие автозапуск
Контейнеры, созданные с помощью docker compose run, не наследуют политику перезапуска из файла. Compose воспринимает их как одноразовые контейнеры. Если сервис игнорирует заданную политику, проверьте, не был ли он запущен через run вместо up.
Относительный путь в томе или в записи env_file вычисляется относительно директории, где находится файл compose. Это работает при запуске из оболочки и из юнита, в котором задан параметр WorkingDirectory. Если этот параметр отсутствует, запуск завершится ошибкой, так как рабочей директорией по умолчанию станет /.
Rootless Docker — особый случай. Демон работает как пользовательский сервис, который завершается после закрытия последней сессии пользователя. Включите его для пользователя, чтобы он продолжал работу даже при отсутствии активных сессий:
systemctl --user enable docker
sudo loginctl enable-linger $USERБез enable-linger rootless-демон выключается при выходе из системы, а вместе с ним останавливаются и контейнеры, что выглядит как неисправная политика перезапуска.
И последнее. Автоматические обновления безопасности могут перезагрузить сервер в заданное время; это безопасно, только если ваш стек восстанавливается самостоятельно. Настройку этого процесса следует выполнять в рамках первоначальной подготовки сервера, как описано в первые десять минут на новом VPS.
FAQ
В чем разница между restart: always и restart: unless-stopped?
Оба параметра перезапускают контейнер, если он остановился сам по себе. Разница проявляется, если вы остановили контейнер вручную. При использовании always контейнер запустится снова при следующем старте Docker daemon, поэтому перезагрузка отменит вашу ручную остановку. При использовании unless-stopped демон «запоминает», что контейнер был остановлен намеренно, и не трогает его. Используйте unless-stopped, если вам не требуется, чтобы контейнер обязательно оставался в выключенном состоянии.
Я добавил restart: unless-stopped, но контейнер все равно не запускается после перезагрузки. Почему?
Политика применяется к контейнеру, а не к файлу, и существующий контейнер не обновляется при редактировании YAML. Выполните docker compose up -d, чтобы Compose пересоздал его, затем проверьте результат с помощью docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app). Если команда выводит no, значит, контейнер был создан до внесения правок. Другая частая причина — docker.service не включен; это можно проверить командой systemctl is-enabled docker.
Нужен ли мне systemd unit, если я уже использую политики перезапуска?
Обычно нет. Политики перезапуска достаточно для стека, которому нужна только сеть (это большинство стеков). Добавляйте unit, если контейнеры зависят от чего-то, что не готово к моменту запуска Docker daemon, например, от внешнего диска, зашифрованного тома, NFS-ресурса или VPN-интерфейса. Unit позволяет задать порядок запуска через After= и RequiresMountsFor=, чего политика перезапуска сделать не может.
Как остановить стек навсегда, чтобы он не запускался после следующей перезагрузки?
При использовании unless-stopped достаточно docker compose stop, так как контейнер, остановленный вручную, не возобновляет работу при перезапуске демона. При использовании always остановки недостаточно, и контейнер вернется после перезагрузки. Либо выполните docker compose down, что удалит контейнеры, либо сначала измените политику с помощью docker update --restart no my-container. Если стек управляется через systemd unit, выполните также sudo systemctl disable myapp.service, иначе unit запустит его снова.