Запускать один контейнер командой 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.
- Устанавливаешь Docker и Docker Compose Plugin (официальная документация, 5 минут)
- Клонируешь репозиторий с проектом и
docker-compose.yml
- Создаёшь
.env с реальными паролями
- Запускаешь
docker compose up -d
- Получаешь SSL через Certbot
- Готово
На следующем сервере — то же самое, те же 10 минут. Никаких «а вдруг там другая версия Node», никаких «у меня работало». Всё окружение описано в коде и воспроизводится точь-в-точь.
Именно это ценят в Docker больше всего — предсказуемость. В REEXY при разработке корпоративных сайтов и интернет-магазинов среда разработки совпадает с продакшен-средой: те же версии, те же зависимости, только тестовые данные. Это убивает целый класс проблем «у меня работает, у клиента нет».
Итого: когда брать Compose
Docker Compose подходит, если:
- Проект живёт на одном сервере
- 2–10 сервисов в стеке
- Команда небольшая или разработчик один
- Нет требований к нулевому даунтайму
- Хочется просто и надёжно, без Kubernetes-оверхеда
Это не временное решение «пока не вырастем». Огромное количество production-проектов годами работает на Compose и не испытывает проблем. Инструмент простой, предсказуемый и хорошо задокументированный — именно то, что нужно, когда у тебя нет DevOps-команды в пять человек.