1. Канонический workflow и docker compose up
Если честно, Docker Compose отлично умеет поднимать контейнеры и без нашего «ритуала каноничности». Проблема не в том, что Compose плохой. Проблема в том, что человек — существо творческое: сегодня он запускал стек одной командой, завтра другой, послезавтра добавил третий файл, а через неделю уже никто не понимает, почему «у меня работает», а «у тебя нет».
Канонический developer workflow — это попытка сделать вашу разработку скучной. В хорошем смысле. Скучной, предсказуемой, воспроизводимой и объяснимой другому человеку. В идеале — так, чтобы вы могли открыть README через полгода и всё равно запустить сервис без археологических раскопок в bash-history.
Давайте зафиксируем, какую боль мы лечим. Обычно она выглядит так: вы уверены, что подняли «тот же самый стек», но на самом деле подняли чуть другой набор сервисов, с другими портами, другими переменными окружения, или вообще с другим stage Dockerfile. Потом вы смотрите на логи, они “странные”, и начинается диагностика уровня «попробую ещё раз, но с другой командой». Это как чинить телевизор методом «постучать сильнее» — иногда помогает, но как бы сказать… не инженерно.
2. Три вопроса перед запуском: режим, файлы, сервисы
Перед любым запуском Compose-стека полезно на секунду остановиться и задать себе три очень практичных вопроса. Это звучит почти как медитация, но на самом деле это страховка от половины типичных «почему не так» в локальной среде. И да, эти вопросы гораздо дешевле, чем потом 40 минут изучать логи и подозревать Spring Boot во всех грехах.
Первый вопрос — в каком режиме вы хотите работать: normal или debug. Второй вопрос — какие Compose-файлы участвуют (только compose.yaml или compose.yaml + compose.dev.yaml). Третий вопрос — какие именно сервисы вы сейчас поднимаете: весь стек или его часть (partial startup). Эти три решения определяют “мир”, в котором вы дальше живёте, и важно, чтобы они были явными прямо в команде.
Ниже — компактная таблица, которая связывает эти вопросы с командами. Её можно воспринимать как «ментальный шаблон», а не как догму.
| Что вы хотите | Какие файлы | Какие сервисы | Пример команды |
|---|---|---|---|
| Обычная разработка (normal) | compose.yaml | весь стек | docker compose up --build |
| Обычная разработка (normal) | compose.yaml | только app + postgres | SPRING_PROFILES_ACTIVE=postgres docker compose up --build app postgres |
| Отладка (debug) | compose.yaml + compose.dev.yaml | app + postgres | SPRING_PROFILES_ACTIVE=postgres docker compose -f compose.yaml -f compose.dev.yaml up --build app postgres |
| Посмотреть итоговый конфиг перед debug/partial запуском | compose.yaml + compose.dev.yaml | ничего не запускаем | SPRING_PROFILES_ACTIVE=postgres docker compose -f compose.yaml -f compose.dev.yaml config |
Обратите внимание: мы специально избегаем ситуации «команда короткая, но непонятно что она делает». Если debug включается — это видно по -f compose.dev.yaml. Если запускается только часть сервисов — они явно перечислены в конце команды. Когда это так, вы почти физически ощущаете, что управляете системой, а не «надеетесь, что снова повезёт».
Здесь важно не смешивать два режима. docker compose up app следует тому graph зависимостей, который уже объявлен в Compose. А когда вы явно пишете app postgres и задаёте SPRING_PROFILES_ACTIVE=postgres, вы уже выбираете конкретный сценарий запуска руками.
В примерах с SPRING_PROFILES_ACTIVE=... показан POSIX-вариант inline assignment. В PowerShell переменную обычно задают отдельной командой перед docker compose.
3. Pre-flight check: docker compose config
Есть команды, которые кажутся скучными, пока вы не попадёте на проблему. docker compose config — ровно такая. В нормальный день она занимает 5–10 секунд и выглядит как лишний шаг. В плохой день она экономит час времени и пару нервных клеток, которые, как известно, не пересобираются даже через multi-stage build.
Смысл docker compose config очень простой: он показывает итоговую конфигурацию после того, как Compose применил merge нескольких файлов и сделал подстановку переменных. То есть вы видите не «что написано в YAML», а «что реально будет применено». Поэтому для scenario-driven запуска config должен видеть ровно те же inputs, что и будущий up: те же -f и те же env overrides.
Для частого сценария debug + app + postgres команда выглядит так:
# Проверяем итоговый config для того же сценария, который потом будем запускать
SPRING_PROFILES_ACTIVE=postgres docker compose -f compose.yaml -f compose.dev.yaml config
Если вы просто запускаете full-stack normal без дополнительных overrides, команда будет короче: docker compose config. Принцип не меняется — сначала проверили тот мир, который потом поднимете.
Что полезного вы увидите в выводе? Например, объединение environment для сервиса app (кусочек, упрощённо):
services:
app:
environment:
SPRING_PROFILES_ACTIVE: postgres
JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
Даже если вы писали environment: в разных файлах, в итоговой картине вы видите “как оно будет” для контейнера. Это особенно ценно, когда вы не уверены, кто кого перекрыл.
Если вы используете debug-режим или сузили сценарий до app + postgres, полезно искать глазами три вещи: что в services.app.build появился target: development, что в services.app.ports действительно есть 5005:5005, и что в services.app.environment профили и JVM-параметры совпадают с вашим сценарием. Не нужно заучивать огромный YAML-вывод; достаточно научиться подтверждать ключевые отличия.
Иногда полезно делать ещё одну простую проверку: убедиться, что вы не «случайно запустили другой мир» из-за переменных окружения. Например, вы ожидаете, что SPRING_PROFILES_ACTIVE включает postgres, а по факту где-то осталось postgres,cache,messaging. Если вы увидите это в config до запуска, вы сэкономите себе серию загадочных ошибок вида «почему приложение пытается ходить в Redis, которого я не запускал» или наоборот.
4. Запуск стека: up и up --build
Compose-команда up — это как кнопка «пуск» у вашего локального стенда. Но важно помнить одну тонкость: Compose умеет и запускать, и собирать, и пересобирать, и иногда даже “незаметно” не делать того, что вы ожидали. Поэтому полезно договориться с собой о паре канонических форм, и не устраивать каждый день «фестиваль флагов».
Для учебного проекта и для обычной разработки чаще всего разумный baseline — запускать так, чтобы сборка образа не была “по памяти Compose”, а была явной. Поэтому часто используют --build: он заставляет Compose пересобрать те сервисы, у которых есть build: (в нашем случае это app). Это не значит, что он всегда пересоберёт всё с нуля — Docker cache всё равно работает — но вы явно просите: «собери свежую версию».
Пример обычного запуска полного стека:
# Запуск всего стека с явной пересборкой сервисов с build:
docker compose up --build
Если вам нужно поднять только часть стека (например, вы сейчас работаете без Redis и RabbitMQ), команда остаётся читаемой:
# Partial startup: поднимаем только выбранные сервисы и сразу согласуем профили
SPRING_PROFILES_ACTIVE=postgres docker compose up --build app postgres
Заметьте, что partial startup здесь задаётся сразу двумя вещами: списком сервисов и matching SPRING_PROFILES_ACTIVE. Без этой пары команда легко превращается в запуск “не того мира”.
Для debug-режима важно, чтобы команда не была «похожа на normal, но с секретным отличием где-то внутри файла». Пусть отличия читаются прямо из CLI:
# Debug-сессия: явно подключаем dev-файл и поднимаем нужные сервисы
SPRING_PROFILES_ACTIVE=postgres docker compose -f compose.yaml -f compose.dev.yaml up --build app postgres
Ещё один момент, который важно держать в голове: up по умолчанию живёт в “foreground”-режиме и пишет логи в ваш терминал. Это удобно, когда вы именно сейчас хотите наблюдать старт. Иногда вам нужен “detached”-режим (-d), чтобы терминал не был занят. Но если вы выбираете -d, не забывайте, что вы добровольно отрезаете себе самые первые секунды наблюдения, и вам придётся сразу перейти к logs. В рамках канонического workflow это нормально, но только если вы делаете это осознанно.
5. Первые 2 минуты после запуска: ps и logs
После docker compose up очень хочется сделать вид, что “если команда не упала, значит всё поднялось”. Это естественное человеческое желание. Оно же — причина половины «стек вроде поднят, но API не отвечает». Поэтому мы вводим маленькую дисциплину: после запуска задаём два быстрых операционных вопроса и отвечаем на них стандартными командами.
Первый вопрос: какие сервисы реально поднялись и в каком они состоянии. На него отвечает docker compose ps. Второй вопрос: что происходит внутри контейнера app прямо сейчас — запускается ли Spring Boot, подключается ли к PostgreSQL по service name, не упал ли на миграциях, не ругается ли на Redis/RabbitMQ из-за профилей. На него отвечает docker compose logs app (иногда с -f, если хотим “подписаться” на поток).
Минимальный сценарий выглядит так:
# Быстро проверяем, какие сервисы поднялись и какой у них статус/health
docker compose ps
Вы увидите таблицу сервисов. Для postgres и других зависимостей часто важен не только статус “Up”, но и “healthy”, если вы используете healthchecks. Это как раз то место, где readiness-модель перестаёт быть теорией: если БД ещё не healthy, а приложение уже стартует — вы почти наверняка увидите проблемы соединения.
Дальше смотрим логи приложения:
# Смотрим логи конкретного сервиса (важно: именно app, чтобы не утонуть в шуме)
docker compose logs app
Если хотите наблюдать старт в динамике (это полезно, когда вы только что что-то поменяли), можно так:
# Подписываемся на поток логов (удобно смотреть старт в реальном времени)
docker compose logs -f app
В контейнерном мире логи — это не “потом посмотрим, если что-то сломалось”. Логи — это ваш основной сигнал о том, какой профиль активировался, на каком порту поднялся сервис, куда он пытается подключиться и почему он принимает решения именно так. В идеале вы в стартовых логах должны видеть что-то вроде: активные профили, datasource URL (без паролей), информацию о том, что миграции применились, и что сервер слушает ожидаемый порт.
Если вы в debug-режиме, логи дополнительно помогают убедиться, что JVM действительно стартовала с нужными параметрами (обычно JDWP пишет характерные сообщения). Но тут важно не превращать лекцию в курс “как читать все варианты JDWP-логов”; нам достаточно уметь отличить «debug включился» от «я думал, что включился».
6. Одна «сессия inputs»: -f ... и те же env overrides
Это один из самых коварных моментов, потому что выглядит как мелочь. Например, вы сначала проверили debug/partial-сценарий так:
SPRING_PROFILES_ACTIVE=postgres docker compose -f compose.yaml -f compose.dev.yaml config
А потом запускаете его уже другой командой:
docker compose -f compose.yaml -f compose.dev.yaml up --build app postgres
Файлы вроде те же, но мир уже другой: config вы смотрели для профиля postgres, а up запустили с дефолтным набором профилей. Ровно так и рождается ощущение, что Compose “сам что-то поменял”.
Поэтому правило простое: в рамках одного сценария не меняем набор inputs между config и up. Если режим задаётся файлами и env overrides, они должны совпадать.
# Debug-сессия app + postgres: один и тот же набор inputs на pre-flight и запуске
SPRING_PROFILES_ACTIVE=postgres docker compose -f compose.yaml -f compose.dev.yaml config
SPRING_PROFILES_ACTIVE=postgres docker compose -f compose.yaml -f compose.dev.yaml up --build app postgres
После старта дисциплина продолжается чуть проще: для ps, logs, down держим тот же набор -f, чтобы не перескакивать между base- и dev-конфигурацией.
# После старта сохраняем тот же набор файлов для команд наблюдения
docker compose -f compose.yaml -f compose.dev.yaml ps
docker compose -f compose.yaml -f compose.dev.yaml logs -f app
Если сценарий normal и без дополнительных overrides, inputs короче, но принцип тот же:
# Normal full-stack сессия
docker compose config
docker compose up --build
docker compose ps
docker compose logs -f app
Даже если отдельные команды иногда “как будто всё равно срабатывают” и без этой дисциплины, привычка не менять набор inputs делает картину предсказуемой и сильно упрощает диагностику.
7. Мини-шаблон команд для README
README в репозитории — это не художественная литература и не инструкция на 40 страниц. Но в контейнеризированном проекте он должен закрывать главный вопрос новичка: «какие команды считаются нормальными, а какие — экспериментами». Если этого нет, у каждого появляется свой собственный “правильный” запуск, и стенд расползается.
Хорошая стратегия — зафиксировать очень короткий набор команд для трёх сценариев: normal full stack, debug (обычно для app + postgres), и partial startup в normal. Вы не обязаны документировать все возможные комбинации. Документируйте то, что реально используется каждый день.
Небольшой пример того, как это может выглядеть в README (это именно пример формата, а не «обязательная структура вселенной»):
# Normal: full stack
docker compose up --build
# Normal: app + postgres (partial startup — профили согласуем с набором сервисов)
SPRING_PROFILES_ACTIVE=postgres docker compose up --build app postgres
# Debug: app + postgres (сначала проверяем тот же сценарий через config)
SPRING_PROFILES_ACTIVE=postgres docker compose -f compose.yaml -f compose.dev.yaml config
SPRING_PROFILES_ACTIVE=postgres docker compose -f compose.yaml -f compose.dev.yaml up --build app postgres
# Debug-session observation: сохраняем тот же набор файлов
docker compose -f compose.yaml -f compose.dev.yaml ps
docker compose -f compose.yaml -f compose.dev.yaml logs -f app
В строках с SPRING_PROFILES_ACTIVE=... показан POSIX-вариант. В PowerShell переменную обычно задают отдельной командой перед docker compose.
Для normal full-stack наблюдение остаётся обычным docker compose ps и docker compose logs -f app.
Обратите внимание: debug-сценарий начинается с config. Это не потому, что мы любим страдать. Это потому, что debug-сценарий почти всегда включает merge, порты и JVM-параметры, а значит “проверить итоговый конфиг” — разумный pre-flight шаг.
И ещё один маленький “человеческий” штрих: обычно полезно рядом дать пару команд наблюдения. Не как отдельный курс по диагностике, а как быстрый ответ «что делать сразу после запуска»:
# Проверяем, что сервисы действительно в ожидаемом состоянии
docker compose ps
# Если API не отвечает — первыми делом смотрим логи приложения
docker compose logs -f app
Этого уже достаточно, чтобы стек был не просто “поднят”, а “наблюдаем”.
8. Типичные ошибки в workflow Compose
Ошибка №1: жизнь “историей терминала”, а не каноническими командами.
Это выглядит невинно: вы один раз подняли стек удачной командой, потом ещё раз, потом чуть изменили её “по ситуации”, и через неделю уже нет “одной правильной”. В результате любой новый участник команды получает не инструкцию, а легенду. Хорошее лекарство — зафиксировать 2–3 сценария в README и сознательно придерживаться их, не превращая каждый запуск в творчество.
Ошибка №2: debug-настройки включены, но не читаются как debug.
Иногда debug-порт и JDWP-флаги тихо оказываются в базовом compose.yaml, и normal mode превращается в “почти debug”. Это плохо хотя бы потому, что вы перестаёте понимать, почему JVM ведёт себя иначе. Debug должен быть явным: отдельный файл compose.dev.yaml, явные -f в команде, и вы всегда можете доказать, что сейчас включён именно debug-режим.
Ошибка №3: docker compose config запускают только после ошибки, а не до запуска.
Парадокс в том, что config чаще всего нужен не тогда, когда уже всё горит, а чуть раньше — когда вы меняете файл и хотите убедиться, что Compose понял вас правильно. Особенно это касается относительных путей и merge-конфликтов между compose.yaml и compose.dev.yaml. Если вы делаете config привычкой, часть ошибок исчезает вообще без стадии “оно не работает”.
Ошибка №4: на pre-flight и на запуске используют разные inputs.
Классика: config смотрят без SPRING_PROFILES_ACTIVE=postgres, а up запускают уже с ним; или поднимают стек с -f compose.yaml -f compose.dev.yaml, а потом читают логи обычной командой без тех же -f. Иногда это ещё и “похоже работает”, что делает ошибку особенно коварной. Дисциплина “одна сессия — один набор inputs” решает это почти полностью.
Ошибка №5: считают, что up = “всё готово”, и сразу идут тестировать API.
Контейнер может быть запущен, но зависимость может быть ещё не healthy, миграции ещё могут идти, а приложение может стартовать дольше, чем вам кажется (особенно на слабом ноутбуке). Если вы сначала делаете docker compose ps, а потом смотрите docker compose logs app, вы видите реальность, а не надежду. И это, внезапно, сильно ускоряет разработку.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ