Пошук уроків, статей та іншого контенту
Спроєктуєте архітектуру з frontend, backend, базою даних і кешем, визначивши залежності та межі сервісів.
Multi-container architecture розділяє застосунок на окремі сервіси, кожен із яких має одну основну відповідальність:
frontend — віддає клієнтський інтерфейс;
backend — реалізує API та бізнес-логіку;
database — зберігає довготривалі дані;
cache — зберігає тимчасові або часто використовувані дані.
У Docker Compose кожен сервіс запускається в окремому контейнері. Контейнери можуть взаємодіяти через внутрішні мережі, але не повинні автоматично отримувати доступ до всіх інших компонентів.
Типовий потік запиту:
Клієнт
│
▼
Frontend
│ HTTP-запити до API
▼
Backend
├──► PostgreSQL
└──► RedisВажливо розділяти:
межі сервісів — хто за яку відповідальність відповідає;
межі мережі — які сервіси можуть спілкуватися між собою;
межі даних — де зберігається стан і хто ним керує.
Frontend не повинен напряму підключатися до PostgreSQL або Redis. Навіть якщо це технічно можливо, така схема порушує інкапсуляцію та створює проблеми з безпекою.
Frontend-контейнер зазвичай:
збирає клієнтський застосунок;
віддає статичні файли;
взаємодіє з backend через HTTP API;
не має доступу до бази даних.
У production frontend часто збирається в окремому build-контейнері, а результати збірки віддаються через Nginx або інший HTTP-сервер.
Backend відповідає за:
автентифікацію та авторизацію;
валідацію запитів;
бізнес-правила;
роботу з базою даних;
роботу з кешем;
формування API-відповідей.
Backend є єдиним сервісом, який повинен мати доступ і до database, і до cache.
База даних зберігає критичний довготривалий стан:
користувачів;
замовлення;
платежі;
налаштування;
інші доменні дані.
Контейнер бази даних не повинен вважатися сховищем даних сам по собі. Дані зберігаються у volume, який переживає перезапуск або пересоздання контейнера.
Redis може використовуватися для:
кешування результатів запитів;
зберігання сесій;
rate limiting;
короткоживучих черг або блокувань.
Кеш не повинен бути єдиним джерелом критичних даних. Якщо Redis очищено, застосунок має залишатися коректним, хоча може працювати повільніше.
Нижче наведено мінімальний, але runnable-приклад із чотирма контейнерами:
frontend — HTTP-сервер клієнтської частини;
backend — HTTP API;
db — PostgreSQL;
cache — Redis.
services:
frontend:
image: node:22-alpine
working_dir: /app
command:
- node
- -e
- |
const http = require("node:http");
const html = `
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<title>Multi-container demo</title>
</head>
<body>
<h1>Frontend працює</h1>
<p>Backend доступний у сусідньому сервісі через внутрішню мережу.</p>
</body>
</html>
`;
http.createServer((request, response) => {
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
response.end(html);
}).listen(3000, "0.0.0.0");
ports:
- "3000:3000"
networks:
- edge
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
backend:
image: node:22-alpine
working_dir: /app
command:
- node
- -e
- |
const http = require("node:http");
const port = 4000;
const server = http.createServer((request, response) => {
if (request.url === "/health") {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ status: "ok" }));
return;
}
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({
service: "backend",
databaseHost: process.env.DATABASE_HOST,
cacheHost: process.env.CACHE_HOST
}));
});
server.listen(port, "0.0.0.0");
environment:
DATABASE_HOST: db
DATABASE_PORT: 5432
CACHE_HOST: cache
CACHE_PORT: 6379
expose:
- "4000"
networks:
- edge
- data
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
healthcheck:
test:
- CMD
- node
- -e
- "fetch('http://127.0.0.1:4000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app-password
volumes:
- postgres_data:/var/lib/postgresql/data
expose:
- "5432"
networks:
- data
healthcheck:
test:
- CMD-SHELL
- pg_isready -U app -d app
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
cache:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
expose:
- "6379"
networks:
- data
healthcheck:
test:
- CMD
- redis-cli
- ping
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
networks:
edge:
data:
volumes:
postgres_data:
redis_data:Збережіть файл як compose.yaml і запустіть:
docker compose up -dПеревірити frontend можна за адресою http://localhost:3000.
Перевірити backend можна з окремого контейнера в мережі Compose:
docker compose exec backend node -e "fetch('http://localhost:4000/health').then(r => r.text()).then(console.log)"Зупинити контейнери:
docker compose downЩоб видалити також volumes із даними:
docker compose down -vУ Compose ім'я сервісу стає DNS-ім'ям у відповідній мережі.
Наприклад, backend підключається до PostgreSQL не через localhost, а через hostname:
db:5432Підключення до Redis:
cache:6379localhost усередині контейнера означає цей самий контейнер, а не інший сервіс.
Тому такі значення неправильні для backend-контейнера:
DATABASE_HOST=localhost
CACHE_HOST=localhostПравильний варіант:
DATABASE_HOST=db
CACHE_HOST=cacheПорт 5432 або 6379 є внутрішнім портом сервісу. Він не потребує публікації на хост, якщо до нього звертаються лише інші контейнери.
Залежності можна описати як граф:
frontend ──► backend ──► db
└──► cacheУ Compose depends_on визначає порядок запуску, але сам по собі не гарантує, що сервіс уже готовий приймати запити.
Наприклад, контейнер PostgreSQL може бути вже запущеним, але процес бази даних ще не завершив ініціалізацію. Тому backend може отримати помилку підключення.
Для цього використовуються:
healthcheck — описує, як перевірити готовність сервісу;
condition: service_healthy — дозволяє запускати залежний сервіс після успішної перевірки.
У прикладі backend очікує на готовність і PostgreSQL, і Redis:
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthyОднак healthcheck не замінює retry-логіку в самому backend. Після перезапуску бази даних застосунок повинен уміти повторити підключення, а не завершуватися назавжди після першої помилки.
У прикладі використано дві мережі:
networks:
edge:
data:Frontend підключений лише до edge:
frontend:
networks:
- edgeBackend підключений до обох мереж:
backend:
networks:
- edge
- dataDatabase і cache підключені лише до data:
db:
networks:
- data
cache:
networks:
- dataЦе створює такі правила:
frontend може взаємодіяти з backend;
backend може взаємодіяти з database і cache;
frontend не бачить database і cache через мережу;
database та cache не доступні безпосередньо з хоста.
Таке розділення не є повноцінною заміною автентифікації, але зменшує поверхню атаки та кількість можливих помилок.
ports та exposeЦі параметри мають різне призначення.
portsports публікує порт контейнера на хості:
ports:
- "3000:3000"Після цього сервіс доступний із хоста через localhost:3000.
exposeexpose документує внутрішній порт сервісу та робить його доступним для комунікації між контейнерами тієї самої мережі:
expose:
- "4000"expose не публікує порт на хост.
Для database і cache в development-архітектурі часто достатньо expose. Це краще, ніж без потреби відкривати PostgreSQL або Redis назовні.
Конфігурація не повинна бути жорстко закодована в backend:
const databaseHost = "db";
const databasePort = 5432;Замість цього застосунок читає змінні середовища:
const databaseHost = process.env.DATABASE_HOST;
const databasePort = Number(process.env.DATABASE_PORT || 5432);Compose передає значення сервісу:
environment:
DATABASE_HOST: db
DATABASE_PORT: 5432
CACHE_HOST: cache
CACHE_PORT: 6379Назви змінних мають описувати логічну конфігурацію застосунку, а не деталі конкретного середовища. Завдяки цьому один і той самий образ backend можна використовувати в development, staging і production з різними значеннями конфігурації.
Паролі, токени та інші секрети не слід зберігати у відкритому compose.yaml. Для локального навчального прикладу це допустимо, але в реальному середовищі секрети потрібно передавати через захищений механізм конфігурації платформи.
Контейнер є тимчасовим середовищем. Якщо контейнер PostgreSQL видалити без volume, дані можуть бути втрачені.
У Compose volume оголошується окремо:
volumes:
postgres_data:
redis_data:І монтується в потрібний контейнер:
db:
volumes:
- postgres_data:/var/lib/postgresql/dataДля PostgreSQL /var/lib/postgresql/data — каталог, у якому зберігаються дані бази.
Для Redis у прикладі ввімкнено AOF-збереження:
cache:
command: redis-server --appendonly yes
volumes:
- redis_data:/dataЦе не перетворює Redis на основну базу даних. Воно лише дозволяє зберігати Redis-стан між перезапусками відповідно до його конфігурації.
Сервіс повинен мати чітку відповідальність і власний життєвий цикл.
До backend належать:
HTTP API;
бізнес-логіка;
міграції бази даних;
клієнти PostgreSQL і Redis;
перевірка конфігурації.
До frontend належать:
вихідні файли UI;
процес збірки;
статичні ресурси;
клієнтський код для виклику API.
Не слід:
додавати код frontend у контейнер PostgreSQL;
розміщувати SQL-логіку в клієнтському JavaScript;
підключати frontend напряму до Redis;
використовувати filesystem контейнера як постійне сховище;
змішувати процес backend і бази даних в одному контейнері.
Один контейнер на сервіс спрощує оновлення, масштабування, моніторинг і діагностику.
Правильна архітектура повинна враховувати не лише успішний старт, а й повторні запуски.
Backend має:
перевіряти обов'язкові змінні середовища;
повторювати підключення до залежностей;
коректно реагувати на сигнали завершення;
не втрачати дані, які належать database;
мати endpoint для перевірки стану.
Healthcheck має перевіряти реальну готовність сервісу. Якщо endpoint /health повертає 200, це означає лише, що HTTP-сервер працює. Для складнішої перевірки можна окремо перевіряти:
доступність бази даних;
доступність Redis;
стан міграцій;
готовність критичних залежностей.
Водночас healthcheck не повинен виконувати важкі операції або змінювати дані.
Stateless-сервіси можна масштабувати горизонтально:
docker compose up -d --scale backend=3Це може створити кілька контейнерів backend. Для такого масштабування backend не повинен зберігати важливий стан у локальній файловій системі або в пам'яті одного контейнера.
Стан потрібно винести до:
PostgreSQL;
Redis;
зовнішнього object storage;
іншого спеціалізованого сховища.
Frontend зі статичними файлами також легко масштабується. Database зазвичай не масштабується простим дублюванням контейнера: для неї потрібні окремі стратегії реплікації, резервного копіювання та керування станом.
localhost між контейнерамиНеправильно:
DATABASE_HOST=localhostУ backend-контейнері це посилання на сам backend.
Правильно:
DATABASE_HOST=dbНе потрібно публікувати PostgreSQL і Redis на хост, якщо вони доступні лише backend.
Замість цього:
db:
expose:
- "5432"а не:
db:
ports:
- "5432:5432"depends_on перевіряє готовністьЗалежність визначає порядок запуску, але без healthcheck сервіс може бути недоступним у момент старту backend.
Пересоздання контейнера без volume може призвести до втрати даних.
Між контейнерами використовується внутрішній порт сервісу, а не порт, опублікований на хості.
Наприклад, якщо використано:
ports:
- "15432:5432"backend усе одно підключається до:
db:5432Порт 15432 потрібен лише для підключення до PostgreSQL із хоста.
Frontend не повинен знати внутрішні hostname database або cache. Йому достатньо публічного контракту backend API.
Контейнер, у якому одночасно запускаються frontend, backend і database, складніше:
оновлювати;
масштабувати;
перезапускати;
діагностувати;
забезпечувати.
Розділення на сервіси робить залежності явними.
Перед запуском перевірте:
Чи має кожен сервіс одну основну відповідальність?
Чи спілкуються сервіси через імена Compose, а не через localhost?
Чи відкриті назовні лише потрібні порти?
Чи ізольовані database і cache від frontend?
Чи зберігаються дані database у volume?
Чи мають критичні залежності healthcheck?
Чи може backend пережити тимчасову недоступність database або cache?
Чи передається конфігурація через змінні середовища?
Чи можна масштабувати stateless-сервіси без перенесення локального стану?
Чи описані всі залежності в compose.yaml, а не лише в документації?
Multi-container архітектура для Full Stack складається з ізольованих сервісів із чіткими межами:
frontend відповідає за клієнтський інтерфейс;
backend є посередником між клієнтом і сховищами;
database зберігає довготривалий стан;
cache оптимізує доступ до тимчасових даних.
Docker Compose дозволяє описати:
залежності між сервісами;
внутрішні мережі;
healthcheck;
змінні середовища;
volumes;
публічні та внутрішні порти.
Ключовий принцип: кожен сервіс повинен мати мінімально необхідний доступ до інших компонентів, а стан має зберігатися поза життєвим циклом окремого контейнера.