Docker Compose — стандарт де-факто для локальной разработки. Но большинство команд останавливаются на базовом docker-compose.yml, не используя и половины возможностей инструмента. В этой статье построим полноценный production-like стек и разберём тонкости, которые экономят часы отладки.
Полный многосервисный стек
Стартовая конфигурация для типичного веб-приложения: API + PostgreSQL + Redis + воркер задач.
# docker-compose.yml — базовая конфигурация (общая для dev и prod)
services:
api:
build:
context: .
dockerfile: Dockerfile
target: development # multi-stage build
ports:
- "3000:3000"
env_file:
- .env
environment:
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy # ждём готовности, не просто старта
redis:
condition: service_healthy
networks:
- backend
worker:
build:
context: .
dockerfile: Dockerfile
target: development
command: node dist/worker.js
env_file:
- .env
environment:
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-myapp}
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d:ro # init-скрипты
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d ${POSTGRES_DB:-myapp}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
networks:
- backend
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD:-}", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
backend:
driver: bridge
Override-файлы: dev vs production
Разделение конфигурации через override-файлы — ключевой паттерн. Базовый docker-compose.yml содержит общее, а переопределения — среду-специфичное.
# docker-compose.override.yml — автоматически применяется в dev
# docker compose up = docker compose -f docker-compose.yml -f docker-compose.override.yml up
services:
api:
volumes:
- .:/app # монтируем исходный код для hot reload
- /app/node_modules # исключаем node_modules из монтирования
command: npm run dev # переопределяем команду
environment:
NODE_ENV: development
LOG_LEVEL: debug
worker:
volumes:
- .:/app
- /app/node_modules
command: npm run worker:dev
# В dev добавляем pgAdmin для удобства
pgadmin:
image: dpage/pgadmin4:latest
environment:
PGADMIN_DEFAULT_EMAIL: admin@local.dev
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "5050:80"
depends_on:
- postgres
networks:
- backend
# Redis Insight для мониторинга Redis
redis-insight:
image: redis/redisinsight:latest
ports:
- "5540:5540"
networks:
- backend
# docker-compose.prod.yml — для production-like тестирования
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up
services:
api:
build:
target: production # финальный stage без dev-зависимостей
restart: unless-stopped
deploy:
replicas: 2
resources:
limits:
cpus: "0.5"
memory: 512M
environment:
NODE_ENV: production
LOG_LEVEL: info
worker:
build:
target: production
restart: unless-stopped
postgres:
restart: unless-stopped
redis:
restart: unless-stopped
Именованные volumes: не теряем данные
Анонимные volumes (- /var/lib/postgresql/data) привязаны к контейнеру и удаляются вместе с ним. Именованные volumes существуют независимо.
# Полезные команды для работы с volumes
docker volume ls # список volumes
docker volume inspect myapp_postgres_data # детали volume
docker volume rm myapp_postgres_data # удалить (осторожно!)
# Бэкап volume
docker run --rm \
-v myapp_postgres_data:/data \
-v $(pwd)/backup:/backup \
alpine tar czf /backup/postgres-backup-$(date +%Y%m%d).tar.gz -C /data .
# Восстановление
docker run --rm \
-v myapp_postgres_data:/data \
-v $(pwd)/backup:/backup \
alpine tar xzf /backup/postgres-backup-20260819.tar.gz -C /data
Healthchecks: правильное ожидание готовности
depends_on без condition: service_healthy только гарантирует старт контейнера, не его готовность. Postgres может запускаться 10–30 секунд — за это время API упадёт с ошибкой подключения.
# Продвинутый healthcheck для API (проверяем /health endpoint)
services:
api:
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s # даём время на запуск прежде чем считать неудачи
nginx:
depends_on:
api:
condition: service_healthy # nginx стартует только после готовности API
// src/routes/health.ts — минимальный healthcheck endpoint
app.get("/health", async (req, reply) => {
try {
// Проверяем критичные зависимости
await db.$queryRaw`SELECT 1`;
await redis.ping();
reply.send({
status: "ok",
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
} catch (err) {
reply.status(503).send({
status: "unhealthy",
error: (err as Error).message
});
}
});
Управление секретами
Вариант 1: .env файлы (dev-окружение)
# .env (не коммитим в git!)
POSTGRES_PASSWORD=dev_password_only
POSTGRES_DB=myapp_dev
REDIS_PASSWORD=
JWT_SECRET=dev-jwt-secret-replace-in-prod
# .env.example (коммитим, без реальных значений)
POSTGRES_PASSWORD=
POSTGRES_DB=myapp
REDIS_PASSWORD=
JWT_SECRET=
Вариант 2: Docker Secrets (production)
# docker-compose.prod.yml с секретами
services:
api:
secrets:
- db_password
- jwt_secret
environment:
# Приложение читает из /run/secrets/<name>
DB_PASSWORD_FILE: /run/secrets/db_password
JWT_SECRET_FILE: /run/secrets/jwt_secret
secrets:
db_password:
file: ./secrets/db_password.txt # файл с паролем, не в git
jwt_secret:
external: true # из Docker Swarm secrets store
// Чтение секретов из файлов (Docker Secrets паттерн)
import { readFileSync } from "fs";
function readSecret(name: string, envKey: string): string {
const filePath = process.env[`${envKey}_FILE`];
if (filePath) {
return readFileSync(filePath, "utf-8").trim();
}
const value = process.env[envKey];
if (!value) throw new Error(`Secret ${envKey} not configured`);
return value;
}
const dbPassword = readSecret("db_password", "DB_PASSWORD");
const jwtSecret = readSecret("jwt_secret", "JWT_SECRET");
Типичные ловушки
Проблема с правами файлов
Контейнер пишет файлы от имени root, хост не может их удалить.
# Решение: указываем пользователя
services:
api:
user: "1000:1000" # uid:gid, совпадающие с пользователем хоста
# В Dockerfile: создаём non-root пользователя
FROM node:22-alpine AS development
RUN addgroup -g 1000 appgroup && adduser -u 1000 -G appgroup -S appuser
USER appuser
WORKDIR /app
DNS-проблемы между сервисами
Сервисы обращаются друг к другу по имени сервиса, а не по localhost.
// Неправильно (внутри контейнера)
const DATABASE_URL = "postgresql://postgres@localhost:5432/myapp";
// Правильно
const DATABASE_URL = "postgresql://postgres@postgres:5432/myapp";
// ^^^^^^^^ имя сервиса в compose
Долгий rebuild при изменении зависимостей
# Плохо: COPY . . — любое изменение инвалидирует кеш node_modules
COPY . .
RUN npm install
# Хорошо: сначала только package.json, install, потом остальное
COPY package*.json ./
RUN npm ci --only=production
COPY . .
Потеря логов при рестарте
services:
api:
logging:
driver: "json-file"
options:
max-size: "100m" # ротация логов
max-file: "5" # 5 файлов максимум
Полезные команды
# Запуск только нужных сервисов
docker compose up postgres redis -d
# Rebuild конкретного сервиса
docker compose build --no-cache api
# Выполнить команду внутри запущенного контейнера
docker compose exec postgres psql -U postgres myapp
# Просмотр логов нескольких сервисов
docker compose logs -f api worker
# Статус healthchecks
docker compose ps
# Полная очистка (volumes тоже!)
docker compose down -v --remove-orphans
# Посмотреть финальный конфиг (с override)
docker compose config
Итог
Docker Compose с override-файлами, healthchecks и named volumes — это не «просто локальная разработка», а полноценный production-like стек. Правильная структура экономит часы при отладке проблем «у меня работает, на сервере нет».
Следующий шаг — настройка CI/CD пайплайна поверх этой конфигурации: смотрите GitLab CI/CD pipeline. Про оптимизацию самих Dockerfile — Docker: оптимизация образов.

