JavaRush /Курсы /Docker for Spring /Один image — два режима runtime

Один image — два режима runtime

Docker for Spring
11 уровень , 4 лекция
Открыта

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 сейчас живёт и с какими параметрами.

1
Задача
Docker for Spring, 11 уровень, 4 лекция
Недоступна
Один image в режимах `standalone` и `postgres` через env vars
Один image в режимах `standalone` и `postgres` через env vars
1
Задача
Docker for Spring, 11 уровень, 4 лекция
Недоступна
`storage-mode` через application arguments без реальной БД
`storage-mode` через application arguments без реальной БД
1
Опрос
Docker Spring, 11 уровень, 4 лекция
Недоступен
Docker Spring
Конфигурация и приоритеты свойств
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ