1. Чиста схема конфігурації: зміст
Якщо чесно, хаос у конфігурації зазвичай зʼявляється не тому, що люди «погані», а тому, що проєкт росте. Спочатку в нас один application.yml, потім зʼявляється PostgreSQL, потім Redis, потім RabbitMQ — і раптом кожен додає свої параметри, «аби швидше запрацювало». За кілька днів це перетворюється на музей: тут лежить application-final.yml, поруч application-docker.yml, а в README — чотири взаємовиключні команди запуску. У цей момент проєкт уже можна контейнеризувати, але підтримувати його стає незручно.
Чиста схема конфігурації — це коли ви можете відповісти на запитання «що буде, якщо я ввімкну профіль cache?» і «чому застосунок шукає базу за цією адресою?» без детективного розслідування. Ми хочемо досягти простого результату: кожен файл має одну відповідальність, а набір профілів складається в режим, як конструктор Lego, а не як «ящик із дротами, які краще не чіпати».
Важливо, що «чистота» тут не про красу заради краси. У світі Docker будь-який зайвий хаос у конфігурації автоматично перетворюється на дві неприємності. Перша — ви починаєте перезбирати image під різні випадки, порушуючи принцип same image, different runtime config. Друга — ви перестаєте розуміти, що саме було запущено в конкретному контейнері: чому він записує файли сюди, чому слухає порт там, чому підключається до Redis… і починається те саме: «а давайте ще одну змінну середовища додамо, раптом допоможе».
У цій лекції ми зафіксуємо канонічний набір файлів і профілів так, щоб він був лекційно зрозумілим, але при цьому дуже схожим на те, як це роблять у реальних командах: без сотні профілів, без копій одного й того самого YAML і без «профілю на кожен чих».
2. Матриця режимів: профілі та шари
Щоб конфігурація не розповзалася, спершу потрібно чесно домовитися: які осі відмінностей є в застосунку. У нашому навчальному сервісі це не «всі можливі опції Spring Boot», а лише кілька інфраструктурних контурів, бо проєкт навмисно простий. І це добра новина: що менше осей — то менше шансів отримати application-please-help.yml.
Уявіть таку картину: є базовий режим, у якому сервіс може жити сам (це standalone), і є режим, де зʼявляється зовнішня база (postgres). А вже поверх postgres ми можемо «докрутити» дві додаткові залежності — Redis (cache) і RabbitMQ (messaging). Тобто cache і messaging — це не самостійні «світи», а додаткові шари.
Для наочності зручно один раз побачити це у вигляді таблиці. Це не список команд і не «як запускати», а просто карта відмінностей:
| Профіль(і) | Сховище даних каталогу | PostgreSQL | Redis | RabbitMQ | Ідея режиму |
|---|---|---|---|---|---|
| standalone | у памʼяті | ні | ні | ні | швидкий старт без інфраструктури |
| postgres | JPA + БД | так | ні | ні | нормальна модель збереження |
| postgres,cache | JPA + БД | так | так | ні | додали кеш для читання |
| postgres,cache,messaging | JPA + БД | так | так | так | додали мінімальне налаштування messaging |
Ключовий методичний момент: ми не намагаємося підтримувати всі комбінації. Скажімо, standalone,cache ми не вважаємо нормальним режимом. Теоретично можна, але це створює запитання «а cache чого, якщо немає бази?», «а як seed-дані?», «а що кешуємо?». Для курсу з Docker така комбінація приносить більше плутанини, ніж користі.
І ще один важливий момент: режим — це композиція профілів, а не «імʼя одного профілю, який усе вирішує». Тому ми спеціально обираємо короткі назви профілів за змістом, а не docker, prod2, mode_test. Профіль має відповідати на запитання «що змінюється». Якщо профіль називається postgres, ви не будете гадати, що він робить.
3. Правило “один ключ — один дім”
Зараз буде правило, яке виглядає нудно, але рятує від конфігураційного апокаліпсиса: кожна властивість має мати одне основне місце. Її можна перевизначити ззовні — це нормально, — але не слід одночасно записувати її як істину в трьох profile-файлах. Інакше ви отримаєте ефект last-wins там, де вам узагалі не хотілося гри у вгадування.
Уявіть, що конфігурація — це шафа. application.yml — це одяг на кожен день: джинси, футболка, куртка. Профілі — це «спецодяг»: каска для будівництва (postgres), рукавички для роботи з металом (cache), рація для переговорів (messaging). Проблема починається, коли ви починаєте зберігати джинси одночасно в касці й у рукавичках. Це вже не шафа, а квест.
Що ми кладемо до базового application.yml? Усе, що однакове для всіх режимів і відображає «особистість» сервісу: порт або його значення за замовчуванням, базові налаштування Actuator, наш app.export-dir — тобто типовий шлях, — а також спільні логічні прапорці. А те, що залежить від конкретної інфраструктури, тобто URL бази, хост Redis і хост RabbitMQ, має жити в профільних файлах, бо за змістом це не «завжди», а «лише коли ввімкнено цей контур».
Окремо зафіксуємо важливий нюанс: профільні файли не мають бути копією application.yml. Їхнє завдання — бути короткими та точковими. Чим коротший application-postgres.yml, тим швидше ви розумієте, що саме вмикає режим postgres.
Щоб це не лишалося абстракцією, давайте один раз визначимо розподіл ключів по файлах. У вигляді таблиці це читається швидше, ніж довге моралізаторство:
| Група налаштувань | Де «дім» | Приклад ключів |
|---|---|---|
| Загальні налаштування сервісу | application.yml | server.port, management.*, app.export-dir |
| Standalone-сховище | application-standalone.yml | app.storage-mode: memory (або аналогічна ознака) |
| PostgreSQL datasource | application-postgres.yml | spring.datasource.*, spring.jpa.*, spring.flyway.* (за потреби) |
| Підключення Redis | application-cache.yml | spring.data.redis.*, плюс ваш вузький прапорець на кшталт app.cache-enabled |
| Підключення RabbitMQ | application-messaging.yml | spring.rabbitmq.*, плюс app.messaging-enabled |
Якщо ви помітили, що одну й ту саму властивість хочеться вписати в два профільні файли, це зазвичай сигнал: або властивість насправді спільна, і тоді її місце в application.yml, або ви намагаєтеся профілем переписати чужу відповідальність. Скажімо, файл cache намагається керувати datasource.
4. Структура ресурсів: п'ять файлів
До цього ми дивилися на властивості частинами. Нижче збираємо їх в один робочий набір файлів; саме його варто тримати в голові як базову схему проєкту, а не окремі фрагменти властивостей.
Ми багато говорили «тримайте файли короткими», але в реальному проєкті це починається з банальної дисципліни: рівно пʼять зрозумілих файлів у src/main/resources, без авторської поезії в назвах. Коли студент або колега відкриває resources, він має одразу побачити карту режимів, а не загадку з трьох літер.
У нашому наскрізному проєкті це виглядає так:
src/main/resources/
├── application.yml
├── application-standalone.yml
├── application-postgres.yml
├── application-cache.yml
└── application-messaging.yml
Чому це важливо саме для Docker-курсу? Бо Docker любить відтворюваність, а відтворюваність любить передбачуваність. Якщо конфіг розкладено чисто, ви можете запускати той самий image з SPRING_PROFILES_ACTIVE=postgres,cache і бути впевненими: Redis увімкнеться, а datasource залишиться привʼязаним до PostgreSQL, і нічого випадково не перетреться.
Ще одна маленька, але корисна дисципліна: в межах курсу ми не змішуємо YAML і .properties в одній локації. Технічно Spring Boot уміє і так, і так, але новачкові це створює зайвий сюрприз: в одній папці два файли з подібним змістом, і раптом пріоритет не той, який ви очікували. Нам потрібно, щоб last-wins було лише там, де ми свідомо це допускаємо, тобто в порядку профілів, а не тому, що хтось випадково додав application.properties.
І останнє: ми не кладемо spring.profiles.active всередину application-postgres.yml та подібних файлів. Профіль — це те, що приходить ззовні під час запуску. Файл профілю має описувати відмінності, а не намагатися ввімкнути сам себе. Це звучить бадьоро, але в підсумку ви або заплутаєтеся, або отримаєте неявну поведінку.
5. Приклад конфігів для Catalog Service
Тепер — повні варіанти packaged baseline, а не окремі фрагменти властивостей. Ми зробимо їх такими, щоб вони дружили із зовнішніми перевизначеннями через env vars, які ви можете передавати в контейнер.
application.yml — лише загальне
application.yml має бути нудним, і це добре. Якщо він виглядає як «енциклопедія всіх налаштувань Spring», ви програли.
# src/main/resources/application.yml
spring:
profiles:
default: standalone # Профіль за замовчуванням, коли під час запуску не вказано активних профілів
server:
port: ${SERVER_PORT:8080} # Порт можна перевизначити через змінну середовища
app:
export-dir: ${APP_EXPORT_DIR:/app/exports} # Базовий шлях для експорту; його теж можна перевизначити ззовні
Тут видно три речі: є зрозумілий профіль за замовчуванням, порт можна перевизначити ззовні, каталог експорту теж можна перевизначити. Ми не говоримо, де фізично розташовано /app/exports — це окрема файлова механіка, а сьогодні ми тримаємо фокус на схемі конфігурації.
Якщо хочеться додати Actuator exposure, можна, але тримайте це коротко й лише тоді, коли це справді спільне для всіх режимів. Це все ще той самий application.yml:
# src/main/resources/application.yml
management:
endpoints:
web:
exposure:
# Список endpointʼів можна перевизначити ззовні — це зручно для різних середовищ
include: ${MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE:health,info,metrics}
application-standalone.yml — відмінності in-memory режиму
Standalone має вмикатися швидко й не вимагати зовнішніх сервісів. Тому в профільному файлі ми описуємо лише те, що відрізняє цей режим.
# src/main/resources/application-standalone.yml
app:
storage-mode: memory # У standalone використовуємо in-memory сховище
Так, файл вийшов «смішно маленький». І це ідеальний комплімент. Якщо режим відрізняється одним ключем, значить, ви добре спроєктували схему.
application-postgres.yml — datasource і все, що пов'язано з БД
У postgres-режимі зʼявляється база, а отже — datasource. Тут логічно тримати datasource «домом» саме в цьому файлі.
# src/main/resources/application-postgres.yml
app:
storage-mode: postgres # Перемикаємо режим зберігання на БД
spring:
datasource:
# Усі ключові параметри можна перевизначати через env vars — це важливо для контейнерів і Compose
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/catalog}
username: ${SPRING_DATASOURCE_USERNAME:catalog}
password: ${SPRING_DATASOURCE_PASSWORD:catalog}
Зверніть увагу на важливий психологічний ефект: навіть коли ви поки запускаєте не в Compose, а просто в контейнері «у вакуумі», дім datasource лишається тут. Не треба переносити spring.datasource.* у application.yml «тому що так швидше». Це саме те «швидше», яке потім перетворюється на «а чому standalone намагається підключитися до бази?».
application-cache.yml — Redis як додатковий шар
Профіль cache має бути саме «додатком», а не «другою версією postgres». Ми не дублюємо datasource й не змінюємо порт сервера. Ми просто додаємо налаштування Redis і, якщо хочете, один маленький прапорець для власних бінів.
# src/main/resources/application-cache.yml
app:
cache-enabled: true # Увімкнюємо контур кешу; це зручно для умовної реєстрації бінів
spring:
data:
redis:
# Параметри Redis очікуємо ззовні: у контейнерах host майже ніколи не localhost
host: ${SPRING_DATA_REDIS_HOST:localhost}
port: ${SPRING_DATA_REDIS_PORT:6379}
Навіть коли ви поки не реалізували складний cache-сценарій, цей файл усе одно корисний: він наперед фіксує, що Redis — це окремий контур, який вмикається окремим профілем.
application-messaging.yml — RabbitMQ як додатковий шар
Те саме для messaging: додаток, а не «новий світ».
# src/main/resources/application-messaging.yml
app:
messaging-enabled: true # Увімкнюємо messaging-контур; це умовна реєстрація бінів і слухачів
spring:
rabbitmq:
# Параметри RabbitMQ зручно передавати через env vars або Compose
host: ${SPRING_RABBITMQ_HOST:localhost}
port: ${SPRING_RABBITMQ_PORT:5672}
Зверніть увагу: усі параметри «заточені» під зовнішні перевизначення. Це і є та схема, яка робить контейнеризацію чесною: всередині jar є розумні defaults, а ззовні ви можете їх перевизначити, не переписуючи артефакт.
6. Корисні нюанси
Профілі в режимі: default, active і last-wins
У цій схемі spring.profiles.default: standalone живе в application.yml і дає запасний старт без зовнішньої інфраструктури. Коли запуск нічого явно не сказав про профілі, сервіс підніметься в in-memory режимі й не почне раптом шукати PostgreSQL, Redis чи RabbitMQ.
Конкретний режим усе одно задається ззовні через spring.profiles.active: postgres, postgres,cache або postgres,cache,messaging. Тобто packaged baseline залишається тим самим, а потрібна комбінація контурів обирається на старті контейнера або java -jar.
Якщо активні кілька профілів, файли накладаються в їхньому порядку, і пізніше вказаний профіль може перекрити ранніший. Саме тому ми й розклали ключі за ролями: postgres відповідає за datasource, cache — за Redis, messaging — за RabbitMQ. Тоді last-wins залишається рідкісним усвідомленим випадком, а не повсякденною рулеткою.
Зовнішні перевизначення та packaged baseline
З цією пʼятифайловою схемою сервіс має нормально стартувати й без зовнішніх файлів: packaged baseline всередині jar самодостатній. Зовнішній конфіг тут не «друга правда», а тонкий override-шар, який потрібен тільки тоді, коли конкретній машині або конкретному запуску потрібне інше значення.
Якщо ззовні лежать звичайні application.yml / application-{profile}.yml у стандартному місці, Spring Boot може підхопити їх сам. Якщо ви обираєте довільну назву на кшталт runtime.yml або нестандартний шлях, такий файл підключають явно через spring.config.import, spring.config.additional-location або spring.config.location. Важлива не механіка заради механіки, а правило: змінюється runtime-шар, а не jar і не image.
Перевірка активного режиму без endpointʼів
Після складання схеми корисно перевіряти не «нібито працює», а два конкретні сигнали на старті: які профілі активні й яке підсумкове значення отримали один-два характерні ключі на кшталт app.export-dir. Така швидка перевірка показує, чи зійшлися packaged defaults, зовнішнє перевизначення й env vars саме в тому порядку, на який ви розраховували.
Це можна вивести у стартовий лог будь-яким простим способом: через ApplicationRunner, звичайний логер, хоч тимчасовий System.out.println у навчальному проєкті. Сенс не в красі реалізації, а в тому, щоб не вгадувати, стартували ви в standalone чи в postgres,cache, і чи не тягнете ви випадково старе значення app.export-dir.
7. Типові помилки в фінальній схемі конфігурації
У цій темі помилки майже завжди виглядають однаково: застосунок «нібито має працювати так», але запускається «нібито інакше». І майже завжди причина не в Docker і не в Spring Boot як у «чорній магії», а в тому, що конфігурація втратила структуру. Нижче — найчастіші граблі, які варто впізнавати в обличчя.
Помилка №1: application-{profile}.yml перетворюється на копію application.yml.
Це виглядає як «надійніше»: ніби кожен профіль самодостатній. На практиці ви отримуєте пʼять майже однакових файлів, у яких відмінності важко помітити, а зміни потрібно робити одразу в пʼяти місцях. У підсумку хтось забуває змінити одне місце, і починається «чому лише в postgres-режимі порт інший?».
Помилка №2: один і той самий ключ живе в кількох файлах «просто тому, що так сталося».
Скажімо, spring.datasource.url опинився і в application.yml, і в application-postgres.yml. Це небезпечно тим, що ви перестаєте розуміти «дім» властивості. У якийсь момент ви змінюєте одне значення, а застосунок продовжує брати інше, і ви починаєте підозрювати містицизм. Насправді це просто конфлікт джерел.
Помилка №3: профіль cache або messaging починає дублювати налаштування postgres.
Додаткові профілі мають додавати відмінності, а не бути «другою версією postgres-режиму». Якщо application-cache.yml раптом починає містити datasource, ви втрачаєте ідею композиції: вмикання cache має зачіпати лише контур кешу, а не переписувати базовий режим роботи з даними.
Помилка №4: спроба «ввімкнути профіль ізсередини профілю».
Іноді хочеться написати spring.profiles.active: postgres всередині application-postgres.yml. Це виглядає логічно, але на практиці ламає передбачуваність: профіль має приходити ззовні під час запуску, інакше ви отримуєте дивні каскади та не можете очима зрозуміти, чому активувалося саме те, що активувалося.
Помилка №5: зовнішній конфіг стає обовʼязковим і без нього сервіс не стартує.
Коли зовнішній файл не позначений як optional: (або ви використовуєте spring.config.location так, що стандартні місця пошуку вимкнені), ви легко отримуєте ситуацію «на моїй машині працює, у тебе — ні». Для курсу й для шаблону важливо, щоб базовий packaged path лишався робочим: завантажили проєкт, запустили — працює.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ