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

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

Docker for Spring
Рівень 11 , Лекція 4
Відкрита

1. Один образ — багато запусків

Образ у нас уже є. Далі пазл складається так: режим не повинен жити в Dockerfile, а підсумкове значення властивості визначається за пріоритетом джерел. Значення приходять через env vars, -D і --..., а імена змінних середовища треба перекладати без вгадувань. Тепер усе це треба зібрати в один робочий сценарій: взяти один image і запустити його в двох runtime-сценаріях, не змінюючи сам артефакт.

У наших командах перемикачем режиму буде spring.profiles.active. Тут profiles слугують уже наявним проєктним перемикачем standalone/postgres. Нам не потрібно зараз розкручувати всю профільну механіку; достатньо побачити важливішу річ: навіть такий Spring-механізм залишається частиною runtime-конфігурації, а не приводом збирати новий image.

Уявімо, що в нас є образ навчального сервісу:

docker-java-catalog-service:local (або будь-який ваш тег — суть не в назві).

І в сервісу є два режими:

  • standalone — сервіс працює «сам по собі», без PostgreSQL, використовуючи in-memory сховище.
  • postgres — сервіс працює з PostgreSQL (через JPA/Flyway), тобто очікує доступний datasource.

Тримайте в голові таку схему:

flowchart TD
    Image["Docker-образ: docker-java-catalog-service:local<br/>(незмінний)"]

    Image -->|запуск №1<br/>SPRING_PROFILES_ACTIVE=standalone| C1["Контейнер: catalog-standalone"]
    Image -->|запуск №2<br/>SPRING_PROFILES_ACTIVE=postgres<br/>+ параметри datasource| C2["Контейнер: catalog-postgres"]

    C1 --> A1["API відповідає в standalone-режимі"]
    C2 --> A2["API відповідає в postgres-режимі (якщо БД доступна)"]

2. Міні-віконце правди: runtime endpoint

Якщо в попередніх експериментах ви вже тимчасово виводили окремі властивості, тепер час зібрати все в один стабільний ендпоїнт /api/runtime. Він і стане головною перевіркою ефективної конфігурації для команд нижче.

Коли запускаєте контейнер, дуже легко повірити самій команді docker run: «я ж написав SPRING_PROFILES_ACTIVE=postgres, отже так і є». На практиці все чесніше й жорсткіше: значення могли бути перекриті, профіль міг не активуватися, а порт міг прийти з іншого джерела. Тому замість розсипу одноразових ендпоїнтів зберемо один короткий 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 — це вже інтерпретація на рівні проєкту: якщо увімкнено postgres, вважаємо режим postgres, інакше залишаємося в standalone. Тому mode=standalone у відповіді не означає, що Spring сам магічно активував профіль standalone за замовчуванням. Це лише короткий спосіб сказати: ми стартували без postgres-режиму.

Цього одного ендпоїнта достатньо для всіх команд нижче. Відповіді читатимемо приблизно так: mode=postgres, profiles=postgres, port=8081.

3. Профіль і порт: дві осі

Дуже людська помилка новачка — думати так: «standalone означає порт 8080, postgres означає 8081». Поки ви один і поки у вас один ноутбук, це навіть може «працювати». Але щойно ви запускаєте два контейнери поруч, додаєте тести або просто хочете швидко змінити порт, цей зв’язок починає заважати. Порт і профіль — різні властивості, і їх вигідно тримати незалежними.

У наших командах запуску профіль — це перемикач проєкту, а порт — окрема вісь запуску.

Давайте зафіксуємо це у вигляді маленької таблиці. Вона проста, але дисциплінує мозок краще, ніж тисяча розумних слів.

Що змінюємо Приклад властивості Навіщо взагалі це змінювати Чи можна змінювати без повторного збирання image
Режим проєкту (standalone/postgres) spring.profiles.active Увімкнути або вимкнути інфраструктурний режим проєкту Так
Порт сервісу server.port Запустити поруч кілька екземплярів і прибрати конфлікт портів Так
Адреса БД spring.datasource.url Підключитися до іншої БД або хоста Так
Логін/пароль до БД spring.datasource .username/password Підключитися до конкретної БД Так

І ось тут — ключова думка лекції. Якщо змінюється лише одна клітинка в цій таблиці, повторне збирання образу марне. Образ — це код. Конфігурація — це умови життя коду.

Ще одна корисна перевірка здорового глузду звучить так: якщо ви можете описати зміну словами «мені потрібно запустити той самий сервіс, але…» — на іншому порті, з іншим профілем, з іншою адресою БД, — це майже завжди runtime-конфігурація. Якщо ж ви кажете: «мені потрібно, щоб сервіс працював інакше, бо я переписав код або залежності», — це вже про збирання.

4. Запуск image у standalone-режимі

Зараз буде момент, де Docker нарешті перестає бути філософією і стає чимось, що можна перевірити curl’ом. standalone-режим цінний тим, що дає змогу запускати сервіс без зовнішніх залежностей. Це «режим без виправдань»: якщо він не стартує, проблема точно не в PostgreSQL, не в мережі й не в service names. Скоріше за все, проблема в конфігурації запуску або в тому, що ви запускаєте не те.

Тут 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 часто зручніші як базовий варіант, особливо коли параметрів багато. Але для разового запуску аргументи теж чудово працюють.

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 має бути всередині цього ж контейнера», а не «десь поруч на вашому ноутбуці». Іноді це випадково працює, але як базовий підхід це майже завжди помилка мислення.

Помилка № 5: пов’язувати профіль і порт як єдиний «режим».
Щойно ви вирішуєте «postgres завжди на 8081», ви самі собі закладаєте міну. Порт — це узгодження на межі мережі, профіль — це увімкнення або вимкнення інфраструктурної конфігурації. Вони не зобов’язані збігатися ні сьогодні, ні завтра. Життя стає помітно простішим, коли ви тримаєте їх незалежними і передаєте окремо.

Помилка № 6: втрачати межу image vs container і дебажити «не той шар».
Образ — це рецепт і артефакт. Контейнер — конкретний запуск із конкретними параметрами. Якщо два контейнери з одного image поводяться по-різному, насамперед потрібно дивитися не в Dockerfile, а в effective config поточного запуску. Для цього і потрібна коротка ручка на кшталт /api/runtime: вона швидко відповідає, який container зараз живе і з якими параметрами.

1
Опитування
Docker Spring, рівень 11, лекція 4
Недоступний
Docker Spring
Конфігурація та пріоритети властивостей
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ