Реальное приложение почти никогда не живет в одном контейнере. Рядом с ним обычно стоит база данных, часто кеш, иногда отдельный воркер или очередь сообщений. И вся эта компания должна подняться, увидеть друг друга и заработать как единая система.
Поднять такую связку руками через docker run можно, но это быстро превращается в мучение. Чтобы запустить всего лишь приложение и базу, придется проделать примерно следующее:
docker network create myapp-net
docker run -d --name db --network myapp-net \
-e POSTGRES_PASSWORD=pass \
-v db_data:/var/lib/postgresql/data postgres:16
docker run -d --name app --network myapp-net \
-p 8080:80 -e DATABASE_URL=postgres://... myappЭто надо не просто набрать без ошибок и в правильном порядке, а еще и помнить все команды, флаги и связи между ними, чтобы повторить их завтра и на другой машине. Стоит забыть один флаг, и связка не заработает.
Docker Compose убирает эту боль: всю связку контейнеров вы описываете один раз в файле, а поднимаете и гасите ее одной командой.
Один файл описывает все
Compose читает файл docker-compose.yml, который лежит рядом с проектом. В нем перечислены все контейнеры приложения и то, как они связаны. Вот тот же стек из приложения и базы, но уже в виде файла:
services:
app:
build: .
ports:
- "8080:80"
depends_on:
- db
environment:
DATABASE_URL: postgres://user:pass@db:5432/mydb
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:Файл читается почти как обычный текст, но ключевые поля стоит разобрать:
| Поле | Что задает |
|---|---|
services | список контейнеров, каждый сервис это один контейнер |
image | готовый образ из реестра (как postgres:16 для базы) |
build | собрать образ из локального Dockerfile (точка значит «из этой директории») |
ports | проброс портов наружу, как флаг -p |
environment | переменные окружения внутри контейнера |
depends_on | какой сервис должен стартовать раньше |
volumes | постоянное хранилище для данных |
Здесь видно удобную деталь: один сервис берет готовый образ базы из реестра (image: postgres:16), а другой собирается из вашего собственного Dockerfile (build: .). То есть в одном файле спокойно уживаются и чужие готовые образы, и ваше приложение.
Одна команда поднимает все
Когда файл готов, вся связка запускается одной командой из папки с этим файлом:
docker compose up -d
Compose сам соберет образ приложения, скачает образ базы, создаст сеть, поднимет оба контейнера и свяжет их. Флаг -d, как и в обычном docker run, отправляет все в фон.
Погасить всю связку так же просто:
docker compose down
Эта команда останавливает и удаляет контейнеры и сеть, которые сама же создала.
Несколько других команд, которые всегда под рукой:
docker compose up -d поднять всю связку в фоне
docker compose down погасить и убрать контейнеры и сеть
docker compose ps показать сервисы этого проекта
docker compose logs -f смотреть логи всех сервисов сразу
docker compose logs -f app смотреть логи одного сервисаКонтейнеры находят друг друга по имени
Тут Compose делает первую из двух вещей, из-за которых его и любят. Он автоматически помещает все сервисы в общую сеть, и внутри этой сети каждый контейнер доступен по имени своего сервиса, как по адресу.
Посмотрите на строку подключения приложения к базе: `postgres://user:pass@db:5432/mydb`. Хостом здесь стоит просто `db`, и это не случайное слово, а имя сервиса базы из того же файла. Приложению не нужно знать IP-адрес базы, который у контейнера меняется при каждом перезапуске. Оно обращается к базе по имени db, а Compose сам направляет запрос в нужный контейнер.
Именно поэтому в docker-compose.yml нигде нет ни одного IP-адреса. Имена сервисов работают как адреса, и это снимает целый класс головной боли.
Данные переживают перезапуск
Вторая важная вещь касается данных. Контейнеры устроены так, что все, что приложение записало внутрь контейнера, пропадает вместе с ним при удалении. Для приложения без состояния это нормально, а вот база данных так жить не может: перезапустили контейнер базы, и все таблицы исчезли.
Решает это том (volume) – хранилище, которое живет отдельно от контейнера, на стороне хоста, и подключается к нему. Контейнер пишет данные как обычно, но лежат они в томе, поэтому при пересоздании контейнера данные остаются на месте.

В нашем файле за это отвечают две строки. Снизу в блоке `volumes` объявлен том `db_data`, а внутри сервиса базы строка `db_data:/var/lib/postgresql/data` подключает этот том в каталог, где PostgreSQL хранит данные. Теперь docker compose down и повторный up не теряют ни одной записи. Если же том нужно стереть вместе с данными, для этого есть отдельная команда docker compose down -v, которая удаляет и тома тоже, так что с ней стоит быть аккуратнее.
Подводный камень с depends_on
Здесь прячется ошибка, на которую стоит обратить внимание заранее. Поле depends_on гарантирует только то, что контейнер базы запустится раньше контейнера приложения. Но запуск контейнера и готовность сервиса внутри него – разные вещи. Контейнер с PostgreSQL уже может считаться запущенным, пока сама база еще инициализируется и не принимает подключения. Приложение в этот момент стучится к базе, получает отказ и падает.
То есть depends_on решает вопрос порядка, а не готовности. Грамотное приложение должно само уметь подождать и переподключиться, если база еще не отвечает, либо в Compose настраивают проверку готовности через healthcheck. Знать про это полезно сразу, потому что капризное поведение в духе «то работает, то падает при старте» почти всегда упирается именно в этот зазор.
Еще одна частая мелочь связана с самим форматом. YAML чувствителен к отступам, и отступы в нем делаются только пробелами, табы запрещены. Сбитый на один пробел отступ ломает весь файл, поэтому при странных ошибках разбора в первую очередь проверяют именно отступы.
Зачем это DevOps
Compose превращает целое окружение в один текстовый файл, который лежит в репозитории рядом с кодом. Новому человеку в команде не нужно вручную ставить базу, кеш и настраивать связи между ними. Он клонирует репозиторий, запускает docker compose up, и через минуту у него поднят весь стек ровно в том виде, в каком он у всех остальных. То же самое окружение легко поднять и на тестовом сервере. По сути это описание инфраструктуры в виде кода, с которым вы будете встречаться дальше на каждом шагу.
Факт. Compose начинался не внутри Docker. В 2014 году отдельная команда сделала инструмент под названием Fig, который умел поднимать несколько контейнеров из одного файла. Идея оказалась настолько удачной, что Docker купил проект и превратил его в
docker-compose, отдельную утилиту, которую ставили вручную. Позже Compose переписали и встроили прямо в Docker, и теперь команда пишется через пробел:docker compose. Поэтому в старых статьях вы встретитеdocker-composeчерез дефис, а в новыхdocker composeчерез пробел, и это одно и то же, просто разные поколения одного инструмента.