Запускать один контейнер командой docker run — просто. Но реальный проект редко состоит из одного сервиса. Обычно это связка: бэкенд + база данных + кеш, иногда очередь задач и Nginx поверх всего этого. Каждый сервис — отдельный контейнер. Без инструмента оркестрации ты запускаешь их вручную, следишь за порядком запуска, пробрасываешь порты и переменные окружения — и это быстро превращается в ад из bash-скриптов.

Docker Compose решает задачу иначе. Ты описываешь все сервисы в одном файле docker-compose.yml и запускаешь всё одной командой. Compose сам создаёт общую сеть для контейнеров, управляет томами и перезапускает упавшие сервисы.

Важная оговорка сразу: Compose — не Kubernetes. Он не умеет распределять нагрузку по нескольким серверам, не делает авто-масштабирование, не управляет кластером. Но для одного сервера, на котором живёт небольшой проект, — это идеальный инструмент. Быстро, понятно, без лишних слоёв абстракции.

Базовая структура файла

Всё начинается с docker-compose.yml. Вот минимальный пример для проекта на Node.js с PostgreSQL:

version: '3.9'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://user:password@db:5432/mydb
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:15
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    restart: unless-stopped

volumes:
  postgres_data:

Что здесь происходит:

  • app — бэкенд, собирается из Dockerfile в текущей директории
  • db — готовый образ PostgreSQL 15
  • depends_on — Compose запустит db перед app
  • volumes — именованный том, данные базы сохраняются между перезапусками
  • restart: unless-stopped — контейнер автоматически поднимается после сбоя или перезагрузки сервера

Основные команды:

docker compose up -d           # запуск в фоне
docker compose down            # остановка и удаление контейнеров
docker compose logs -f app     # логи конкретного сервиса
docker compose restart app     # перезапуск одного сервиса

Переменные окружения — не в файл, в .env

Хранить пароли прямо в docker-compose.yml — плохая идея, особенно если файл попадёт в репозиторий. Правильный подход — файл .env рядом с docker-compose.yml:

POSTGRES_USER=user
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=mydb
DATABASE_URL=postgres://user:supersecret@db:5432/mydb

Compose подхватывает .env автоматически. В самом docker-compose.yml используешь переменные через ${VAR_NAME}:

environment:
  POSTGRES_USER: ${POSTGRES_USER}
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

.env добавляешь в .gitignore — и никаких секретов в репозитории. Для продакшена используешь один .env с реальными данными, для разработки — другой. Один и тот же docker-compose.yml работает в обоих случаях.

Сети — как контейнеры видят друг друга

По умолчанию Compose создаёт одну общую сеть для всех сервисов. Внутри этой сети контейнеры обращаются друг к другу по имени сервиса. Поэтому бэкенд обращается к базе через db:5432, а не через IP-адрес.

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

services:
  nginx:
    image: nginx
    networks:
      - public

  app:
    build: .
    networks:
      - public
      - internal

  db:
    image: postgres:15
    networks:
      - internal

networks:
  public:
  internal:

Теперь Nginx видит бэкенд, бэкенд видит базу, но Nginx не имеет прямого доступа к PostgreSQL. Это не просто эстетика — это снижает поверхность атаки: если Nginx скомпрометируют, к базе напрямую не добраться.

Nginx как reverse proxy

Nginx в Docker — стандартная история для малых проектов. Он принимает входящие запросы, раздаёт статику и проксирует запросы к бэкенду.

nginx:
  image: nginx:alpine
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - ./nginx/conf.d:/etc/nginx/conf.d:ro
    - ./certbot/conf:/etc/letsencrypt:ro
    - ./certbot/www:/var/www/certbot:ro
  depends_on:
    - app
  restart: unless-stopped

Конфиг Nginx лежит в ./nginx/conf.d/ на хосте и монтируется в контейнер. Менять конфиг можно без пересборки образа — просто меняешь файл и делаешь docker compose restart nginx.

SSL через Certbot добавляется отдельным сервисом. Certbot обновляет сертификаты, Nginx их подхватывает. Всё это описывается в одном docker-compose.yml и разворачивается за считанные минуты на чистом сервере.

Healthcheck — не надейся на depends_on

depends_on гарантирует только порядок запуска контейнеров, но не то, что сервис внутри готов принимать запросы. PostgreSQL стартует быстро, но инициализация базы занимает несколько секунд. Если бэкенд попытается подключиться раньше — упадёт с ошибкой подключения.

Решение — healthcheck:

db:
  image: postgres:15
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
    interval: 5s
    timeout: 5s
    retries: 5

app:
  depends_on:
    db:
      condition: service_healthy

Теперь app запустится только когда db вернёт статус healthy. Те же проверки настраиваются для Redis, RabbitMQ и любого другого сервиса.

Ограничение ресурсов

На VPS с 2 ГБ оперативки всё работает нормально — пока PostgreSQL не решит взять 1.5 ГБ. Чтобы такого не было, выставляй лимиты:

services:
  db:
    image: postgres:15
    deploy:
      resources:
        limits:
          memory: 512m
          cpus: '0.5'
        reservations:
          memory: 256m

limits — максимум, который контейнер может взять. reservations — гарантированный минимум. Если контейнер превысит лимит памяти, Docker его убьёт и перезапустит (при настроенном restart). Это предсказуемее, чем OOM killer на уровне ОС, который может убить что угодно.

Несколько окружений из одного файла

Compose поддерживает переопределение файлов. Базовая конфигурация — в docker-compose.yml, дополнения для конкретного окружения — в отдельных файлах:

docker-compose.yml          # базовый конфиг
docker-compose.dev.yml      # переопределения для разработки
docker-compose.prod.yml     # переопределения для продакшена

Запуск для разработки:

docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

В docker-compose.dev.yml добавляешь монтирование исходного кода (чтобы изменения применялись без пересборки), отключаешь restart: unless-stopped, добавляешь Mailhog для перехвата почты. В docker-compose.prod.yml — ограничения по памяти, логирование в JSON, продакшен-специфичные переменные.

Обновление без лишнего даунтайма

Для малых проектов полноценный Blue-Green деплой избыточен, но обновить контейнер с минимальным простоем можно и с Compose.

Простой способ:

docker compose build app
docker compose up -d --no-deps app

Флаг --no-deps говорит Compose не трогать зависимые сервисы. База и Nginx остаются работать, обновляется только бэкенд. Пока app поднимается (обычно 5–15 секунд), Nginx вернёт 502. Для большинства небольших проектов это приемлемо.

Если нужен нулевой даунтайм — смотри в сторону Docker Swarm или Kubernetes. Но для проекта на одном сервере секундный перерыв в 3 часа ночи незаметен.

Типичная ошибка — данные внутри контейнера

Контейнеры эфемерны. Удалишь контейнер базы данных без именованного тома — потеряешь данные. Это очевидно, когда знаешь, но в спешке легко налететь.

Правило: всё, что должно пережить перезапуск контейнера — в именованный том или монтируй из хоста. Базы данных, загружаемые файлы, сертификаты.

Полезные команды:

docker volume ls                         # все тома
docker volume inspect имя_тома           # содержимое
docker compose exec db pg_dump -U user mydb > backup.sql  # бэкап Postgres

Бэкап стоит автоматизировать через cron на хосте: два раза в день, дамп базы, ротация старых файлов — и никаких сюрпризов.

Когда Compose перестаёт хватать

Compose хорошо работает до определённого масштаба. Есть сигналы, что пора думать о чём-то другом:

  • Проект нужно распределить по нескольким серверам
  • Нужно авто-масштабирование под нагрузкой
  • Сервисов больше 15, и конфиг становится неуправляемым
  • Нужны rolling updates без даунтайма для критичного сервиса

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

Хорошее правило: если проект живёт на одном VPS и не испытывает проблем с производительностью — Docker Compose полностью справляется.

Практика: разворачиваем стек за 10 минут

Реальный сценарий: чистый VPS с Ubuntu, нужно поднять сайт на Node.js с PostgreSQL и Nginx с SSL.

  1. Устанавливаешь Docker и Docker Compose Plugin (официальная документация, 5 минут)
  2. Клонируешь репозиторий с проектом и docker-compose.yml
  3. Создаёшь .env с реальными паролями
  4. Запускаешь docker compose up -d
  5. Получаешь SSL через Certbot
  6. Готово

На следующем сервере — то же самое, те же 10 минут. Никаких «а вдруг там другая версия Node», никаких «у меня работало». Всё окружение описано в коде и воспроизводится точь-в-точь.

Именно это ценят в Docker больше всего — предсказуемость. В REEXY при разработке корпоративных сайтов и интернет-магазинов среда разработки совпадает с продакшен-средой: те же версии, те же зависимости, только тестовые данные. Это убивает целый класс проблем «у меня работает, у клиента нет».

Итого: когда брать Compose

Docker Compose подходит, если:

  • Проект живёт на одном сервере
  • 2–10 сервисов в стеке
  • Команда небольшая или разработчик один
  • Нет требований к нулевому даунтайму
  • Хочется просто и надёжно, без Kubernetes-оверхеда

Это не временное решение «пока не вырастем». Огромное количество production-проектов годами работает на Compose и не испытывает проблем. Инструмент простой, предсказуемый и хорошо задокументированный — именно то, что нужно, когда у тебя нет DevOps-команды в пять человек.