О насБлогКонтакты
DevOps17 апреля 2026 г. 7 мин 4

Docker Compose в production: 7 ошибок, которые мы совершили, и как их избежать

AunimedaAunimeda
📋 Содержание

Docker Compose в production: 7 ошибок, которые мы совершили, и как их избежать

Docker Compose — это не "только для разработки". На небольших и средних проектах это вполне рабочий production-инструмент. Мы деплоили несколько проектов на VPS с Compose и набили шишки, которые теперь можно описать конкретно, с примерами.

Статья — не теория. Это список реальных ошибок, каждая из которых стоила либо потерянных данных, либо часов даунтайма, либо звонка клиента в 2 ночи.


Ошибка 1: Bind mount вместо named volume — и потеря данных при реструктуризации

Самая дорогостоящая ошибка. Bind mount выглядит так:

# ❌ Опасно для production
services:
  postgres:
    image: postgres:16
    volumes:
      - ./data/postgres:/var/lib/postgresql/data

Проблема: ./data/postgres привязан к конкретному пути на хосте. Когда мы переносили проект на новый VPS и клонировали репозиторий в другую директорию — путь изменился. Данные остались на старом сервере. Да, был бэкап. Нет, он был двухдневной давности.

Named volume управляется Docker'ом и не зависит от пути:

# ✅ Правильно
services:
  postgres:
    image: postgres:16
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
    driver: local

Named volume живёт в /var/lib/docker/volumes/ — его не затронет переименование проектной директории. При docker compose down данные не удаляются. Только docker compose down -v удалит volumes (добавьте это в ваш чеклист вещей-которые-нельзя-запускать-в-production).


Ошибка 2: depends_on без healthcheck — и "база ещё не готова"

# ❌ depends_on не ждёт готовности сервиса
services:
  app:
    depends_on:
      - postgres

depends_on гарантирует только что контейнер запустился, но не что PostgreSQL принял соединения. Приложение стартует, пытается подключиться, получает ECONNREFUSED и крашится. Особенно больно при первом запуске когда Postgres инициализирует кластер.

# ✅ Правильно: depends_on с condition
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 10s
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  app:
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

start_period — время после старта контейнера в течение которого неудачные healthcheck'и не считаются провалом. Для Postgres это важно: первый запуск инициализирует кластер (~8-15 секунд).


Ошибка 3: Секреты в docker-compose.yml

# ❌ Никогда не делайте так
environment:
  DB_PASSWORD: my_super_secret_password_123
  JWT_SECRET: anotherSecret

Даже если репозиторий приватный — это плохая практика. .env файл + .gitignore:

# .env (в .gitignore!)
DB_NAME=myapp_prod
DB_USER=myapp
DB_PASSWORD=<генерируйте через: openssl rand -base64 32>
JWT_SECRET=<openssl rand -base64 64>
REDIS_PASSWORD=<openssl rand -base64 32>
# docker-compose.yml
services:
  postgres:
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}

Запускайте docker compose --env-file .env.production up -d — разные .env файлы для staging и production. Не храните .env.production в репозитории никогда. На сервере он лежит в /opt/myapp/.env.production с правами 600.


Ошибка 4: Образ Node.js на 1.2 ГБ — multi-stage build снижает до 180 МБ

Стандартный Dockerfile без оптимизации:

# ❌ Результат: ~1.2 ГБ
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]

Multi-stage build:

# ✅ Результат: ~180 МБ

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app

# Копируем только package файлы для кэширования слоёв
COPY package*.json ./
RUN npm ci --only=production=false

COPY . .
RUN npm run build

# Удаляем dev-зависимости
RUN npm prune --production

# Stage 2: Runtime
FROM node:20-alpine AS runtime
WORKDIR /app

# Только необходимые системные пакеты
RUN apk add --no-cache dumb-init

# Непривилегированный пользователь
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# Копируем только то что нужно для запуска
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

# dumb-init: правильная обработка сигналов и PID 1
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

Что происходит: в builder образе остаются TypeScript, devDependencies, исходники. В runtime — только скомпилированный JS и production dependencies. node:20-alpine (~130 МБ) вместо node:20 (~1 ГБ).

dumb-init решает проблему PID 1 в контейнере: Node.js не обрабатывает SIGTERM корректно как init-процесс. С dumb-init graceful shutdown работает.


Ошибка 5: Все сервисы в одной сети — нет изоляции

По умолчанию Docker Compose создаёт одну сеть, и все контейнеры могут достучаться друг до друга. Это нарушает принцип наименьших привилегий:

# ✅ Изоляция через несколько сетей
networks:
  frontend_net:     # nginx ↔ app
    driver: bridge
  backend_net:      # app ↔ postgres, app ↔ redis
    driver: bridge

services:
  nginx:
    networks:
      - frontend_net
    # nginx НЕ имеет прямого доступа к postgres/redis

  app:
    networks:
      - frontend_net
      - backend_net

  postgres:
    networks:
      - backend_net
    # postgres доступен только для app, не для nginx

  redis:
    networks:
      - backend_net

Теперь даже если атакующий взломал nginx-контейнер — он не видит postgres напрямую. Это не панацея, но это дополнительный слой защиты.


Ошибка 6: Rolling update без даунтайма

Стандартный docker compose up -d --build имеет короткий даунтайм: старый контейнер останавливается, новый стартует, nginx получает 502 пока приложение поднимается (~5-15 секунд).

Решение через nginx upstream с двумя инстансами:

# nginx/conf.d/app.conf
upstream app_backend {
  server app_blue:3000;
  server app_green:3000 backup;
  keepalive 32;
}

server {
  listen 80;
  server_name example.com;

  location / {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_connect_timeout 5s;
    proxy_next_upstream error timeout http_502 http_503;
    proxy_next_upstream_tries 2;
  }
}
services:
  app_blue:
    image: myapp:${VERSION:-latest}
    networks: [frontend_net, backend_net]
    
  app_green:
    image: myapp:${VERSION:-latest}
    networks: [frontend_net, backend_net]

Деплой-скрипт:

#!/bin/bash
NEW_VERSION=$1

# Собираем новый образ
VERSION=$NEW_VERSION docker compose build app_blue app_green

# Сначала поднимаем green (backup)
VERSION=$NEW_VERSION docker compose up -d --no-deps app_green
sleep 10

# Проверяем health
if curl -sf http://localhost:3001/health; then
  # Переключаем blue на новую версию
  VERSION=$NEW_VERSION docker compose up -d --no-deps app_blue
  echo "Deploy successful"
else
  echo "Health check failed, rolling back"
  docker compose up -d --no-deps app_green  # возвращаем старый
fi

Ошибка 7: Логи заполняют диск до 100%

По умолчанию Docker хранит логи без ограничений в /var/lib/docker/containers/*/. Мы несколько раз сталкивались с ситуацией когда диск забивается логами за 2-3 дня на активном проекте.

# Глобальные настройки логирования
x-logging: &default-logging
  driver: json-file
  options:
    max-size: "50m"
    max-file: "5"
    # compress: "true"  # для Docker >= 26

services:
  app:
    logging: *default-logging
  
  postgres:
    logging: *default-logging
  
  nginx:
    logging:
      driver: json-file
      options:
        max-size: "100m"  # nginx логи обычно больше
        max-file: "10"

max-size: "50m" + max-file: "5" = максимум 250 МБ на сервис. Для VPS с 50 ГБ диском это разумно.


Полный production docker-compose.yml

version: "3.9"

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "50m"
    max-file: "5"

networks:
  frontend_net:
    driver: bridge
  backend_net:
    driver: bridge

volumes:
  postgres_data:
  redis_data:
  nginx_certs:

services:

  # ─── NGINX ──────────────────────────────────────────────
  nginx:
    image: nginx:1.25-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - nginx_certs:/etc/letsencrypt:ro
    networks:
      - frontend_net
    depends_on:
      app:
        condition: service_healthy
    logging: *default-logging

  # ─── APP ────────────────────────────────────────────────
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: runtime
    restart: unless-stopped
    env_file: .env.production
    environment:
      NODE_ENV: production
      PORT: 3000
    networks:
      - frontend_net
      - backend_net
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s
    logging: *default-logging
    # Ограничение ресурсов
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 512M
        reservations:
          memory: 256M

  # ─── POSTGRES ───────────────────────────────────────────
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file: .env.production
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
    command: postgres -c config_file=/etc/postgresql/postgresql.conf
    networks:
      - backend_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 10s
    logging: *default-logging

  # ─── REDIS ──────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru --save 60 1000
    volumes:
      - redis_data:/data
    networks:
      - backend_net
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    logging: *default-logging

Docker Compose — отличный инструмент для production на небольших проектах если знать эти подводные камни. Kubernetes избыточен пока у вас меньше 10 сервисов и один VPS.

Если вам нужна помощь с настройкой production-инфраструктуры для вашего проекта в Кыргызстане — команда Aunimeda занимается именно такими задачами. Посмотрите наши услуги или напишите напрямую в WhatsApp.

Читайте также

Мониторинг Node.js в production: Prometheus, Grafana и OpenTelemetryaunimeda
DevOps

Мониторинг Node.js в production: Prometheus, Grafana и OpenTelemetry

Как понять что ваш Node.js сервер падает ещё до того как пользователи начали жаловаться. Настройка метрик с Prometheus, дашборды в Grafana, трейсинг с OpenTelemetry — полная конфигурация для production.

Хостинг для сайта в Кыргызстане: VPS, shared или облако - что выбрать в 2026aunimeda
DevOps

Хостинг для сайта в Кыргызстане: VPS, shared или облако - что выбрать в 2026

Разбираем типы хостинга для сайтов в Кыргызстане: shared, VPS, облачный и выделенный сервер. Реальные цены в сомах, рейтинг провайдеров и когда стоит апгрейдиться.

Docker и CI/CD для небольшой команды: что мы реально запускаем в продакшенеaunimeda
DevOps

Docker и CI/CD для небольшой команды: что мы реально запускаем в продакшене

Kubernetes - не для всех. Наш продакшен-стек для 6 проектов командой из 8 человек: Docker Compose, GitHub Actions, Nginx - без Kubernetes и без $800/месяц на AWS.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Кыргызстане. Бесплатная консультация.

Получить консультацию Все статьи