lenec ru

← все посты

Docker Compose в продакшене: что не так и где аккуратно

12K

Docker Compose часто используют как «продакшен-оркестратор для бедных»: один VPS, пара сервисов, простой docker compose up -d и пошёл. Я через это прошёл и сейчас на части проектов от Compose принципиально отказался, на части — оставил с оговорками. Расскажу, что именно у меня болело и как я с этим живу.

Сразу скажу: Compose — нормальный инструмент. Если ты понимаешь его ограничения, его можно использовать в проде. Но «по умолчанию» он не делает того, что нужно от прод-окружения, и эти пробелы ловятся в самые неудобные моменты.

Что не так с Compose в проде

1. Деплой = downtime

Базовый сценарий docker compose up -d для изменённого сервиса:

  1. Compose останавливает старый контейнер.
  2. Запускает новый с обновлённым образом.
  3. Между этими шагами сервис недоступен.

На простом блоге это терпимо. На API, который обслуживает фронт — пользователи получают 502 на пару секунд. Без отдельного балансировщика и health-check downtime не убрать.

Что я обычно делаю:

  • На простых сервисах — оставляю downtime, признаю его, делаю деплой ночью.
  • На критичных — поднимаю blue/green-схему: два набора контейнеров, перед ними nginx, который переключает upstream.

2. Health checks — формальность

Compose умеет healthcheck, но при up -d по умолчанию не ждёт, пока новый контейнер станет здоровым. Опция --wait помогает, но и она не делает rolling update — она просто ждёт.

services:
  api:
    image: myapp/api:1.2.3
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 20s

Без healthcheck Compose считает, что контейнер запущен в момент, когда внутри запустился процесс. Долгий старт Node-приложения с подключением к БД на 10–15 секунд означает, что nginx уже отправляет запросы туда, куда не следует.

3. Секреты лежат текстом в .env

Большинство Compose-сетапов хранят DATABASE_URL, JWT_SECRET и прочее в файле .env. Этот файл часто оказывается в git-репозитории, на резервном дампе диска, в чьём-то домашнем каталоге. Compose поддерживает secrets через файлы, но мало кто этим пользуется на одиночном VPS.

Минимум, что я делаю:

  • .env на сервере с правами 600 для пользователя, под которым работает Docker.
  • В git — только .env.example с заглушками.
  • На больших проектах — Vault или хранилище секретов хостера.

4. Логи без ротации

По умолчанию Docker пишет логи в JSON-файлы и не ограничивает их размер. На многословном Node-приложении это означает, что через месяц у тебя 50 GB логов в /var/lib/docker/containers/. Лимиты ставятся в Compose:

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: '50m'
        max-file: '5'

Или глобально в /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5"
  }
}

Без этого первый «странный сервер тормозит» через полгода — это диск, забитый логами.

5. Сеть и порты

Compose открывает порты через ports: - "3000:3000". Это значит, что Docker сам обходит iptables — твои настройки UFW его не остановят. Если ты не хочешь, чтобы порт был открыт во внешний мир, надо явно указывать localhost:

ports:
  - "127.0.0.1:3000:3000"

Меня эта особенность ловила дважды. Один раз обнаружил публичный 5432 на VPS, потому что забыл префикс 127.0.0.1:.

6. Переменные окружения и .env

Compose читает файл .env в текущем каталоге как переменные подстановки в YAML. Это удобно, но ровно один раз больно ловишь, когда переменная в YAML и переменная внутри контейнера — это разные вещи. environment: в сервисе — это переменные внутри. $$ и ${VAR} в YAML — это переменные на уровне Compose. Путаются часто.

7. restart: always vs systemd

Compose умеет рестартить контейнеры. systemd тоже умеет. На одном VPS это часто пересекается. Я выбираю restart: unless-stopped в Compose и не оборачиваю Compose в systemd — если очень нужно, делаю один compose@.service:

[Unit]
Description=Compose stack %i
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/stacks/%i
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target

Это даёт чистый старт стека после reboot и контролируемый compose down.

Когда Compose в проде нормальный

Я готов оставить Compose как продовое решение, если все пункты выполнены:

  • Один VPS, не нужен failover.
  • 2–5 сервисов, не больше.
  • Допустим короткий downtime при деплое.
  • Команда понимает, как смотреть логи и состояние контейнеров.
  • Бэкапы данных делаются вне Compose (БД через managed или отдельный pg_dump).

В таких условиях Compose — отличный лёгкий вариант. Не нужно ставить kubernetes ради 5 контейнеров.

Когда Compose уже не годится

Я перехожу на что-то другое, когда:

  • Нужен zero-downtime deploy. Вариант — Docker Swarm, k3s или Nomad. Все три позволяют rolling update без танцев с blue/green.
  • Нужно несколько серверов и автоматика «один упал — нагрузка перешла на другой». Compose так не умеет в принципе.
  • Команда выросла, и хочется единого UI/CLI для деплоя десятков сервисов.

Дополнительно есть Coolify и Dokploy — open-source инструменты, которые поверх Compose дают удобный UI. Они закрывают часть болячек выше: автогенерация nginx, ACME, базовая ротация. Если хочется нагрузку «между голым Compose и кубом» — стоит посмотреть.

Шаблон Compose, который не стыдно показать

services:
  api:
    image: registry.example.ru/myapp/api:${API_VERSION}
    restart: unless-stopped
    env_file: ./api.env
    networks: [internal]
    expose: ["3000"]
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/healthz || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 20s
    logging:
      driver: json-file
      options:
        max-size: '50m'
        max-file: '5'
    deploy:
      resources:
        limits:
          memory: 1g
          cpus: '1'

  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    depends_on:
      api:
        condition: service_healthy
    networks: [internal]
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - /var/www/certbot:/var/www/certbot:ro
    logging:
      driver: json-file
      options:
        max-size: '50m'
        max-file: '5'

networks:
  internal:
    driver: bridge

Что важно:

  • expose вместо ports для api — порт открыт только внутри Compose-сети, наружу его не видно.
  • depends_on: condition: service_healthy — nginx стартует только после готовности api.
  • deploy.resources.limits — ограничения по памяти и CPU. Без этого один процесс с утечкой может прижать весь VPS.
  • Логирование настроено явно у каждого сервиса.

Деплой

Простейший деплой-скрипт, который у меня лежит в CI:

#!/bin/bash
set -e
VERSION=$(git rev-parse --short HEAD)

docker build -t registry.example.ru/myapp/api:${VERSION} .
docker push registry.example.ru/myapp/api:${VERSION}

ssh deploy@srv bash <<EOF
set -e
cd /opt/stacks/myapp
export API_VERSION=${VERSION}
docker compose pull api
docker compose up -d --no-deps api
docker image prune -f
EOF

--no-deps важно: не трогаем nginx ради обновления только api. Image prune убирает старые образы, иначе диск кончается.

Если нужен zero-downtime, схема усложняется: запускаем два сервиса api-blue и api-green, переключаем upstream nginx через include + reload. На Compose это делается, но каждый шаг — твой шаг. На k3s/Nomad rolling update встроен.

Резюме

  • Compose в проде работает, но требует осознанной настройки: лимиты, health, логи, secrets, restart-policy.
  • Без отдельного балансировщика zero-downtime не получится.
  • Логи и сеть — самые частые источники головной боли. Лимит логов и 127.0.0.1: в портах ставь сразу.
  • На сложных проектах смотри в сторону Swarm/k3s/Nomad. Лучше тот, который команда понимает.

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

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.