В современной Fullstack-разработке на Next.js 15 серверная часть приложения больше не является простым генератором HTML-страниц. Приложение App Router выступает в качестве высоконагруженного backend-узла, активно взаимодействующего со сложной инфраструктурой:
Попытка ручной установки и координации этих сервисов на локальных рабочих машинах разработчиков неизбежно приводит к проблеме расхождения окружений («works on my machine») и усложняет онбординг. Для решения этой задачи применяется спецификация Docker Compose (Compose Spec v2).
[Топология локального Fullstack-стека]
Хост-система разработчика / Браузер
(HTTP: localhost:3000, MinIO UI: localhost:9001)
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Docker Network: backend-tier (Изолированная мостовая сеть) │
│ │
│ ┌────────────────────┐ ┌─────────────────────────┐ │
│ │ web (Next.js) │ ─────────► │ postgres (PostgreSQL) │ │
│ │ │ TCP: 5432 │ Volume: postgres_data │ │
│ │ • Server Actions │ └─────────────────────────┘ │
│ │ • Route Handlers │ │
│ │ • RSC SSR │ ─────────► ┌─────────────────────────┐ │
│ │ │ TCP: 6379 │ redis (Redis 7) │ │
│ │ │ │ Volume: redis_data │ │
│ │ │ └─────────────────────────┘ │
│ │ │ │
│ │ │ ─────────► ┌─────────────────────────┐ │
│ │ │ S3: 9000 │ minio (MinIO S3) │ │
│ │ │ │ Volume: minio_data │ │
│ └────────────────────┘ └────────────▲────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ minio-init (Sidecar) │ │
│ │ Создание бакета uploads │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Главной инженерной задачей при оркестрации такого кластера является управление сетевой адресацией (Service Discovery) и порядком инициализации сервисов (Startup Ordering & Healthchecks).
Внутри мостовой сети (bridge network) Docker Compose запускает встроенный DNS-сервер (127.0.0.11), который автоматически сопоставляет имена сервисов (postgres, redis, minio) с их динамическими IP-адресами контейнеров. Это исключает жесткую привязку к локальным IP и портам хоста.
Ментальная модель Docker Compose базируется на концепции декларативного графа зависимостей с проверкой готовности (Health-Conditioned Dependency Graph).
Частой ошибкой начинающих разработчиков является предположение о том, что стандартная директива depends_on: ['postgres'] гарантирует готовность базы данных к приему запросов. В реальности Docker считает сервис «запущенным», как только ядро ОС создало процесс контейнера. В этот момент процесс PostgreSQL только начинает считывать конфигурационные файлы, проверять целостность табличных пространств и инициализировать сокет. Если Next.js стартует в эту секунду и запускает миграции Prisma или Drizzle, приложение падает с ошибкой ECONNREFUSED.
Для устранения состояния гонки (Race Condition) Compose использует связку двух директив:
healthcheck: низкоуровневая проверка жизнеспособности внутри контейнера сервиса (например, утилитой pg_isready для Postgres или redis-cli ping для Redis). Сервис получает статус healthy только тогда, когда демон БД готов обслуживать входящие TCP-соединения.depends_on с условием condition: service_healthy: блокирует старт зависимого сервиса web до тех пор, пока целевые инфраструктурные контейнеры не пройдут свои healthchecks.Запуск [docker compose up]
│
├─────────────────────────────────┬─────────────────────────────────┐
▼ ▼ ▼
[Postgres: Запуск демона] [Redis: Инициализация памяти] [MinIO: Старт S3 API]
│ │ │
[pg_isready: Отказ...] [redis-cli ping: PONG!] [curl /minio/health: 200]
│ │ │
[pg_isready: 200 OK!] ▼ ▼
│ Статус: HEALTHY Статус: HEALTHY
▼ │ │
Статус: HEALTHY │ │
│ │ │
└─────────────────────────────────┼─────────────────────────────────┘
│ Все условия выполнены
▼
[minio-init: Создание бакетов]
│ Успешно завершен
▼
[web (Next.js): Старт приложения]
• Выполнение миграций Prisma
• Подключение пулов к Postgres/Redis
• Прием клиентских HTTP-запросовВторая критическая концепция — двухуровневая адресация S3 (Dual-Endpoint Resolution). Поскольку MinIO используется и серверным кодом Next.js (внутри Docker-сети), и клиентским браузером (на хост-машине), конфигурация обязана разграничивать внутренние вызовы SDK (http://minio:9000) и внешние предподписанные ссылки для загрузки файлов через браузер (http://localhost:9000).
Создание файла .env с паролями для Postgres, токеном Redis и учетными данными MinIO.
Выполнение команды docker compose up -d. Демон поднимает мостовую сеть и монтирует тома.
Compose сдерживает старт Next.js до получения статуса healthy от pg_isready и redis-cli ping.
Контейнер minio-init создает бакет app-uploads и настраивает публичную политику доступа download.
Контейнер web поднимается на порту 3000, успешно подключаясь к готовым базам данных.
Почему простой директивы depends_on: [postgres] недостаточно для надежного старта веб-приложения и почему необходимо указывать condition: service_healthy?
Новый инженер в команде клонирует репозиторий, выполняет команду docker compose up -d и получает полностью сконфигурированную среду: базу данных со схемой, Redis-инстанс и предсозданные бакеты S3 без необходимости ручной установки Postgres или создания AWS-аккаунта.
В раннерах GitHub Actions или GitLab CI перед запуском сквозных тестов Playwright поднимается идентичный стек сервисов через Docker Compose. Тесты выполняются против реальной СУБД и реального хранилища S3, исключая неточности моков и эмуляторов.
Для корпоративных заказчиков с жесткими требованиями к локализации данных (отсутствие доступа к зарубежным облакам AWS/GCP) данный стек развертывается как самодостаточная производственная система на собственных серверах предприятия.
Построим продакшен-манифест compose.yaml, объединяющий все четыре сервиса с корректными сетевыми связями, постоянными томами (Volumes) и скриптом инициализации S3-бакетов.
Инициализация изолированной мостовой сети fullstack-net и выделение именованных томов.
Параллельный запуск Postgres, Redis и MinIO. Ожидание прохождения внутренних проверок работоспособности.
Запуск контейнера minio/mc, авторизация через API-ключи и создание публичного бакета app-uploads.
Приложение web ожидает готовности всех трех баз данных и запускает HTTP-сервер на порту 3000.
compose.yaml#Обратите внимание на использование современного стандарта Compose v2 (директива version: признана устаревшей):
name: agenticlab-fullstack
services:
# ── 1. СУБД POSTGRESQL 17 ──
postgres:
image: postgres:17-alpine
container_name: fullstack-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_dev_secret}
POSTGRES_DB: ${POSTGRES_DB:-agenticlab_db}
ports:
# Проброс порта на хост для подключения DBeaver / DataGrip
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- backend-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-agenticlab_db}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 5s
# ── 2. IN-MEMORY КЭШ REDIS 7 ──
redis:
image: redis:7.4-alpine
container_name: fullstack-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-redis_dev_secret}"]
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- backend-net
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis_dev_secret}", "ping"]
interval: 5s
timeout: 3s
retries: 5
start_period: 3s
# ── 3. ОБЪЕКТНОЕ ХРАНИЛИЩЕ MINIO S3 ──
minio:
image: minio/minio:RELEASE.2024-11-07T00-52-20Z
container_name: fullstack-minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${S3_ACCESS_KEY:-minio_admin}
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:-minio_secret_password}
ports:
- "9000:9000" # S3 REST API
- "9001:9001" # MinIO Web Console UI
volumes:
- minio_data:/data
networks:
- backend-net
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 5s
retries: 5
start_period: 5s
# ── 4. ИНИЦИАЛИЗАТОР БАКЕТОВ S3 (SIDECAR) ──
minio-init:
image: minio/mc:RELEASE.2024-11-05T11-41-28Z
container_name: fullstack-minio-init
depends_on:
minio:
condition: service_healthy
networks:
- backend-net
entrypoint: >
/bin/sh -c "
echo 'Подключение к MinIO...';
/usr/bin/mc alias set myminio http://minio:9000 ${S3_ACCESS_KEY:-minio_admin} ${S3_SECRET_KEY:-minio_secret_password};
echo 'Создание бакета app-uploads...';
/usr/bin/mc mb myminio/app-uploads --ignore-existing;
echo 'Установка публичной политики чтения на изображения...';
/usr/bin/mc anonymous set download myminio/app-uploads;
echo 'Инициализация MinIO завершена успешно.';
exit 0;
"
# ── 5. NEXT.JS 15 ПРИЛОЖЕНИЕ (WEB) ──
web:
build:
context: .
dockerfile: Dockerfile
target: runner
container_name: fullstack-web
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: 3000
# Внутренние DNS-имена внутри сети backend-net
DATABASE_URL: "postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres_dev_secret}@postgres:5432/${POSTGRES_DB:-agenticlab_db}?schema=public"
REDIS_URL: "redis://:${REDIS_PASSWORD:-redis_dev_secret}@redis:6379"
# S3 конфигурация для серверных обращений
S3_ENDPOINT: "http://minio:9000"
# S3 конфигурация для браузера (Presigned URLs)
NEXT_PUBLIC_S3_PUBLIC_URL: "http://localhost:9000"
S3_BUCKET: "app-uploads"
S3_ACCESS_KEY: ${S3_ACCESS_KEY:-minio_admin}
S3_SECRET_KEY: ${S3_SECRET_KEY:-minio_secret_password}
S3_REGION: "us-east-1"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio-init:
condition: service_completed_successfully
networks:
- backend-net
networks:
backend-net:
driver: bridge
volumes:
postgres_data:
name: fullstack_postgres_data
redis_data:
name: fullstack_redis_data
minio_data:
name: fullstack_minio_dataДля работы с MinIO используется официальный @aws-sdk/client-s3. Серверный код использует внутренний адрес http://minio:9000, но сгенерированные предподписанные URL должны указывать на http://localhost:9000, чтобы браузер пользователя мог выполнить прямую загрузку (Direct Upload):
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
// Серверный клиент для внутренних сетевых вызовов (Next.js -> MinIO)
export const s3ServerClient = new S3Client({
region: process.env.S3_REGION || 'us-east-1',
endpoint: process.env.S3_ENDPOINT || 'http://minio:9000',
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY || '',
secretAccessKey: process.env.S3_SECRET_KEY || '',
},
// Критично для MinIO: путь к бакету передается в path (http://host/bucket), а не в sub-domain
forcePathStyle: true,
});
/**
* Генерация предподписанной ссылки для безопасной прямой загрузки файла из браузера
*/
export async function createPresignedUploadUrl(
fileKey: string,
contentType: string,
expiresInSeconds = 300
): Promise<{ uploadUrl: string; finalPublicUrl: string }> {
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET || 'app-uploads',
Key: fileKey,
ContentType: contentType,
});
// Получаем базовый Presigned URL
const rawUrl = await getSignedUrl(s3ServerClient, command, {
expiresIn: expiresInSeconds,
});
// Заменяем внутренний адрес minio:9000 на доступный браузеру NEXT_PUBLIC_S3_PUBLIC_URL
const publicOrigin = process.env.NEXT_PUBLIC_S3_PUBLIC_URL || 'http://localhost:9000';
const internalOrigin = process.env.S3_ENDPOINT || 'http://minio:9000';
const uploadUrl = rawUrl.replace(internalOrigin, publicOrigin);
const finalPublicUrl = `${publicOrigin}/${process.env.S3_BUCKET}/${fileKey}`;
return { uploadUrl, finalPublicUrl };
}С какой проблемой сталкивается клиентский браузер при загрузке файлов по Presigned URL, сгенерированному AWS SDK внутри контейнера Next.js с эндпоинтом http://minio:9000?
Разработчик описывает в compose.yaml зависимость depends_on: [postgres]. При запуске docker compose up контейнер web сразу падает с ошибкой PrismaClientInitializationError: Can't reach database server at postgres:5432.
Почему так делают: разработчики считают, что depends_on ждет полной инициализации СУБД.
Что происходит в продакшене: сервис стартует до того, как демон Postgres готов принимать TCP-соединения.
# ❌ ОШИБКА: Проверяется только создание контейнера, но не его готовность
depends_on:
- postgres
# ✅ ПРАВИЛЬНО: Ожидание успешного прохождения healthcheck
depends_on:
postgres:
condition: service_healthyБраузер отправляет файл на полученный от Server Action URL: PUT http://minio:9000/app-uploads/photo.jpg и падает с ошибкой net::ERR_NAME_NOT_RESOLVED.
Почему так делают: SDK AWS подставляет значение endpoint, переданное в конструктор S3Client, без учета различий между внутренней сетью Docker и сетью хоста.
Что происходит в продакшене: клиентский браузер находится за пределами Docker-сети и ничего не знает о DNS-имени minio. Ссылка должна вести на localhost:9000 или внешний домен.
// ❌ ОШИБКА: Ссылка указывает на внутренний хост Docker
const uploadUrl = await getSignedUrl(s3Client, command); // вернет http://minio:9000/...
// ✅ ПРАВИЛЬНО: Подмена внутреннего хоста на внешний публичный эндпоинт
const uploadUrl = rawUrl.replace(process.env.S3_ENDPOINT, process.env.NEXT_PUBLIC_S3_PUBLIC_URL);Запросы к MinIO падают с сетевой ошибкой ENOTFOUND app-uploads.minio.
Почему так делают: по умолчанию AWS SDK использует virtual-hosted style адресацию (http://bucket-name.s3.amazonaws.com).
Что происходит в продакшене: SDK пытается сделать DNS-запрос к поддомену app-uploads.minio, который не существует во внутреннем DNS Docker. MinIO требует path-style адресации (http://minio:9000/app-uploads).
// ❌ ОШИБКА: По умолчанию forcePathStyle = false
const client = new S3Client({ endpoint: 'http://minio:9000' });
// ✅ ПРАВИЛЬНО: Принудительный path-style
const client = new S3Client({
endpoint: 'http://minio:9000',
forcePathStyle: true,
});Вместо именованного тома postgres_data:/var/lib/postgresql/data используется монтирование локальной папки ./data/postgres:/var/lib/postgresql/data.
Почему так делают: хотят видеть файлы базы данных прямо в проводнике или IDE.
Что происходит в продакшене: критическое падение производительности дискового I/O из-за трансляции файловых систем WSL2/OSXFUSE, а также сбои прав доступа (Permissions) и повреждение заголовков страниц PostgreSQL WAL.
# ❌ АНТИПАТТЕРН: Host bind mount для СУБД
volumes:
- ./local-db-data:/var/lib/postgresql/data
# ✅ ПРАВИЛЬНО: Именованный том Docker с нативной Linux-производительностью
volumes:
- postgres_data:/var/lib/postgresql/data