Модель Compose: проект, сеть и сервисы, находящие друг друга по имени
Compose превращает топологию из нескольких контейнеров в декларативный YAML: namespace проекта, сеть по умолчанию с DNS по имени сервиса, именованные тома и модель сходимости к желаемому состоянию. Файл — это локальное окружение, а не вики из команд run.
Документ для онбординга назывался «Как поднять стек локально» и содержал 41 нумерованный шаг: создай сеть руками, запусти Postgres с правильным портом и паролем, запусти Redis, собери образ API, запусти его с одиннадцатью флагами окружения, указывающими на двух других по IP, потом воркер, потом фронтенд. Каждый новичок терял на этом день, а страница плыла — шаг 17 всё ещё ссылался на имя контейнера, переименованное в шаге 9. Staff-инженер удалил всю страницу и заменил её одним файлом compose.yaml и одной командой. IP исчезли, потому что сервисы теперь находили друг друга по имени через сеть проекта, которую инструмент создавал сам. Одиннадцать флагов схлопнулись в блок environment. То, что было племенным знанием, размазанным по вики, стало диффом в системе контроля версий, который ревьюер мог прочитать сверху донизу. Выигрыш был не в меньшем числе нажатий клавиш — а в том, что окружение перестало быть последовательностью команд, которую кто-то должен выполнить правильно, и стало описанием желаемого состояния, к которому инструмент сходится.
Проект — это namespace
Файл Compose описывает один проект, и имя проекта (по умолчанию имя каталога или -p) — это префикс на всём, что Compose создаёт: контейнерах, сетях, томах, метках. Этот namespace и делает compose up и compose down идемпотентными — Compose ищет ресурсы по метке com.docker.compose.project, а не угадывает, поэтому повторный up сверяется с существующим проектом, а не плодит дубли. Две копии одного репозитория в двух каталогах дают два независимых проекта с двумя сетями и двумя томами БД, без коллизий. Модель — это сходимость к желаемому состоянию, а не скрипт: ты объявляешь набор сервисов, которые хочешь видеть запущенными, а up сравнивает это с тем, что есть, и делает минимум изменений — стартует недостающее, пересоздаёт изменённое, оставляет совпадающее. Поэтому правка образа одного сервиса и повторный up пересоздают именно этот контейнер, а не весь стек.
# compose.yaml — всё локальное окружение как данные
services:
api:
build: .
environment:
DATABASE_URL: postgres://app:secret@db:5432/app # "db" — DNS-имя
REDIS_URL: redis://cache:6379
ports:
- "8080:8080"
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data # именованный том переживает `down`
cache:
image: redis:7
volumes:
pgdata:Сервисы находят друг друга по имени, а не по IP
Когда ты запускаешь compose up, Compose создаёт для проекта сеть по умолчанию (с именем <project>_default) и подключает к ней каждый сервис. В этой сети Docker поднимает встроенный DNS-сервер, и каждый сервис разрешается по имени сервиса — api достаёт Postgres по хосту db, порт 5432, без единого IP в конфиге. Это главная причина, по которой вики из команд run исчезает: хрупкой частью ручной оркестрации всегда была сшивка контейнеров по адресам, а адреса меняются при каждом перезапуске. DNS по имени сервиса делает связи стабильными и читаемыми. Два момента кусаются на senior-уровне: DNS-имя — это ключ сервиса, и container_name его не меняет — переопредели имя контейнера, и DNS-запись всё равно db. И опубликованный маппинг ports: ("8080:8080") нужен, чтобы хост достучался до контейнера; трафик между сервисами идёт прямо на порт контейнера через сеть проекта и вовсе не требует записи ports:. Частая ошибка — публиковать порт БД на хост, «чтобы API мог достучаться» — API достучится по имени через сеть в любом случае; публикация лишь открывает БД твоему ноутбуку.
В файле выше сервис api подключается к Postgres по хосту db. Откуда берётся это имя и что его разрешает?
▸Почему это работает
Почему сходимость лучше стартового скрипта? Скрипт кодирует путь — сделай это, потом то, — а путь можно выполнить наполовину: он умирает на шаге 17 и оставляет окружение в состоянии, которое никто не проектировал. Сходимость кодирует цель: up всегда ведёт к объявленному набору, поэтому повтор прерванного up доводит дело до конца, а не множит хаос, а down с последующим up — чистая пересборка, ведь желаемое состояние — единственный источник истины. Файл говорит, что должно быть правдой, а задача инструмента — привести реальность в соответствие — та же ментальная модель, что у манифестов Kubernetes или Terraform, освоенная дёшево на ноутбуке.
Именованные тома — это долговечный слой
Контейнеры одноразовы; данные под ними — обычно нет. Именованный том (pgdata выше) — это управляемый Compose, привязанный к проекту объект хранения, переживающий пересоздание контейнера: compose up после смены образа пересоздаёт контейнер db, но заново монтирует тот же pgdata, так что локальная база сохраняет строки. Острые края — это разрушающие команды: обычный compose down удаляет контейнеры и сеть по умолчанию, но сохраняет именованные тома, а compose down -v их удаляет — этот флаг и есть разница между «перезапустить стек» и «стереть локальную базу». Анонимные тома (голый путь без имени) — ловушка: они создаются заново при каждом пересоздании и осиротевают, поэтому сервис, который должен хранить данные, но использует анонимный том, тихо их теряет на следующем up. Дай тому имя, смонтируй его в путь данных — и down/up можно гонять весь день безопасно.
Разработчик делает compose down, затем compose up после смены тега образа Postgres и удивлён, что его засеянные локальные данные пропали. Какой один факт объясняет наиболее вероятную причину?
- 01Объясни, как один сервис Compose достигает другого без единого IP в конфигурации, и одну тонкость, которая удивляет.
- 02Сопоставь стартовый скрипт с моделью сходимости Compose и объясни, как именованные тома взаимодействуют с down и down -v.
Compose схлопывает локальное окружение из нескольких контейнеров с вики из команд run в один декларативный файл, описывающий один проект. Имя проекта даёт namespace всему, что Compose создаёт, и делает up и down идемпотентными: up сравнивает объявленное желаемое состояние с тем, что есть, и делает минимум изменений, поэтому правка одного сервиса пересоздаёт один контейнер, а не стек. Сервисы находят друг друга через сеть проекта по умолчанию, где встроенный DNS Docker разрешает каждый сервис по его ключу — api достаёт Postgres по хосту db, без единого IP, и container_name это DNS-имя не меняет. Опубликованные порты — только для хоста; трафик между сервисами идёт на порт контейнера через сеть проекта, так что публикация порта БД — это открытие, а не требование. Именованные тома — долговечный слой, переживающий пересоздание контейнера; обычный compose down их сохраняет, а compose down -v удаляет, тогда как анонимные тома пересоздаются пустыми на каждом up и тихо теряют данные. Выигрыш концептуальный, а не косметический: окружение становится доступным для ревью, версионируемым описанием желаемого состояния — той же ментальной моделью, что у манифестов Kubernetes, освоенной на ноутбуке. Теперь, когда встретишь у коллеги документ «запусти эти двенадцать команд», ты знаешь, чем его заменить — и что именно перестанет ломаться после следующего переименования контейнера.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.