Docker Compose в продакшене на VPS без сожалений

Политики перезапуска, ротация логов, лимиты ресурсов, обратный прокси с автоматическим TLS и резервная копия томов в две строки.

ИкИнженерная команда CheapServАвтор 8 мин чтения
Светящиеся контейнеры, сложенные стопкой на парящей платформе
На этой странице10
  1. Структура каталогов, которая переживёт вас
  2. Подъём после перезагрузки
  3. Логи, которые не забивают диск
  4. Один взбесившийся процесс не должен класть сервер
  5. Обратный прокси с автоматическим TLS
  6. Секреты
  7. Обновление без сюрпризов
  8. Резервные копии: тома, а не контейнеры
  9. Как узнать о поломке
  10. Чек-лист

Большинство небольших продакшен-стеков на наших тарифах VPS запускается через Docker Compose, и это хороший способ. Сожалеть потом приходится из-за настроек по умолчанию: контейнеры, которые не поднимаются после перезагрузки, логи, забивающие диск к третьему месяцу, один взбесившийся процесс, который кладёт весь сервер, и резервная копия, в которую попали контейнеры, но не данные. У каждой из этих проблем есть решение в две строки. Вот все они — в том порядке, в каком вам понадобятся.

Структура каталогов, которая переживёт вас

По одному каталогу на стек в /srv: файл Compose, .env для секретов и каталог data/ для bind-монтирований, которые вы хотите видеть. Для всего остального — именованные тома. Пусть всё будет предельно скучно:

/srv/app/
  compose.yaml
  .env               # chmod 600, never in git
  data/caddy/        # bind mounts you want to inspect
/var/lib/docker/volumes/   # named volumes, managed by Docker

Приложение Docker, устанавливаемое в один клик, ставит Engine 27 с Compose v2 и размещает данные демона на корневом NVMe-диске. Если позже вы добавите отдельный том, перенесите /var/lib/docker туда через data-root в daemon.json, а не через символическую ссылку.

Подъём после перезагрузки

Обновления ядра перезагружают сервер ночью. Каждый сервис должен явно объявить, что должно с ним произойти:

services:
  app:
    image: ghcr.io/example/app:1.8.2
    restart: unless-stopped
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16.4
    restart: unless-stopped
    volumes: [pgdata:/var/lib/postgresql/data]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      retries: 5
volumes:
  pgdata:

Две привычки из этого фрагмента важнее, чем кажется. Фиксируйте теги образов на конкретной версии, никогда не используйте latest: перезагрузка, которая в 4 утра подтягивает новую мажорную версию PostgreSQL, — это авария, в которой потом не разобраться. А healthcheck-проверки с условиями depends_on не дают приложению уйти в crash-loop, пока база данных ещё запускается.

Логи, которые не забивают диск

По умолчанию Docker хранит каждую строку, которую когда-либо записал любой контейнер, в JSON и навсегда. Приложение, которое активно пишет в лог, набирает 40 GB за несколько месяцев. Исправьте это один раз для всего демона:

cat > /etc/docker/daemon.json <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": { "max-size": "20m", "max-file": "5" },
  "default-address-pools": [{ "base": "172.30.0.0/16", "size": 24 }]
}
EOF
systemctl restart docker

Существующие контейнеры сохраняют старые настройки, пока не будут пересозданы; docker compose up -d --force-recreate применит новые. Строка с пулом адресов — бонус: она не даёт сетям Compose позже пересечься с диапазоном WireGuard или офисной сети.

Один взбесившийся процесс не должен класть сервер

Без лимитов утечка памяти в одном контейнере вынуждает OOM killer ядра выбирать жертву, и нередко выбор падает на базу данных. Задайте лимиты памяти для всего — щедрые, но конечные:

    deploy:
      resources:
        limits:
          memory: 1g
          cpus: "2.0"

Compose v2 учитывает deploy.resources.limits и без Swarm. Подбирайте лимиты так, чтобы их сумма была меньше объёма RAM VPS за вычетом примерно 1 GB для хоста; на VPS-4 с 8 GB это около 6.5 GB, которые нужно распределить. Когда контейнер упирается в свой лимит, перезапускается только он, а журнал событий покажет, какой именно.

Обратный прокси с автоматическим TLS

Привязывайте порты приложений только к localhost и пусть портами 80 и 443 владеет один прокси. Caddy в контейнере сам получает сертификаты и перечитывает конфигурацию при её изменении:

services:
  caddy:
    image: caddy:2.8
    restart: unless-stopped
    ports: ["80:80", "443:443", "443:443/udp"]
    volumes:
      - ./data/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
  app:
    expose: ["3000"]      # reachable by caddy on the compose network, not published
volumes:
  caddy_data:

Caddyfile — это по одной строке на сайт: app.example.com { reverse_proxy app:3000 }. Публикация только портов прокси заодно обходит известную проблему, когда Docker пробивает дыры в ufw: наружу не публикуется ничего, кроме 80 и 443.

Секреты

Храните их в .env и выполните для него chmod 600, ссылайтесь на них в файле Compose как ${DB_PASSWORD} и не добавляйте .env в git, положив рядом .env.example. Если значений больше нескольких,secrets: в файле Compose, монтируемые как файлы, чище переменных окружения, которые утекают в docker inspect и отчёты о сбоях.

Обновление без сюрпризов

cd /srv/app
docker compose pull            # fetches the new pinned tags you edited
docker compose up -d           # recreates only what changed
docker image prune -f          # frees the old layers

Поправьте тег, выполните pull, затем up. Автообновление в стиле Watchtower соблазнительно на одиночном VPS, но именно так несовместимое изменение прилетает в полночь; если вы им пользуетесь, фиксируйте минорные версии, чтобы оно могло применять только патч-релизы.

Резервные копии: тома, а не контейнеры

Контейнеры одноразовы; данные — это тома. Базу данных выгружайте дампом, а не копированием файлов, на которых она работает:

docker compose exec -T db pg_dump -U postgres -Fc app > /srv/backups/app-$(date +%F).dump

Для всего остального подойдёт tar-архив тома, снятый из одноразового контейнера: docker run --rm -v app_uploads:/v -v /srv/backups:/b alpine tar czf /b/uploads-$(date +%F).tgz -C /v .. Отправляйте /srv/backups за пределы сервера с помощью restic; рецепт приведён в материале «Резервные копии, которые действительно восстанавливаются». Снапшот из панели перед крупными обновлениями даёт «отмену» за считанные секунды.

Как узнать о поломке

docker compose ps показывает, здоровы ли контейнеры;docker stats --no-stream показывает, кто съедает RAM. Для постоянного наблюдения дополнение netdata автоматически находит контейнеры и строит графики CPU, памяти и перезапусков по каждому из них. Настройте оповещения на перезапуски: контейнер, который перезапустился три раза за час, сообщает вам о проблеме раньше, чем это сделают ваши пользователи.

Чек-лист

  • Зафиксированные теги, restart: unless-stopped, healthcheck-проверки и условия depends_on.
  • Ротация логов на уровне всего демона.
  • Лимиты памяти на каждом сервисе, в сумме меньше объёма RAM VPS.
  • Один обратный прокси, публикующий порты 80 и 443; всё остальное — во внутренней сети.
  • Секреты в .env с правами 600, вне git.
  • Дампы и архивы томов каждую ночь уходят за пределы сервера, а раз в месяц восстанавливаются где-нибудь для проверки.

VPS-4, настроенный таким образом, годами обслуживает типичный стек от пяти до восьми сервисов, и никому не приходится заходить на него, — для сервера это высшая похвала.

Ик
Инженерная команда CheapServ

Те, кто создаёт конвейер развёртывания, панель управления и слой хранения данных.

Разверните первый сервер меньше чем за минуту.

Пополните баланс от $25 в BTC, ETH, XMR или USDT. Баланс не сгорает, а неиспользованные средства можно вернуть.

Создать аккаунт