JavaRush /Курси /Spring Data JPA /Локальна PostgreSQL у Docker Compose

Локальна PostgreSQL у Docker Compose

Spring Data JPA
Рівень 4 , Лекція 0
Відкрита

1. Жива PostgreSQL як старт стенду

Якщо раніше ви вивчали Spring «у вакуумі», могло здаватися, що головне — це application.yml, анотації та якісь стартові модулі. Але в шарі даних усе починається не з конфігурації, а з живої й доступної бази даних. Поки бази немає, обговорювати підключення застосунку просто ні до чого. Інакше виходить ситуація: «Я налаштував DataSource, але під’єднуватися немає до чого» — приблизно як налаштовувати Wi‑Fi-роутер у квартирі, де ще не підвели електрику.

Є й більш приземлена, але не менш важлива причина: курс має бути відтворюваним. Коли в половини групи PostgreSQL встановлено «якось», в іншої — взагалі не встановлено, а в третьої — інша версія та ще й старі бази, ми витрачаємо час не на навчання, а на археологію чужих ноутбуків. Тому стенд описуємо в репозиторії як частину проєкту: одна команда — і в усіх одна й та сама БД.

До цього моменту в нас уже є базова картина реляційної БД: таблиці, ключі, JOIN, INSERT/UPDATE/DELETE, commit і rollback. Тепер цій картині потрібна жива PostgreSQL, інакше DataSource, застосунок і перші @Entity так і залишаться висіти в повітрі. Спершу піднімаємо реальну базу, а вже потім будуємо підключення й увесь інший шар даних поверх неї.

І ще: PostgreSQL ми обираємо як основну точку відліку курсу цілком свідомо. Це не означає, що інші СУБД погані. Це означає, що нам потрібна одна конкретна реальність, під яку будуть написані приклади, логи, сценарії та подальші практики.

Невелика схема, щоб було видно, де що живе:

flowchart TD
    %% Репозиторій містить docker-compose.yml, який описує стенд (Compose-«рецепт»)
    Repo["Репозиторій shop-data-jpa, файл docker-compose.yml"] -->|docker compose up| Docker["Docker Engine"]
    %% Docker піднімає контейнер Postgres за описом у compose-файлі
    Docker --> PG["Контейнер postgres"]
    %% Дані живуть у volume і переживають перезапуски та створення контейнера заново
    PG --> Volume["Volume: shop-postgres-data, дані між перезапусками"]
    %% Розробник підключається до Postgres через перенаправлений порт
    Dev["Ви (термінал / IDE)"] -->|5432| PG

2. Docker Compose як рецепт стенду

Docker Compose зручно сприймати як невеликий, але дуже практичний «рецепт»: які сервіси потрібні проєкту локально, які версії взяти, які порти відкрити, які змінні середовища задати й які дані зберігати. Для навчального проєкту це особливо цінно. Ми не граємо в «нехай кожен поставить PostgreSQL, як уміє», а тримаємо рівно те, що потрібно, в одному файлі. І цей файл лежить у репозиторії поруч із кодом, а не десь у папці «Downloads/налаштування_потім_розберуся».

Але й переоцінювати Compose не варто. Ми не йдемо в оркестрацію контейнерів та решту дорослого життя. У межах курсу Compose — це просто спосіб підняти локальну PostgreSQL так, щоб будь-який студент міг повторити кроки без шаманства. Він дає ізоляцію (база живе в контейнері), просту заміну (не сподобалося — створили заново) і повторюваність (у всіх однаково).

Три поняття краще розрізняти з першого дня, інакше плутанина почнеться одразу.

Образ (image) — це «заготовка» PostgreSQL потрібної версії. Контейнер (container) — уже запущений екземпляр цього образу, тобто живий процес бази. А сервіс (service) у Compose — це опис того, як цей контейнер має запускатися: порти, змінні, volume та все інше.

3. Мінімальний docker-compose.yml

docker-compose.yml виглядає страшно рівно до того моменту, поки не стає зрозуміло: це звичайна декларація «хочу ось такий сервіс, з такого образу, з такими портами й такими налаштуваннями». У курсі ми починаємо з мінімальної версії, щоб ви не потонули в «зайвих» рядках. Якщо щось знадобиться пізніше, додамо це акуратно й по суті.

Ось мінімальний Compose-файл для PostgreSQL — стартова база проєкту:

services:
  postgres: # імʼя сервісу (будемо використовувати в командах docker compose ... postgres)
    image: postgres:16 # фіксуємо версію, не використовуємо latest
    ports:
      - "5432:5432" # порт хоста : порт контейнера
    environment:
      POSTGRES_DB: shop # імʼя БД (створюється під час первинної ініціалізації)
      POSTGRES_USER: shop # користувач (створюється під час первинної ініціалізації)
      POSTGRES_PASSWORD: shop # пароль (створюється під час первинної ініціалізації)

Тепер людське пояснення без містики.

Ключ services означає: «ось список сервісів, які мені потрібні». Ми заводимо один сервіс з імʼям postgres — це просто імʼя, за яким потім будемо звертатися до нього в командах (docker compose logs postgres тощо).

image: postgres:16 означає: «завантаж і використовуй офіційний образ Postgres версії 16». Версію фіксуємо явно, тому що latest — чудовий спосіб наробити собі пригод. Іноді здається, що latest означає «найновіше й найкраще», але на практиці це радше «сьогодні працювало, завтра раптово перестало, і тепер ви вивчаєте DevOps замість JPA».

ports — це міст між контейнером і вашим компʼютером. Ми перенаправляємо порт PostgreSQL назовні, щоб до нього могли підключатися інструменти на хості: Java-застосунок, psql, DBeaver — що завгодно.

environment задає параметри первинної ініціалізації: імʼя БД, користувача й пароль. Так, пароль shop виглядає як «найзахищеніша система у Всесвіті». Для навчального стенду це нормально. Зараз ми не будуємо бойову систему безпеки, ми будуємо відтворюваність. Але до дисципліни звикати все одно потрібно: значення мають бути явними й узгодженими.

Щоб закріпити модель, ось маленька таблиця: як читати Compose очима розробника.

Шматок файлу Що означає в реальності Навіщо нам це в курсі
services.postgres «Сервіс бази даних» Один стенд для всіх
image: postgres:16 «Фіксована версія Postgres» Однакова поведінка й логи
ports: "5432:5432" «Доступ до Postgres з хоста» Щоб застосунок міг підключитися
environment: POSTGRES_* «Ініціалізація бази» Імʼя БД і логін/пароль єдині для проєкту

4. Проброс портів у Compose

Перенаправлення портів — одне з тих місць, де особливо легко заплутатися. У записі "5432:5432" ліве число стосується вашого компʼютера, тобто хоста, а праве — контейнера. По суті ви говорите Docker: «усе, що прийшло на порт 5432 на моїй машині, перенаправ на порт 5432 всередині контейнера». Тому IDE або Spring Boot підключаються до localhost:5432.

Цей запис зручно уявляти як перехідник: зовні в нього один розʼєм, всередині — інший. Часта помилка — думати: «обидва порти однакові, отже й розуміти тут нічого». Розуміти якраз потрібно — хоча б до того моменту, коли на вашому компʼютері порт 5432 уже зайнятий якимось локально встановленим Postgres.

Якщо порт зайнятий, це не трагедія й не «Docker зламався». Це просто конфлікт портів. У такому разі змінюєте ліву частину — порт хоста, а праву залишаєте 5432, тому що саме його слухає PostgreSQL всередині контейнера.

Приклад, якщо на хості ви хочете використовувати порт 15432:

services:
  postgres:
    image: postgres:16
    ports:
      - "15432:5432" # 15432 на хості -> 5432 всередині контейнера

Тепер зовні підключення буде йти на localhost:15432, а всередині все так само залишається PostgreSQL на 5432. І так, це той випадок, коли «одна цифра» вирішує пів години страждань.

Щоб остаточно прибити плутанину, ось таблиця:

Де ви перебуваєте Порт Що це означає
Хост (ваш ноутбук) 5432 Порт, на який підключається застосунок
Контейнер (Postgres всередині Docker) 5432 Порт, на якому слухає Postgres

5. Volume для даних PostgreSQL

Контейнер — річ одноразова. У цьому його плюс: швидко підняв, швидко видалив. Але в цьому ж і ризик: видалили контейнер — втратили дані. Тому для бази даних потрібен механізм, який зберігає дані між перезапусками, створенням контейнера заново та вашим раптовим «ой, я натиснув не ту кнопку». Цей механізм у Docker називається volume.

Volume зручно сприймати як окреме сховище з даними, яке Docker тримає окремо від контейнера. Контейнер можна видалити, а volume залишиться. На практиці це означає просту річ: ви зупинили PostgreSQL, потім знову запустили — і дані на місці. Для навчального проєкту це критично: сьогодні ви просто запускаєте базу, завтра додасте таблиці, післязавтра будете зберігати сутності, і все це не має зникати після кожного down.

Мінімальний приклад додавання volume:

services:
  postgres:
    image: postgres:16
    volumes:
      # named volume -> стандартний каталог даних Postgres всередині контейнера
      - shop-postgres-data:/var/lib/postgresql/data

volumes:
  # оголошуємо named volume (Docker зберігатиме його окремо від контейнера)
  shop-postgres-data:

Зверніть увагу: шлях /var/lib/postgresql/data — це стандартне місце, де PostgreSQL зберігає свої файли даних всередині контейнера. Ми під’єднуємо туди named volume shop-postgres-data.

Тут важливо зрозуміти одну дуже практичну річ: змінні середовища POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD працюють тільки під час первинної ініціалізації даних. Якщо volume вже існує, PostgreSQL бачить, що дані на місці, і не створює кластер заново, не змінює пароль і не створює базу ще раз. Тому новачки часто змінюють пароль у compose-файлі, перезапускають контейнер і дивуються, що «нічого не змінилося». Це не баг, а захист від випадкового знищення даних.

І давайте одразу розмежуємо команди: тут теж багато магічних очікувань.

Якщо ви виконуєте docker compose down, контейнери й мережа видаляються, але volume зазвичай залишається. А docker compose down -v каже Docker: «видали ще й volume». Це вже режим «почати з чистого аркуша», і вмикати його краще свідомо.

6. Життєвий цикл Docker Compose

Цінність Docker Compose — у простому й повторюваному життєвому циклі: підняв стенд, переконався, що він живий, подивився логи, зупинив. Так база перестає бути «магічною штукою десь там» і стає зрозумілим сервісом, яким розробник керує сам. У реальній роботі саме це відрізняє спокійний проєкт від проєкту, де база «іноді не працює, але ми не знаємо чому».

Мінімальний цикл команд виглядає так:

# Піднімаємо сервіси у фоновому режимі (detached)
docker compose up -d
# Перевіряємо статус контейнерів і перенаправлені порти
docker compose ps
# Дивимося логи саме сервісу postgres
docker compose logs postgres

Перша команда піднімає сервіси у фоновому режимі (-d = detached). Друга показує статус: чи запущено контейнер, які порти перенаправлено. Третя дає логи саме PostgreSQL-сервісу.

Окрема важлива думка: «контейнер запущено» не завжди означає «PostgreSQL уже готова приймати зʼєднання». Іноді Postgres кілька секунд стартує, застосовує ініціалізацію, створює базу. Тому логи — це не «шум», а спосіб зрозуміти, на якому етапі старту ви зараз перебуваєте. У логах зазвичай зʼявляється фраза на кшталт database system is ready to accept connections. Щойно ви її побачили, можна вважати, що база готова.

Щоб не вірити логам на слово, можна зробити найчеснішу перевірку: виконати простий SQL-запит. Для цього зручно зайти в контейнер і запустити psql. Наприклад так:

# Запускаємо psql всередині контейнера й виконуємо простий запит (smoke-check)
docker compose exec postgres psql -U shop -d shop -c "select 1;"

Якщо все добре, ви отримаєте вивід з однією колонкою та значенням 1. Це максимально чесна перевірка: база не тільки «живе», вона реально відповідає на запити.

А зупинка стенду залежить від того, що саме ви хочете. Якщо потрібно просто «вимкнути на ніч», достатньо зупинки:

# Зупинити контейнери без видалення (дані залишаються, контейнери залишаються)
docker compose stop

Якщо ви хочете видалити контейнери, але залишити дані у volume, використовуйте:

# Видалити контейнери й мережу, але залишити volume (дані збережуться)
docker compose down

А якщо хочете видалити взагалі все, включно з даними, то це вже режим «я точно розумію, що роблю»:

# Видалити контейнери, мережу та volume (дані будуть видалені)
docker compose down -v

7. Compose-файл у репозиторії

На цьому етапі легко зробити типову помилку: «це ж просто локальна база, нехай compose-файл лежить десь окремо». У навчальному проєкті це здається нешкідливим — рівно до моменту, коли ви переносите проєкт на інший компʼютер, ділитеся ним з одногрупником або повертаєтеся до коду через два тижні. І тут раптово виявляється, що поруч із кодом немає «рецепта» стенду, і ви знову згадуєте: який порт, який користувач, який пароль, яка версія Postgres.

Тому правило курсу просте: docker-compose.yml лежить у корені репозиторію shop-data-jpa. Він комітиться разом із кодом. Він стає частиною документації у вигляді коду. І це не формальність: далі ви будете нарощувати проєкт, і будь-які практики (збереження сутностей, generated SQL, транзакційні сценарії) мають працювати на одному й тому самому стенді.

Ще один важливий момент — узгодженість значень. Сьогодні ми обираємо базу shop, користувача shop, пароль shop, порт 5432. Це буде «якір», під який далі підлаштовуються налаштування застосунку. Наша мета — не просто підняти PostgreSQL, а зафіксувати домовленість: які параметри підключення вважаються нормою в проєкті.

8. Типові помилки під час підняття PostgreSQL через Docker Compose

Помилка № 1: переплутали порядок портів у "5432:5432" і потім «нічого не підключається».
У записі ports дуже легко подумки вирішити, що порядок неважливий. На практиці він критичний: ліворуч порт хоста, праворуч порт контейнера. Якщо ви підключаєтеся не туди, застосунок чесно відповідає connection refused, а ви починаєте підозрювати Spring, JDBC і фазу місяця. Лікується це просто: звіряєте docker compose ps, дивитеся перенаправлений порт і підключаєтеся саме до нього.

Помилка № 2: змінили POSTGRES_PASSWORD у compose-файлі, перезапустили й здивувалися, що пароль «не змінився».
PostgreSQL ініціалізує користувача й пароль під час першого створення даних. Якщо у вас під’єднано volume, значить дані вже існують, і Postgres не буде створювати кластер заново та змінювати пароль автоматично. Це захищає дані, але ламає очікування новачка. Якщо ви хочете застосувати нові значення POSTGRES_* «з нуля», потрібно свідомо видалити volume через docker compose down -v, розуміючи, що разом із ним зникнуть і дані.

Помилка № 3: забули додати volume й отримали «зникаючу базу».
Без volume база живе всередині контейнера. Видалили контейнер — видалили все. Це особливо прикро, коли ви впевнені, що «дані десь є», а після down вони раптово випарувалися. Навіть у навчальному проєкті краще відразу робити правильно: named volume — мінімальна страховка від випадкової втрати результату.

Помилка № 4: побачили Up у docker compose ps і вирішили, що Postgres уже готова, хоча вона ще стартує.
Контейнер може бути “Up”, але PostgreSQL усередині ще виконує ініціалізацію. Потім ви запускаєте застосунок, ловите помилку підключення й починаєте перебирати налаштування. Спокійніший шлях — подивитися docker compose logs postgres і дочекатися моменту, коли база повідомить, що готова приймати зʼєднання, або виконати чесний select 1 через psql.

Помилка № 5: compose-файл лежить «десь на компʼютері», а не в репозиторії, і команда не може відтворити стенд.
Це одна з тих помилок, які не вибухають одразу, тому особливо підступні. Сьогодні ви все памʼятаєте, завтра — уже ні. Правильна дисципліна проста: docker-compose.yml — такий самий артефакт проєкту, як build.gradle.kts. Він має жити поруч із кодом, комітитися й бути частиною «як запустити проєкт» нарівні з командою ./gradlew bootRun.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ