Docker Compose в продакшне: best practices для небольшого бизнеса
Привет! Я Евгений Сергеевич Семёнов, директор ITFresh. Знаете, какая картина всплывает в 90% случаев, когда к нам приходит клиент и говорит: «У нас есть Docker Compose, но после каждого обновления всё почему-то рушится»? Проблема почти всегда в том самом стартовом compose-файле. Его, скорее всего, кто-то когда-то «на коленке» для тестов написал, а потом просто забыли как следует доработать. Мы в ITFresh используем 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.
Приведём ваш compose в боевое состояние
У нас на 8 серверах Dell Xeon Platinum 8280 в дата-центре МТС Москва крутится больше 200 контейнеров клиентов. Переведём ваши сервисы на продакшн-стандарт: healthcheck, лимиты, логи, бэкапы, мониторинг. 15+ лет опыта администрирования корпоративных инфраструктур.
Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш
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.
