docker dockercompose

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

Поднять такую связку руками через 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 через пробел, и это одно и то же, просто разные поколения одного инструмента.