lenec ru

← все посты

Docker Compose в продакшене: что не так и как с этим жить

17K

Docker Compose — отличная штука, чтобы за пять минут поднять локально стек из приложения, базы и Redis. Но когда тот же compose.yml уезжает на продакшен сервер, у людей внезапно обнаруживается, что часть привычных удобств работает не так, как казалось. И что там, где казалось «оркестратор», его на самом деле нет.

Я держу несколько небольших проектов на Compose в проде. Покажу, в какие грабли я наступал и как сейчас собираю минимальный, но устойчивый сетап.

Compose — это не оркестратор

Главное, что не очевидно сразу: Compose — это утилита, которая запускает контейнеры на одной машине из декларативного файла. Никакого автоматического распределения по нодам, никакого rolling update без даунтайма, никакого встроенного healthcheck-driven рестарта вроде Kubernetes.

Если у тебя один сервер и нагрузка не критичная — Compose в проде нормальная история. Если требования жёстче — лучше сразу смотреть в сторону Nomad, Swarm-mode или Kubernetes. Я говорю про первый сценарий.

Что чаще всего ломается

Перезапуск сервисов и потеря состояния

Без явного restart контейнер после ребута сервера просто не поднимется. Дальше — бесконечная серия странных писем «сайт лежит после обновлений ядра».

services:
  app:
    image: ghcr.io/me/app:1.4.2
    restart: unless-stopped

Обычно я ставлю unless-stopped. always тоже работает, но он перезапустит контейнер, даже если ты его специально остановил руками — на проде это часто мешает.

Latest и неуправляемые апдейты

Тег latest в продакшене — это лотерея. Сегодня там одна сборка, завтра — другая. docker compose pull && docker compose up -d и поехали в неизвестность.

Я всегда фиксирую конкретный тег:

services:
  app:
    image: ghcr.io/me/app:1.4.2
  postgres:
    image: postgres:17.2
  redis:
    image: redis:7.4.1

А ещё лучше — фиксировать через digest:

image: postgres:17.2@sha256:abcd...

Тогда даже если кто-то перезаливает тег с тем же именем, у тебя в проде ничего не меняется.

Volume и пути

Грабли номер один — кто-то монтирует ./data, потом переезжает на другой хост и обнаруживает, что данных нет. Для прода я использую именованные volume:

services:
  postgres:
    image: postgres:17.2
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    secrets:
      - db_password

volumes:
  pgdata:

secrets:
  db_password:
    file: ./secrets/db_password.txt

Так данные живут отдельно от рабочей директории и переживают docker compose down.

Healthcheck и порядок старта

В compose есть depends_on, но по умолчанию он только ждёт, пока контейнер БД создастся. Не пока Postgres реально начнёт принимать соединения. Если приложение стартует быстрее, чем Postgres успевает подняться, — приложение крашится.

Лечится связкой healthcheck + condition:

services:
  postgres:
    image: postgres:17.2
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10

  app:
    image: ghcr.io/me/app:1.4.2
    depends_on:
      postgres:
        condition: service_healthy

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

Дефолтный драйвер логов в docker — json-file без лимитов. На активном сервисе через пару месяцев /var/lib/docker распухает на десятки гигабайт, и вот ты ищешь, чем это занят диск, в три часа ночи.

Я задаю лимит явно — либо в daemon.json, либо в самом сервисе:

services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

Сеть и порты на 0.0.0.0

Если ты в compose написал ports: ["5432:5432"] на сервере, у которого нет настроенного фаервола, ты только что выставил Postgres в интернет. Я сам на это однажды напоролся: видел подозрительные коннекты в логе через 12 минут после up -d.

Есть два варианта:

  • не публиковать порт вообще, если он нужен только другим контейнерам в той же compose-сети;
  • публиковать только на localhost: "127.0.0.1:5432:5432".

Деплой через compose: как обновляюсь

Деплой на сервере у меня сводится к двум командам в каталоге проекта:

docker compose pull
docker compose up -d --remove-orphans

Compose обновит только те контейнеры, у которых изменился образ. Сервисы, у которых ничего не поменялось, останутся теми же. Полного перезапуска нет — только то, что реально нужно.

Перед обновлением я делаю снимок текущего состояния:

docker compose ps
docker compose config > /tmp/compose-snapshot-$(date +%F).yml

Так у меня всегда есть точная копия рендера compose-файла на дату апдейта.

Бэкапы базы — отдельной командой, не в compose-сервисе

Соблазнительно сделать service: backup, который раз в день делает pg_dump. Я пробовал — мне не понравилось. restart: unless-stopped не подходит для разовых задач, в логи ничего нормального не уходит, ошибки теряются.

У меня бэкапы лежат в systemd-таймере на хосте, который запускает docker compose exec postgres pg_dump ... и кладёт результат в S3 через rclone. Compose остаётся для долгоживущих сервисов, а коротко-живущие задачи живут в systemd. Так и логи человеческие, и retry-логику можно нормально написать.

Минимальный продовый compose, к которому я прихожу

services:
  app:
    image: ghcr.io/me/app:1.4.2
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@postgres:5432/app
      NODE_ENV: production
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "127.0.0.1:3000:3000"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

  postgres:
    image: postgres:17.2
    restart: unless-stopped
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 10
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

volumes:
  pgdata:

Сверху сидит nginx (на хосте, не в compose), термирует TLS и проксирует на 127.0.0.1:3000. Получается простой и понятный сетап без оркестратора, который я могу один поддерживать на нескольких VPS без выходных дней.

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

Я честно признаюсь себе, что пора съезжать с Compose, если:

  • сервис не выдерживает даже секундный даунтайм — нужен rolling update;
  • нагрузка перестаёт влезать в одну машину;
  • команда выросла и хочется единого деплой-конвейера;
  • появляются stateful-сервисы, которым нужен failover.

На таком этапе обычно проще взять Nomad или Kubernetes (даже в одной из managed-облачных версий). Compose — это про «один сервер, простая нагрузка, понятные сервисы». В этом сегменте он всё ещё лучший по соотношению трудоёмкость/польза.

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

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

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