Docker Compose в продакшене: что не так и как с этим жить
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-orphansCompose обновит только те контейнеры, у которых изменился образ. Сервисы, у которых ничего не поменялось, останутся теми же. Полного перезапуска нет — только то, что реально нужно.
Перед обновлением я делаю снимок текущего состояния:
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 — это про «один сервер, простая нагрузка, понятные сервисы». В этом сегменте он всё ещё лучший по соотношению трудоёмкость/польза.