У попередній статті ми познайомилися з основами Docker Compose: створили базовий docker-compose.yml файл, запустили двосервісний застосунок (C# API + PostgreSQL), та навчилися керувати lifecycle через команди docker compose up/down. Це був фундамент — мінімальна конфігурація, достатня для розуміння концепції.
Але реальні застосунки рідко обмежуються двома сервісами. Сучасна архітектура веб-застосунку може включати:
Кожен з цих компонентів має свої залежності (база даних має запуститися до API), мережеві вимоги (frontend не повинен мати прямого доступу до бази даних), вимоги до даних (volumes для персистентності), та конфігурацію (environment variables, secrets).
Проблема: Як організувати такий складний multi-service застосунок у Docker Compose, щоб він був:
У цій статті ми детально розглянемо просунуті можливості Docker Compose, які дозволяють вирішити ці проблеми. Ми побудуємо повноцінний multi-service застосунок з правильною архітектурою, навчимося керувати залежностями через depends_on та health checks, організуємо мережеву топологію з ізоляцією рівнів, налаштуємо profiles для різних середовищ, та застосуємо patterns для перевикористання конфігурацій.
Коли ви запускаєте docker compose up, Compose створює та запускає всі сервіси паралельно (для швидкості). Але це створює проблему:
Сценарій: Ваш ASP.NET Core API намагається підключитися до PostgreSQL при старті (у Program.cs через DbContext). Якщо контейнер з API запуститься раніше за контейнер з базою даних, застосунок отримає помилку підключення та завершиться з exit code 1.
// Program.cs — типовий код ініціалізації
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Якщо PostgreSQL ще не готовий — exception!
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate(); // ❌ Npgsql.NpgsqlException: Connection refused
}
Наївне рішення: Додати sleep 10 перед запуском API. Це anti-pattern — ви не знаєте, скільки часу потрібно базі даних для ініціалізації (залежить від навантаження системи, розміру даних, тощо).
Правильне рішення: Використовувати механізми Docker Compose для керування залежностями.
depends_on — базовий механізмДиректива depends_on дозволяє вказати, що один сервіс залежить від іншого. Compose запустить залежності перед залежним сервісом.
Синтаксис (базовий):
services:
api:
image: myapp-api:latest
depends_on:
- db
- cache
db:
image: postgres:16
cache:
image: redis:7-alpine
Що відбувається:
db та cache (паралельно)db та cache перейдуть у стан runningapiВажливо: depends_on чекає лише на запуск контейнера (стан running), а не на готовність сервісу всередині. PostgreSQL контейнер може бути у стані running, але сам PostgreSQL сервер ще ініціалізується (створює системні таблиці, завантажує конфігурацію). Це займає 2-5 секунд.
Результат: API все одно може отримати помилку підключення, якщо спробує з'єднатися з базою даних до завершення її ініціалізації.
depends_on з умовами — правильний підхідDocker Compose підтримує розширений синтаксис depends_on з умовами очікування:
services:
api:
image: myapp-api:latest
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
cache:
image: redis:7-alpine
Розбір конфігурації:
1. condition: service_healthy — Compose чекає, поки сервіс db перейде у стан healthy (не просто running). Це означає, що PostgreSQL дійсно готовий приймати з'єднання.
2. healthcheck — визначає, як перевіряти здоров'я сервісу:
test — команда для перевірки. pg_isready — утиліта PostgreSQL, яка повертає exit code 0, якщо сервер готовийinterval: 5s — перевіряти кожні 5 секундtimeout: 3s — якщо команда не завершилася за 3 секунди — вважати невдалоюretries: 5 — після 5 невдалих спроб вважати контейнер unhealthystart_period: 10s — grace period після запуску контейнера, коли невдалі перевірки не враховуються (дає час на ініціалізацію)3. condition: service_started — для Redis достатньо просто дочекатися запуску контейнера (Redis стартує дуже швидко, health check не критичний).
Lifecycle з health checks:
Що відбувається покроково:
db та cache паралельноrunning, але сервіси всередині ще ініціалізуютьсяdb (перша спроба після start_period)pg_isready повертає exit code 0db переходить у стан healthy, cache вже у стані startedapiРезультат: API ніколи не отримає помилку підключення, бо Compose гарантує, що база даних готова до прийому з'єднань.
depends_onDocker Compose підтримує чотири типи умов:
running. Найшвидша умова, але не гарантує готовність сервісу всередині.Використання: Для сервісів, які стартують миттєво (Redis, Memcached) або коли залежність не критична.healthy (health check повертає success). Вимагає наявності healthcheck у сервісі.Використання: Для баз даних, message queues, будь-яких сервісів з тривалою ініціалізацією.Часто потрібно виконати міграції бази даних перед запуском API. Для цього використовується окремий сервіс з умовою service_completed_successfully:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
migrations:
image: myapp-api:latest
command: dotnet ef database update
depends_on:
db:
condition: service_healthy
environment:
ConnectionStrings__DefaultConnection: "Host=db;Database=myapp;Username=postgres;Password=secret"
restart: "no" # Не перезапускати після завершення
api:
image: myapp-api:latest
depends_on:
migrations:
condition: service_completed_successfully
ports:
- "5000:8080"
environment:
ConnectionStrings__DefaultConnection: "Host=db;Database=myapp;Username=postgres;Password=secret"
Порядок виконання:
db, чекаємо healthymigrations, виконує dotnet ef database update, завершується з exit code 0api — база даних вже має актуальну схемуВажливо: restart: "no" для migrations — інакше Compose буде перезапускати контейнер після завершення, що призведе до повторного виконання міграцій.
За замовчуванням Docker Compose створює одну мережу для всіх сервісів у docker-compose.yml. Це означає, що кожен сервіс може з'єднатися з будь-яким іншим за іменем:
services:
frontend:
image: myapp-frontend:latest
api:
image: myapp-api:latest
db:
image: postgres:16
У цій конфігурації frontend може напряму підключитися до db за адресою postgresql://db:5432. Це порушення принципу least privilege — frontend не повинен мати доступу до бази даних, він має комунікувати лише з API.
Проблеми плоскої мережі:
Рішення: Створити кілька мереж з чіткою ізоляцією рівнів.
Типова трирівнева архітектура веб-застосунку:
services:
frontend:
image: myapp-frontend:latest
networks:
- frontend-network
ports:
- "3000:3000"
api:
image: myapp-api:latest
networks:
- frontend-network # Доступний для frontend
- backend-network # Має доступ до DB
ports:
- "5000:8080"
db:
image: postgres:16
networks:
- backend-network # Ізольований від frontend
environment:
POSTGRES_PASSWORD: secret
cache:
image: redis:7-alpine
networks:
- backend-network
networks:
frontend-network:
driver: bridge
backend-network:
driver: bridge
Мережева топологія:
Що відбувається:
frontend може з'єднатися з api (обидва у frontend-network)api може з'єднатися з db та cache (всі у backend-network)frontend НЕ МОЖЕ з'єднатися з db або cache (різні мережі, немає маршруту)Перевірка ізоляції:
# Спроба підключитися до PostgreSQL з frontend-контейнера
docker compose exec frontend ping db
# ping: db: Name or service not known ✓
# Спроба підключитися до PostgreSQL з api-контейнера
docker compose exec api ping db
# PING db (172.20.0.3): 56 data bytes ✓
За замовчуванням Docker Compose додає префікс до імен мереж: <project-name>_<network-name>. Якщо ваш проєкт знаходиться у директорії myapp, мережа frontend-network матиме реальне ім'я myapp_frontend-network.
Перевірка:
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# a1b2c3d4e5f6 myapp_frontend-network bridge local
# f6e5d4c3b2a1 myapp_backend-network bridge local
Кастомне ім'я проєкту:
Ви можете змінити префікс через змінну оточення COMPOSE_PROJECT_NAME або прапорець -p:
# Через змінну оточення
export COMPOSE_PROJECT_NAME=kostyl
docker compose up
# Через прапорець
docker compose -p kostyl up
Використання зовнішніх мереж:
Якщо мережа вже існує (створена вручну або іншим Compose-проєктом), використовуйте external: true:
networks:
shared-network:
external: true
name: company-shared-network
Це корисно, коли кілька застосунків мають комунікувати між собою (наприклад, через спільний Nginx reverse proxy).
У попередній статті ми використовували іменовані томи для збереження даних PostgreSQL. У multi-service застосунках томів може бути багато:
services:
db:
image: postgres:16
volumes:
- postgres-data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
- redis-data:/data
rabbitmq:
image: rabbitmq:3-management
volumes:
- rabbitmq-data:/var/lib/rabbitmq
volumes:
postgres-data:
driver: local
redis-data:
driver: local
rabbitmq-data:
driver: local
Важливо: Секція volumes: на верхньому рівні декларує томи. Якщо ви не вкажете том у цій секції, Compose створить його автоматично, але з префіксом проєкту.
Для передачі конфігураційних файлів у контейнери використовуйте bind mounts:
services:
nginx:
image: nginx:alpine
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./static:/usr/share/nginx/html:ro
ports:
- "80:80"
db:
image: postgres:16
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
volumes:
postgres-data:
Розбір:
./nginx/nginx.conf:/etc/nginx/nginx.conf:ro — монтує файл з хоста у контейнер у read-only режимі (:ro)./init-scripts:/docker-entrypoint-initdb.d:ro — PostgreSQL автоматично виконає всі .sql та .sh файли з цієї директорії при першому запускуСтруктура проєкту:
myapp/
├── docker-compose.yml
├── nginx/
│ ├── nginx.conf
│ └── conf.d/
│ └── default.conf
├── init-scripts/
│ ├── 01-schema.sql
│ └── 02-seed.sql
└── static/
└── index.html
Для розробки зручно монтувати вихідний код у контейнер, щоб зміни застосовувалися без пересборки образу:
services:
api:
build: ./backend
volumes:
- ./backend:/app
- /app/bin
- /app/obj
environment:
- ASPNETCORE_ENVIRONMENT=Development
- DOTNET_USE_POLLING_FILE_WATCHER=true
command: dotnet watch run
Розбір:
./backend:/app — монтує вихідний код у контейнер/app/bin та /app/obj — анонімні томи, які перекривають директорії з хоста (інакше артефакти збірки з хоста конфліктуватимуть з контейнерними)dotnet watch run — автоматично перезапускає застосунок при зміні файлівDOTNET_USE_POLLING_FILE_WATCHER=true — використовувати polling замість inotify (потрібно для Docker на macOS/Windows)COPY у Dockerfile.Docker Compose дозволяє запускати кілька екземплярів одного сервісу через прапорець --scale:
services:
api:
image: myapp-api:latest
networks:
- backend-network
environment:
- DATABASE_URL=postgresql://db:5432/myapp
db:
image: postgres:16
networks:
- backend-network
Запуск 3 екземплярів API:
docker compose up --scale api=3
Що відбувається:
myapp-api-1, myapp-api-2, myapp-api-3backend-networkdb за іменемПроблема: Якщо у сервісі вказано ports, масштабування не працюватиме:
services:
api:
image: myapp-api:latest
ports:
- "5000:8080" # ❌ Конфлікт портів при --scale api=3
Помилка:
Error response from daemon: driver failed programming external connectivity:
Bind for 0.0.0.0:5000 failed: port is already allocated
Рішення 1: Видалити ports та використовувати reverse proxy (Nginx, Traefik) для load balancing:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- api
api:
image: myapp-api:latest
# Без ports — доступний лише всередині мережі
nginx.conf з upstream:
upstream api_backend {
server api:8080; # Docker DNS автоматично балансує між екземплярами
}
server {
listen 80;
location /api/ {
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Рішення 2: Використовувати динамічне призначення портів:
services:
api:
image: myapp-api:latest
ports:
- "5000-5010:8080" # Compose призначить вільний порт з діапазону
deploy (Compose Spec v3+)Для декларативного масштабування використовуйте секцію deploy:
services:
api:
image: myapp-api:latest
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
Важливо: Секція deploy ігнорується у docker compose up (працює лише у Docker Swarm або Kubernetes). Для локальної розробки використовуйте --scale.
У development-середовищі часто потрібні утилітні сервіси для налагодження:
Але у production ці сервіси не потрібні (і навіть небезпечні). Як організувати docker-compose.yml, щоб можна було запускати різні набори сервісів?
Наївне рішення: Створити два файли — docker-compose.dev.yml та docker-compose.prod.yml. Це призводить до дублювання конфігурації.
Правильне рішення: Використовувати profiles.
Profiles дозволяють групувати сервіси та запускати лише потрібні групи:
services:
# Основні сервіси (без profile — запускаються завжди)
api:
image: myapp-api:latest
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
cache:
image: redis:7-alpine
# Утилітні сервіси (profile: tools)
adminer:
image: adminer:latest
ports:
- "8080:8080"
profiles:
- tools
depends_on:
- db
redis-commander:
image: rediscommander/redis-commander:latest
ports:
- "8081:8081"
environment:
- REDIS_HOSTS=local:cache:6379
profiles:
- tools
depends_on:
- cache
# Моніторинг (profile: monitoring)
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
profiles:
- monitoring
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
profiles:
- monitoring
depends_on:
- prometheus
Запуск різних конфігурацій:
# Лише основні сервіси (api, db, cache)
docker compose up
# Основні + утилітні
docker compose --profile tools up
# Основні + моніторинг
docker compose --profile monitoring up
# Основні + утилітні + моніторинг
docker compose --profile tools --profile monitoring up
# Або через змінну оточення
export COMPOSE_PROFILES=tools,monitoring
docker compose up
Перевірка активних profiles:
docker compose config --profiles
# tools
# monitoring
Для складніших сценаріїв комбінуйте profiles з override файлами:
docker-compose.yml (базова конфігурація):
services:
api:
image: myapp-api:latest
environment:
- ASPNETCORE_ENVIRONMENT=Production
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
docker-compose.override.yml (автоматично застосовується у dev):
services:
api:
build: ./backend
volumes:
- ./backend:/app
environment:
- ASPNETCORE_ENVIRONMENT=Development
command: dotnet watch run
db:
environment:
- POSTGRES_PASSWORD=dev_password # Перекриває secrets для dev
ports:
- "5432:5432" # Відкриває порт для локального доступу
adminer:
image: adminer:latest
ports:
- "8080:8080"
profiles:
- tools
Використання:
# Development (застосовує обидва файли)
docker compose up
# Production (лише базовий файл)
docker compose -f docker-compose.yml up
Docker Compose автоматично шукає файл docker-compose.override.yml та мержить його з docker-compose.yml. Це дозволяє тримати production-конфігурацію у базовому файлі, а development-специфічні налаштування — в override.
extendsУявіть, що у вас є кілька .NET API-сервісів з однаковою базовою конфігурацією:
services:
users-api:
image: myapp-users-api:latest
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
orders-api:
image: myapp-orders-api:latest
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
products-api:
image: myapp-products-api:latest
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
Проблема: Якщо потрібно змінити health check (наприклад, збільшити interval), доведеться редагувати три місця. Це порушує принцип DRY (Don't Repeat Yourself).
extendsСтворіть базовий шаблон у окремому файлі:
docker-compose.base.yml:
services:
dotnet-api-base:
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 30s
restart: unless-stopped
networks:
- backend-network
docker-compose.yml:
services:
users-api:
extends:
file: docker-compose.base.yml
service: dotnet-api-base
image: myapp-users-api:latest
environment:
- DATABASE_URL=postgresql://db:5432/users
orders-api:
extends:
file: docker-compose.base.yml
service: dotnet-api-base
image: myapp-orders-api:latest
environment:
- DATABASE_URL=postgresql://db:5432/orders
products-api:
extends:
file: docker-compose.base.yml
service: dotnet-api-base
image: myapp-products-api:latest
environment:
- DATABASE_URL=postgresql://db:5432/products
db:
image: postgres:16
networks:
- backend-network
networks:
backend-network:
Що відбувається:
dotnet-api-baseimage, додаткові environment) доповнюють або перекривають базовіdocker-compose.base.yml автоматично застосується до всіх сервісівПравила мержу:
environment, volumes) — об'єднуютьсяlabels, deploy.resources) — мержаться рекурсивноЯкщо ви не хочете створювати окремий файл, використовуйте YAML anchors:
x-dotnet-api-defaults: &dotnet-api-defaults
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
services:
users-api:
<<: *dotnet-api-defaults
image: myapp-users-api:latest
environment:
- DATABASE_URL=postgresql://db:5432/users
orders-api:
<<: *dotnet-api-defaults
image: myapp-orders-api:latest
environment:
- DATABASE_URL=postgresql://db:5432/orders
Розбір:
x-dotnet-api-defaults: — extension field (починається з x-), ігнорується Docker Compose, використовується лише для anchors&dotnet-api-defaults — створює anchor (посилання)<<: *dotnet-api-defaults — вставляє вміст anchor у сервісextends — для перевикористання між файлами (base + overrides)Тепер застосуємо всі вивчені концепції для створення реального застосунку з правильною архітектурою.
Архітектура:
Мережева топологія:
frontend-network — Nginx ↔ Frontend ↔ APIbackend-network — API ↔ PostgreSQL ↔ Redisdocker-compose.yml:
version: '3.9'
services:
# ============================================
# Frontend Layer
# ============================================
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
networks:
- frontend-network
environment:
- REACT_APP_API_URL=http://localhost/api
depends_on:
- api
# ============================================
# API Layer
# ============================================
api:
build:
context: ./backend
dockerfile: Dockerfile
networks:
- frontend-network
- backend-network
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
- ConnectionStrings__DefaultConnection=Host=db;Database=myapp;Username=postgres;Password=${DB_PASSWORD}
- Redis__ConnectionString=cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 30s
restart: unless-stopped
# ============================================
# Database Layer
# ============================================
db:
image: postgres:16-alpine
networks:
- backend-network
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
restart: unless-stopped
cache:
image: redis:7-alpine
networks:
- backend-network
volumes:
- redis-data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
restart: unless-stopped
# ============================================
# Reverse Proxy
# ============================================
nginx:
image: nginx:alpine
networks:
- frontend-network
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./static:/usr/share/nginx/html:ro
depends_on:
- frontend
- api
restart: unless-stopped
# ============================================
# Development Tools (profile: tools)
# ============================================
adminer:
image: adminer:latest
networks:
- backend-network
- frontend-network
ports:
- "8080:8080"
environment:
- ADMINER_DEFAULT_SERVER=db
profiles:
- tools
depends_on:
- db
# ============================================
# Networks
# ============================================
networks:
frontend-network:
driver: bridge
backend-network:
driver: bridge
# ============================================
# Volumes
# ============================================
volumes:
postgres-data:
driver: local
redis-data:
driver: local
.env файл:
# Database
DB_PASSWORD=super_secret_password_change_in_production
# Compose
COMPOSE_PROJECT_NAME=myapp
Запуск:
# Production (без dev tools)
docker compose up -d
# Development (з Adminer)
docker compose --profile tools up -d
# Перегляд логів
docker compose logs -f api
# Перевірка здоров'я
docker compose ps
Очікуваний вивід docker compose ps:
Мережева діаграма:
У цій статті ми розглянули просунуті можливості Docker Compose для організації складних multi-service застосунків:
Залежності між сервісами:
depends_on з умовами (service_healthy, service_started, service_completed_successfully)Мережева топологія:
Управління томами:
Масштабування:
--scale для запуску кількох екземплярівdeploy.resourcesProfiles:
Перевикористання конфігурацій:
extends для наслідування між файламиУ наступній статті ми застосуємо ці знання для створення повноцінного full-stack .NET застосунку з фронтендом, бекендом, базою даних та всіма необхідними сервісами — від розробки до production deployment.
Завдання 1.1: Створіть docker-compose.yml з трьома сервісами: Nginx, PostgreSQL, Redis. Налаштуйте depends_on так, щоб Nginx запускався останнім.
Завдання 1.2: Додайте health checks для PostgreSQL та Redis. Перевірте статус через docker compose ps.
Завдання 1.3: Створіть дві мережі: frontend та backend. Підключіть Nginx до frontend, PostgreSQL та Redis до backend.
Завдання 2.1: Розширте конфігурацію з Завдання 1.3: додайте сервіс api, який має доступ до обох мереж. Перевірте, що Nginx може з'єднатися з api, але не може з db.
Завдання 2.2: Створіть init-контейнер migrations, який виконує SQL-скрипт перед запуском api. Використайте condition: service_completed_successfully.
Завдання 2.3: Налаштуйте profiles: tools (Adminer, Redis Commander) та monitoring (Prometheus). Запустіть застосунок з різними комбінаціями profiles.
Завдання 3.1: Створіть повноцінний multi-service застосунок з ASP.NET Core API, PostgreSQL, Redis, Nginx. Організуйте структуру проєкту з окремими директоріями для кожного сервісу.
Завдання 3.2: Реалізуйте перевикористання конфігурацій через extends або YAML anchors для кількох API-сервісів з однаковими health checks.
Завдання 3.3: Створіть docker-compose.yml (production) та docker-compose.override.yml (development). У dev-версії додайте bind mounts для hot reload, відкрийте порти для прямого доступу до БД, додайте Adminer.