1. Один образ — много запусков
Образ у нас уже есть. Дальше пазл складывается так: режим не должен жить в Dockerfile, итоговое значение свойства определяется по приоритету источников, значения прилетают через env vars, -D и --..., а имена env vars нужно переводить без угадываний. Теперь всё это надо собрать в один runnable workflow: взять один image и запустить его в двух runtime-сценариях, не меняя сам артефакт.
В наших командах переключателем режима будет spring.profiles.active. Здесь profiles используются как уже существующий project-specific switch standalone/postgres. Нам не нужно сейчас раскручивать всю профильную механику; достаточно увидеть более важную вещь: даже такой Spring-механизм остаётся частью runtime-конфигурации, а не поводом собирать новый image.
Представим, что у нас есть образ учебного сервиса:
docker-java-catalog-service:local (или любой ваш тег — суть не в названии).
И у сервиса есть два режима:
- standalone — сервис работает «сам по себе», без PostgreSQL, используя in-memory хранилище.
- postgres — сервис работает с PostgreSQL (через JPA/Flyway), то есть ждёт доступный datasource.
Держите мысленную картинку:
flowchart TD
Image["Docker image: docker-java-catalog-service:local<br/>(immutable)"]
Image -->|run #1<br/>SPRING_PROFILES_ACTIVE=standalone| C1["Container: catalog-standalone"]
Image -->|run #2<br/>SPRING_PROFILES_ACTIVE=postgres<br/>+ datasource vars| C2["Container: catalog-postgres"]
C1 --> A1["API отвечает в standalone режиме"]
C2 --> A2["API отвечает в postgres режиме (если БД доступна)"]
2. Мини-окошко правды: runtime endpoint
Если в предыдущих экспериментах вы уже временно выводили отдельные свойства, теперь пора собрать это в одну устойчивую /api/runtime ручку. Она и станет главной проверкой effective config для команд ниже.
Когда запускаешь контейнер, очень легко поверить самой команде docker run: «я же написал SPRING_PROFILES_ACTIVE=postgres, значит так и есть». На практике всё честнее и жёстче: значение могло быть перекрыто, профиль мог не активироваться, а порт мог прийти из другого источника. Поэтому вместо россыпи одноразовых endpoint’ов соберём один короткий runtime-probe, который показывает effective config целиком.
package com.example.catalog.ops.web;
import java.util.Arrays;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class RuntimeController {
private final Environment env;
RuntimeController(Environment env) {
this.env = env;
}
@GetMapping("/api/runtime")
String runtime() {
String[] activeProfiles = env.getActiveProfiles();
String profiles = activeProfiles.length == 0
? "<none>"
: String.join(",", activeProfiles);
String effectiveMode = Arrays.asList(activeProfiles).contains("postgres")
? "postgres"
: "standalone";
String port = env.getProperty("server.port", "8080");
return "mode=" + effectiveMode + ", profiles=" + profiles + ", port=" + port;
}
}
Здесь есть две разные вещи, и их важно не смешивать. profiles показывает, что Spring реально активировал. mode — это уже project-level интерпретация запуска: если включён postgres, считаем режим postgres, иначе остаёмся в standalone. Поэтому mode=standalone в ответе не означает «Spring сам магически сделал активным профиль standalone по умолчанию»; это просто короткий способ сказать «мы стартовали без postgres-режима».
Этой одной ручки достаточно для всех команд ниже. Ответы будем читать примерно так: mode=postgres, profiles=postgres, port=8081.
3. Профиль и порт: две оси
Очень человеческая ошибка новичка — думать так: «standalone значит порт 8080, postgres значит 8081». Пока вы один и пока у вас один ноутбук — это даже может “работать”. Но как только вы запускаете два контейнера рядом, добавляете тесты, или просто хотите быстро поменять порт, эта связь начинает мешать. Порт и профиль — разные свойства, и их выгодно держать независимыми.
В наших run-командах профиль — это project switch, а порт — отдельная ось запуска.
Давайте зафиксируем это в виде маленькой таблички. Она простая, но дисциплинирует мозг лучше, чем тысяча умных слов.
| Что меняем | Пример свойства | Зачем вообще это менять | Можно ли менять без пересборки image |
|---|---|---|---|
| Режим проекта (standalone/postgres) | spring.profiles.active | Включить/выключить инфраструктурный режим проекта | Да |
| Порт сервиса | server.port | Запустить рядом несколько экземпляров, убрать конфликт портов | Да |
| Адрес БД | spring.datasource.url | Подключиться к другой БД/хосту | Да |
| Логин/пароль к БД | spring.datasource .username/password | Подключиться к конкретной БД | Да |
И вот здесь — ключевая мысль лекции. Если меняется только одна ячейка в этой таблице, пересборка образа бессмысленна. Образ — это код. Конфигурация — это условия жизни кода.
Ещё одна полезная «проверка здравого смысла» звучит так: если вы можете описать изменение словами «мне нужно запустить тот же сервис, но…» (на другом порту, с другим профилем, с другим адресом БД), то это почти всегда runtime-конфигурация. Если вы говорите «мне нужно, чтобы сервис делал по-другому, потому что я переписал код/зависимости» — это уже про сборку.
4. Запуск image в standalone режиме
Сейчас будет момент, где Docker наконец перестаёт быть философией и становится чем-то, что можно проверить curl’ом. standalone режим ценен тем, что он позволяет запускать сервис без внешних зависимостей. Это “режим без оправданий”: если он не стартует — проблема точно не в PostgreSQL, не в сети, не в сервиснеймах. Скорее всего, проблема в конфигурации запуска или в том, что вы запускаете не то.
Здесь SPRING_PROFILES_ACTIVE — просто ещё один runtime-ключ. Он приходит теми же env vars, что и SERVER_PORT, и не требует отдельного image.
Вариант через env vars
Допустим, мы хотим:
- контейнер слушает внутри 8080;
- мы публикуем этот порт наружу;
- включаем профиль standalone.
Команда может выглядеть так:
# --rm: удалить контейнер после остановки
# --name: чтобы не путаться, что именно сейчас запущено
# -p: проброс порта (host:container)
# -e: задаём runtime-конфигурацию (профиль/порт) без пересборки image
docker run --rm \
--name catalog-standalone \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=standalone \
-e SERVER_PORT=8080 \
docker-java-catalog-service:local
Теперь проверяем наш “окошко правды”:
# Проверяем эффективные значения профиля и порта, которые реально поднялись после старта
curl http://localhost:8080/api/runtime
# mode=standalone, profiles=standalone, port=8080
Если вы увидели mode=standalone, profiles=standalone, port=8080, значит с профилем и портом всё честно. Мы не пересобирали image. Мы не трогали Dockerfile. Мы просто дали другие значения при запуске контейнера.
Вариант через application arguments
Поскольку Dockerfile у нас (по канону курса) запускает приложение через ENTRYPOINT в exec-form, вы можете дописать аргументы после имени image — и они попадут как application args (--key=value).
Например, так:
# Аргументы после имени image попадут в Spring Boot как application args (--key=value)
docker run --rm \
--name catalog-standalone \
-p 8080:8080 \
docker-java-catalog-service:local \
--spring.profiles.active=standalone \
--server.port=8080
И снова проверяем:
curl http://localhost:8080/api/runtime
# mode=standalone, profiles=standalone, port=8080
Выбор между env vars и application args — это не религия. Это вопрос удобства и договорённости в команде. В Docker/Compose мире env vars часто удобнее как baseline (особенно когда параметров много), но для разового запуска аргументы тоже отлично работают.
5. Запуск image в postgres режиме
postgres режим по смыслу должен подключиться к PostgreSQL. Где эта PostgreSQL живёт — сейчас неважно. Она может быть на вашей машине, на другом сервере или в соседнем контейнере. Наша задача скромнее: показать, что для переключения режима не нужен новый image.
Чтобы postgres режим был возможен, нам нужно передать как минимум:
— активный профиль postgres,
— порт (чтобы не конфликтовать со standalone‑контейнером, если мы поднимаем оба),
— настройки datasource: URL, username, password.
Покажем команду запуска через env vars (она более типична для контейнеров):
# Здесь задаём профиль и datasource через env vars — это именно runtime-конфигурация.
# Важно: <DB_HOST> должен быть доступен ИЗ контейнера, а не «как вам привычно с хоста».
docker run --rm \
--name catalog-postgres \
-p 8081:8081 \
-e SPRING_PROFILES_ACTIVE=postgres \
-e SERVER_PORT=8081 \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://<DB_HOST>:5432/catalog \
-e SPRING_DATASOURCE_USERNAME=catalog \
-e SPRING_DATASOURCE_PASSWORD=catalog \
docker-java-catalog-service:local
Обратите внимание: <DB_HOST> — это не “localhost на автомате”, а конкретный хост, который доступен из контейнера. Сегодня мы сознательно не уходим в сетевую модель multi-container мира, поэтому пишем это как параметр, который приходит извне.
Если база доступна и параметры корректны, то проверка будет такой же простой:
curl http://localhost:8081/api/runtime
# mode=postgres, profiles=postgres, port=8081
Если база не доступна, приложение, скорее всего, упадёт на старте (и это нормально для postgres режима). Важно, что вы всё равно увидите принцип: вы не делали отдельный образ «под Postgres». Вы запускали тот же image с другим набором входных значений.
Здесь есть хороший “ментальный тест”: представьте, что вы опубликовали image в registry, и его использует коллега. У него может быть другая БД, другой пароль, другой порт. Если ему придётся пересобирать ваш образ ради пароля — это не “он плохой”, это “образ спроектирован неправильно”. Пароль и URL должны приходить при запуске, а не жить в Dockerfile.
6. Два контейнера из одного image
Самая наглядная демонстрация “same image, different runtime config” — это запуск двух контейнеров одновременно из одного и того же образа. Это момент, когда мозг перестаёт «сомневаться» и начинает верить: один артефакт действительно может жить в разных режимах. В обычной разработке это часто используется для тестов, миграций, экспериментов, и просто для аккуратного разделения “как сервис живёт здесь и сейчас”.
Представим две команды рядом:
# Первый контейнер: standalone (свой профиль и свой порт)
docker run --rm --name catalog-standalone -p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=standalone -e SERVER_PORT=8080 \
docker-java-catalog-service:local
# Второй контейнер: postgres (тот же image, но другой профиль/порт и datasource-настройки)
docker run --rm --name catalog-postgres -p 8081:8081 \
-e SPRING_PROFILES_ACTIVE=postgres -e SERVER_PORT=8081 \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://<DB_HOST>:5432/catalog \
-e SPRING_DATASOURCE_USERNAME=catalog \
-e SPRING_DATASOURCE_PASSWORD=catalog \
docker-java-catalog-service:local
Обратите внимание: имя image одинаковое (docker-java-catalog-service:local). Отличаются только параметры запуска контейнера. Разные имена контейнеров (catalog-standalone, catalog-postgres) помогают не запутаться, а разные host‑порты (8080 и 8081) убирают конфликт.
Проверка тоже симметрична:
curl http://localhost:8080/api/runtime
# mode=standalone, profiles=standalone, port=8080
curl http://localhost:8081/api/runtime
# mode=postgres, profiles=postgres, port=8081
Если вы хоть раз ловили себя на мысли «мне надо пересобрать образ, потому что я меняю порт/профиль/хост БД», то вот это упражнение выбивает привычку довольно быстро. Пересборка нужна, когда изменился код. Всё остальное — входные значения запуска.
7. Типичные ошибки при запуске одного image в разных режимах
Ошибка №1: делать отдельные images под режимы или кодировать режим в имени image.
catalog-service-postgres, catalog-service-standalone, catalog-postgres:1.0 — всё это сначала выглядит “удобно”, а потом быстро превращается в зоопарк тегов и Dockerfile. Вы уже не уверены, что разные режимы запускают один и тот же код, а любая мелкая правка начинает требовать две сборки вместо одной. Режим должен жить в параметрах запуска контейнера, а имя image должно говорить только о том, что это за сервис.
Ошибка №2: запекать профиль в Dockerfile через ENV и потом лечить это пересборкой.
ENV SPRING_PROFILES_ACTIVE=postgres внутри Dockerfile выглядит невинно, пока вам не понадобится standalone. После этого вы либо пересобираете образ, либо плодите ещё один Dockerfile, либо начинаете переопределять всё сразу и теряете прозрачность. Если меняется только режим, значит проблема не в сборке. Значит, нужно поменять runtime-конфигурацию.
Ошибка №3: смешивать каналы для одного и того же ключа или путать -D... и --...
В контейнерном запуске это происходит особенно легко: вокруг уже есть Docker CLI, ENTRYPOINT, JVM и Spring Boot. -Dserver.port=9090 — это system property JVM, а --server.port=9090 — application argument Spring Boot. Если к этому ещё добавить SERVER_PORT=8080, получить “а почему победило не то?” очень просто. На один ключ лучше выбирать один главный канал, пока у вас нет осознанной причины делать сложнее.
Ошибка №4: думать, что localhost в SPRING_DATASOURCE_URL означает вашу хост-машину.
Внутри контейнера localhost указывает на сам контейнер. Поэтому jdbc:postgresql://localhost:5432/... означает “Postgres должен быть внутри этого же контейнера”, а не “где-то рядом на вашем ноутбуке”. Иногда это случайно работает, но как baseline это почти всегда ошибка мышления.
Ошибка №5: связывать профиль и порт как единый “режим”.
Как только вы решаете “postgres всегда на 8081”, вы сами себе закладываете мину. Порт — это согласование на границе сети, профиль — это включение/выключение инфраструктурной конфигурации. Они не обязаны совпадать ни сегодня, ни завтра. Жизнь становится заметно проще, когда вы держите их независимыми и передаёте отдельно.
Ошибка №6: терять границу image vs container и дебажить “не тот слой”.
Образ — это рецепт и артефакт. Контейнер — конкретный запуск с конкретными параметрами. Если два контейнера из одного image ведут себя по-разному, первым делом нужно смотреть не в Dockerfile, а в effective config текущего запуска. Для этого и нужна короткая ручка вроде /api/runtime: она быстро отвечает, какой container сейчас живёт и с какими параметрами.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ