Docker Compose в продакшне: best practices для небольшого бизнеса
В 90% случаев, когда клиент приходит с жалобой «у нас есть Docker Compose, но после каждого обновления всё почему-то рушится», проблема в том самом стартовом compose-файле — его кто-то когда-то «на коленке» написал для тестов и забыл доработать. Мы используем Docker Compose для десятков клиентских инфраструктур. Дальше — что обязательно должно быть в каждом боевом compose-файле, чтобы он работал как часы.
Структура проекта
В продакшне один compose-файл точно не справится. У нас всегда минимум три таких файла:
docker-compose.yml— база, общая для всех сред.docker-compose.override.yml— локальная разработка, bind-mounts, дебаг.docker-compose.prod.yml— продакшн-оверрайды: лимиты, рестарты, логирование.
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Это помогает не запутаться: у нас всегда один источник истины и чёткое понимание, где что лежит.
Restart policy — никаких unless-stopped на всё подряд
В каждом сервисе должен быть restart. Но не бездумный always, иначе сломанный контейнер будет перезапускаться в петле и жечь CPU.
| Политика | Когда использовать |
|---|---|
| no | Одноразовые init-контейнеры |
| on-failure[:N] | Джобы, которые могут упасть по ошибке |
| unless-stopped | Обычные сервисы — автозапуск при рестарте хоста, но не при ручном stop |
| always | Критичные службы (reverse proxy), но с осторожностью |
Healthcheck — без этого compose не знает, живы ли вы
Нет healthcheck? Docker просто считает контейнер «здоровым», если процесс не упал. Но вот загвоздка: PostgreSQL внутри может ещё инициализироваться, а зависимый сервис уже ломится коннектами. Итог понятен.
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
api:
image: myapp:1.4.2
depends_on:
db:
condition: service_healthy
Я всегда ставлю start_period — он не считает неудачи первые N секунд, пока сервис просыпается.
Ресурсы: лимиты и резервы
Без лимитов один сервис с утечкой памяти запросто положит весь хост. Чтобы такого не случилось, задавайте их явно.
services:
api:
image: myapp:1.4.2
mem_limit: 1g
mem_reservation: 512m
cpus: "1.5"
pids_limit: 200
restart: unless-stopped
На нашей практике: выдайте сервису в 1.5 раза больше ресурсов, чем он обычно потребляет в пике. Это страховка от короткого всплеска, но не даст ему «съесть» соседей.
Логирование с ротацией
Не ограничите логи — они съедят диск за неделю. Бывало такое: 1С-шлюз писал 2 ГБ/день в json-файл, и через две недели раздел на 40 ГБ был забит под завязку! Как решаем:
services:
api:
image: myapp:1.4.2
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
compress: "true"
Идеально, конечно, отправлять логи в централизованный стек, например, Loki или ELK, используя syslog/gelf-драйверы. Но даже если нет такой возможности, базовая ротация должна быть всегда и везде.
Секреты и переменные окружения
Пароли никогда, слышите, никогда не храните прямо в compose-файле! Вот варианты, расположенные по возрастанию безопасности:
.envрядом с compose,chmod 600, в git-е игнорим.env_file:с отдельным файлом для каждого сервиса.docker secrets(только в Swarm mode) — пароль виден как файл внутри контейнера.- Как мы управляем секретами? Используем внешний Vault: все нужные данные подтягиваются прямо на старте нашим скриптом-обёрткой. Чётко и безопасно.
# .env
DB_PASSWORD=StrongPassword123!
# docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
Сети и изоляция
Не собирайте все сервисы в одну большую сеть. Разделяйте их на публичные и внутренние:
networks:
frontend:
backend:
internal: true
services:
web:
image: nginx:alpine
networks: [frontend, backend]
ports: ["80:80", "443:443"]
api:
image: myapp:1.4.2
networks: [backend]
db:
image: postgres:16
networks: [backend]
База данных и API не должны «торчать» наружу. Nginx выступает единственной точкой входа.
Реальный кейс: магазин автозапчастей, 160 заказов в день
Помню, как в 2024 году к нам обратился классный интернет-магазин автозапчастей. У них было 160 заказов ежедневно, 22 сотрудника в офисе в Мытищах — неплохой такой бизнес! Но была одна беда: их старый compose-файл совершенно не справлялся с реальной нагрузкой. Там не было ни healthcheck, ни лимитов, ни даже ротации логов. В итоге, после каждого обновления платёжный модуль постоянно падал, потому что база данных банально не успевала нормально запуститься. Наша команда полностью переписала их compose-файл всего за 3 рабочих дня, приведя его в соответствие со всеми продакшн-стандартами. Мы добавили healthcheck во все сервисы, установили лимиты по 2 ГБ на каждый контейнер, и, конечно же, перенесли все логи в Loki, который крутится на нашем собственном сервере Dell Xeon Platinum 8280.
Стоимость этой работы составила 62 000 руб. Результат? За следующие 3 месяца — ни одного падения при деплоях! А время обновления сократилось с 15 минут простоя до каких-то 10 секунд. Естественно, клиент остался у нас на абонентском обслуживании.
Обновления без даунтайма
Для простых сервисов, например, stateless API за прокси, есть свои особенности:
# В compose.yml
services:
api:
image: myapp:${TAG}
deploy:
replicas: 2
restart: unless-stopped
# Обновляем
TAG=1.4.3 docker compose up -d --no-deps api
Compose пересоздаст контейнеры по очереди, если перед ними стоит traefik или nginx с upstream-ами — запросы не потеряются.
Бэкапы томов
Бэкапить /var/lib/docker/volumes целиком — плохая идея, потому что файлы базы могут быть в неконсистентном состоянии. Правильно — dump внутри контейнера:
# Бэкап Postgres
docker exec pg pg_dumpall -U postgres | gzip > /backup/pg-$(date +%F).sql.gz
# Бэкап MySQL
docker exec mysql mysqldump --all-databases -uroot -p$MYSQL_ROOT_PASSWORD | \
gzip > /backup/mysql-$(date +%F).sql.gz
# Файлы — просто rsync с тома
rsync -a /var/lib/docker/volumes/userdata/_data/ /backup/userdata/
Автоматизируйте бэкапы через cron. Но обязательно, хотя бы раз в квартал, проверяйте восстановление! Иначе, как мы говорим, бэкап — это просто вера в бэкап, а не реальный инструмент.
Что обязательно должно быть в prod-compose
- Фиксированные теги образов (
postgres:16.2, неlatest). restart: unless-stoppedна долгоживущих сервисах.healthcheckна всём, от чего кто-то зависит.mem_limitиcpusу каждого сервиса.- Что делаем с логами, чтобы не забивали место и всегда были доступны? Есть два проверенных пути. Либо мы настраиваем их ротацию – старые автоматически удаляются или архивируются. Либо, что часто удобнее для анализа, отправляем все логи в центральный коллектор. Выбор зависит от ваших задач.
- Переменные окружения в
.env, не в YAML. - Разделённые сети (frontend / backend).
- Бэкапы баз — это святое, но делать их недостаточно. Мы настроили регулярные dump-бэкапы, чтобы данные всегда были актуальны, и постоянно проверяем их — проводим тесты восстановления, потому что бэкап без проверки восстановления бесполезен.
HEALTHCHECKв Dockerfile, если своё приложение.USERв Dockerfile, не root.
FAQ — Docker Compose в продакшне
- Можно ли использовать docker compose в продакшне или нужен Kubernetes?
- Для одного-двух хостов и десятка сервисов compose — отличный выбор. K8s имеет смысл от трёх нод и 30+ сервисов, где нужно автомасштабирование и мульти-узловая отказоустойчивость. У большинства малого бизнеса compose закрывает 90% задач.
- Как переносить секреты в compose?
- Не храните пароли в compose-файле. Используйте .env с ограниченными правами (chmod 600), docker secrets для Swarm, либо внешний Vault. Никогда не коммитьте .env в git — добавьте в .gitignore.
- Нужно ли задавать лимиты памяти и CPU?
- Да, обязательно. Без лимитов утекающий сервис съедает весь хост. Минимум: mem_limit или deploy.resources.limits.memory. Для CPU — cpus: '1.5', например.
- Как делать zero-downtime deploy на compose?
- Compose умеет rolling-update через docker compose up -d с новым тегом — контейнер пересоздаётся с даунтаймом 1–3 секунды. Для полного zero-downtime ставьте nginx/traefik перед двумя инстансами и переключайте.
- Как бэкапить данные в volumes?
- Простой способ — остановить контейнер и скопировать /var/lib/docker/volumes/имя/_data. Для баз — dump внутри контейнера: docker exec pg pg_dumpall -U postgres > dump.sql. Автоматизируйте через cron или restic.
