Docker Compose в продакшне: best practices — АйТи Фреш
· 17 мин чтения

Docker Compose в продакшне: best practices для небольшого бизнеса

Docker Compose в продакшне: best practices для небольшого бизнеса

Привет! Я Евгений Сергеевич Семёнов, директор ITFresh. Знаете, какая картина всплывает в 90% случаев, когда к нам приходит клиент и говорит: «У нас есть Docker Compose, но после каждого обновления всё почему-то рушится»? Проблема почти всегда в том самом стартовом compose-файле. Его, скорее всего, кто-то когда-то «на коленке» для тестов написал, а потом просто забыли как следует доработать. Мы в ITFresh используем Docker Compose для десятков клиентских инфраструктур. Наша команда накопила огромный опыт! Сейчас поделюсь, что обязательно должно быть в каждом боевом compose-файле, чтобы он работал как часы.

Структура проекта

В продакшне один compose-файл точно не справится. У нас всегда минимум три таких файла:

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-файле! Вот варианты, расположенные по возрастанию безопасности:

  1. .env рядом с compose, chmod 600, в git-е игнорим.
  2. env_file: с отдельным файлом для каждого сервиса.
  3. docker secrets (только в Swarm mode) — пароль виден как файл внутри контейнера.
  4. Как мы управляем секретами? Используем внешний 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

Приведём ваш 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.

Подпишитесь на рассылку ITfresh

Устали от общей информации? Каждую неделю мы делимся чисто практикой! Готовим гайды для руководителей IT и системных администраторов. Что внутри? Всё, что нужно для работы: от тонкостей безопасности до грамотной миграции, секреты 1С, проверенные методы резервного копирования и, конечно, лайфхаки, которые мы сами вытащили из наших реальных проектов. Полезно? Ещё бы!

Письмо придёт в течение минутыНе нашли его во «Входящих» — загляните в папку «Спам» или «Промоакции» и нажмите «Не спам». Так все следующие выпуски будут приходить прямо в основную почту.
Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.