SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-25

Автозапуск 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 по-прежнему полезен. Он подходит для контейнера, выполняющего задачу, которая может завершиться с ошибкой, если вам нужно ограничить количество попыток перезапуска и избежать бесконечного цикла. Это неподходящий инструмент для поддержания работоспособности долгоживущего сервиса после перезагрузок.

Политики перезапуска работают только при автозапуске 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 управляет порядком при загрузке, а демон управляет контейнером, если тот аварийно завершится в три часа ночи.

Юнит выглядит иначе, если вы поддерживаете работу обычного долгоживущего процесса, а не стека, так как в этом случае под ним нет демона, и функцию контроля берет на себя Restart= самого systemd; запуск 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 запустит его снова.