Перейти к содержимому
devopsСредний120 минут

Docker и Docker Compose для разработчиков

Полное практическое руководство по Docker и Docker Compose — от основ контейнеризации до production-ready конфигураций с углубленной безопасностью и troubleshooting

#docker#docker-compose#контейнеризация#devops#deployment#микросервисы#разработка

Docker и Docker Compose для разработчиков

Docker решает проблему «у меня работает, а у тебя нет». Если вы устали от фразы «works on my machine» — этот материал для вас.

Как пользоваться материалом

  • Спешишь? Читай введение, раздел про Dockerfile и Docker Compose basics.
  • Разрабатываешь приложение? Сосредоточься на примерах с веб-приложениями и базами данных.
  • Готовишься к деплою? Изучи разделы про multi-stage builds, volumes и production best practices.
  • Работаешь с микросервисами? Смотри раздел про networks и оркестрацию нескольких сервисов.
  • Не знаешь терминов? Начни с глоссария ниже.

Глоссарий

Container (Контейнер) — Изолированная среда выполнения приложения со всеми зависимостями. Легковесная альтернатива виртуальной машине.

Image (Образ) — Шаблон для создания контейнеров. Содержит ОС, код приложения, библиотеки и конфигурацию.

Dockerfile — Текстовый файл с инструкциями для создания Docker образа.

Docker Compose — Инструмент для управления multi-container приложениями через YAML-конфигурацию.

Volume (Том) — Механизм для постоянного хранения данных вне контейнера.

Network (Сеть) — Виртуальная сеть для связи между контейнерами.

Registry (Реестр) — Хранилище Docker образов (Docker Hub, GitHub Container Registry, частные реестры).

Layer (Слой) — Каждая инструкция в Dockerfile создает слой. Слои кешируются для ускорения сборки.

Build Context — Директория, содержимое которой отправляется Docker daemon при сборке образа.

Container Orchestration (Оркестрация) — Автоматизация развертывания, масштабирования и управления контейнерами (Docker Swarm, Kubernetes).

Введение: Зачем нужен Docker?

Проблема без Docker

Разработчик: "У меня работает!"
Тестировщик: "У меня не запускается, ошибка с Python 3.9"
DevOps: "На сервере вообще другая версия PostgreSQL"
Клиент: "А у меня macOS, ничего не работает"
 
Часы потрачены на отладку окружения вместо разработки...

Решение с Docker

Разработчик: "Вот Dockerfile и docker-compose.yml"
Тестировщик: docker compose up всё работает
DevOps: docker compose up всё работает
Клиент: docker compose up всё работает
 
Все используют идентичное окружение!

Когда использовать Docker

✅ Используйте Docker:

  • Веб-приложения с зависимостями (Node.js, Python, Ruby)
  • Микросервисы
  • CI/CD pipelines
  • Разработка в команде (единое окружение)
  • Продакшн деплой
  • Локальное поднятие инфраструктуры (БД, Redis, Elasticsearch)

❌ НЕ обязательно использовать Docker:

  • Простые CLI-скрипты
  • Desktop GUI приложения
  • Когда команда не готова к обучению
  • Проекты с экстремально низкими требованиями к ресурсам

Docker в контексте других инструментов

Docker — не единственное решение для контейнеризации. Понимание альтернатив поможет выбрать правильный инструмент для вашего проекта.

Сравнение инструментов контейнеризации:

КритерийDockerDocker ComposeDocker SwarmKubernetesPodman
СложностьНизкаяНизкаяСредняяВысокаяНизкая
Кривая обучения1-2 дня1 неделя2-4 недели2-6 месяцев1-2 дня
Локальная разработка✅✅⚠️
Production (малый проект)⚠️⚠️⚠️ (overkill)
Production (enterprise)⚠️✅✅⚠️
Автомасштабирование✅✅
Self-healing✅✅
Multi-host кластер⚠️
Управление секретами⚠️✅✅⚠️
Мониторинг out-of-box⚠️
Сообщество✅✅✅✅✅✅✅

Когда использовать каждый инструмент:

Docker (одиночные контейнеры):

  • Локальное тестирование контейнеров
  • CI/CD pipelines (сборка образов)
  • Быстрые эксперименты
  • Обучение основам контейнеризации

Docker Compose (этот материал!):

  • Локальная разработка — идеальный выбор
  • Простые production проекты (1 сервер)
  • Staging окружения
  • Быстрый прототип микросервисной архитектуры

Docker Swarm:

  • Prod проекты с 3-10 серверами
  • Когда Kubernetes избыточен
  • Нужна простая оркестрация
  • Миграция с Docker Compose (совместимый синтаксис)

Kubernetes:

  • Enterprise production — стандарт индустрии
  • Проекты с > 10 серверов
  • Сложная микросервисная архитектура
  • Нужны advanced features (auto-scaling, service mesh)
  • Мультиоблачность (AWS + GCP + on-premise)

Podman:

  • Rootless контейнеры (безопасность)
  • Замена Docker без daemon
  • Совместимость с docker командами
  • Compliance требования (no daemon)

Эволюция проекта:

Стадия 1: MVP на ноутбуке
└─ Docker Compose ← Вы здесь после прочтения этого материала

Стадия 2: Первые пользователи (1 сервер)
└─ Docker Compose в production

Стадия 3: Растущий бизнес (2-5 серверов)
└─ Docker Swarm или managed Kubernetes (EKS, GKE)

Стадия 4: Scale-up (10+ серверов)
└─ Kubernetes с полным monitoring stack

Для 90% разработчиков Docker Compose — идеальный баланс между простотой и функциональностью. Начните с него, а Kubernetes изучите когда действительно понадобится.

Основы Docker

Главный принцип Docker: один контейнер = одна зона ответственности (один сервис). Не запускайте в одном контейнере и веб-сервер, и базу данных, и очередь — это антипаттерн!

Почему "один контейнер = один процесс"?

Этот принцип часто неправильно понимают. Точнее: один контейнер = одна служба (сервис), а не буквально один process ID.

6 причин следовать этому принципу:

1. Изоляция отказов

# ❌ ПЛОХО: Всё в одном контейнере
services:
  monolith:
    # Nginx + App + PostgreSQL + Redis
    # Если упадёт PostgreSQL → упадёт ВСЁ! 💀
 
# ✅ ХОРОШО: Изолированные сервисы
services:
  nginx:
    image: nginx
  app:
    image: my-app
  db:
    image: postgres
  redis:
    image: redis
# Если упадёт Redis → остальное работает ✅

2. Независимое масштабирование

# ❌ Монолит: невозможно масштабировать только app
docker compose up --scale monolith=3
# 3 копии всего (3 БД конфликтуют!)
 
# ✅ Микросервисы: масштабируем что нужно
docker compose up --scale app=10 --scale celery=5
# 10 app, 5 workers, 1 БД

3. Простая отладка

# ❌ Монолит: все логи вместе
docker logs monolith  # Nginx + App + DB + Redis в куче
 
# ✅ Микросервисы: логи по сервисам
docker logs app    # Только приложение
docker logs db     # Только БД

4. Zero-downtime deployments

# ❌ Монолит: БД упадёт при обновлении app
docker stop monolith && docker start monolith-v2
 
# ✅ Микросервисы: БД работает при обновлении app
docker compose up -d app-v2  # Только app перезапустился

5. Переиспользование образов

# ❌ Монолит: 2GB кастомный образ
FROM ubuntu
RUN apt-get install nginx postgresql redis python
 
# ✅ Микросервисы: официальные образы
FROM postgres:16-alpine  # 50MB, переиспользуется
FROM redis:7-alpine      # 30MB, переиспользуется
FROM python:3.11-slim    # 150MB, переиспользуется

6. Разные ресурсы для разных задач

# PostgreSQL: много RAM, persistent volumes
# Nginx: минимум ресурсов
# App: горизонтальное масштабирование
 
# В одном контейнере настроить это невозможно!

Исключения из правила (когда несколько процессов — OK):

# ✅ Nginx + PHP-FPM (тесно связанные процессы одного сервиса)
FROM php:8.2-fpm
RUN apt-get install nginx supervisor
 
# ✅ App + Log shipper (sidecar паттерн)
# Main: приложение, Sidecar: fluentd
 
# ✅ App + Tini (init для zombie процессов)
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["python", "app.py"]

Итого: "Один контейнер = один процесс" на самом деле означает "один контейнер = одна зона ответственности". Это даёт изоляцию отказов, масштабируемость, простоту отладки и возможность orchestration. Не следовать этому — значит потерять все преимущества контейнеризации!

Архитектура Docker

┌─────────────────────────────────────────┐
│         Docker CLI (команды)            │
│    docker build, run, push, pull...     │
└──────────────┬──────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────┐
│         Docker Daemon (движок)          │
│   Управляет контейнерами, образами      │
└──────────────┬──────────────────────────┘
               │
     ┌─────────┴──────────┬───────────┐
     ▼                    ▼           ▼
┌──────────┐      ┌──────────┐  ┌──────────┐
│Container1│      │Container2│  │Container3│
│  (nginx) │      │(postgres)│  │  (app)   │
└──────────┘      └──────────┘  └──────────┘

Установка Docker

macOS / Windows:

# Скачайте Docker Desktop с официального сайта
https://www.docker.com/products/docker-desktop
 
# После установки проверьте
docker --version
docker compose version  # Compose встроен в Docker CLI

Linux (Ubuntu/Debian):

# Удалите старые версии
sudo apt-get remove docker docker-engine docker.io containerd runc
 
# Установите зависимости
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release
 
# Добавьте официальный GPG ключ Docker
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
 
# Добавьте репозиторий
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
 
# Установите Docker
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
 
# Проверьте установку
docker --version
docker compose version  # Обратите внимание: compose без дефиса

Основные команды Docker

# ========================================
# Работа с образами
# ========================================
 
# Скачать образ из Docker Hub
docker pull nginx:latest
 
# Список локальных образов
docker images
 
# Удалить образ
docker rmi nginx:latest
 
# Собрать образ из Dockerfile
docker build -t my-app:1.0 .
 
# ========================================
# Работа с контейнерами
# ========================================
 
# Запустить контейнер
docker run -d --name my-nginx -p 80:80 nginx
 
# Опции:
# -d              запустить в фоне (detached)
# --name          имя контейнера
# -p 80:80        проброс портов host:container
# -v /path:/path  проброс volumes
# --rm            удалить контейнер после остановки
# -e KEY=VALUE    переменные окружения
 
# Список запущенных контейнеров
docker ps
 
# Список всех контейнеров (включая остановленные)
docker ps -a
 
# Остановить контейнер
docker stop my-nginx
 
# Запустить остановленный контейнер
docker start my-nginx
 
# Перезапустить контейнер
docker restart my-nginx
 
# Удалить контейнер
docker rm my-nginx
 
# Удалить все остановленные контейнеры
docker container prune
 
# ========================================
# Логи и отладка
# ========================================
 
# Посмотреть логи контейнера
docker logs my-nginx
 
# Следить за логами в реальном времени
docker logs -f my-nginx
 
# Войти в shell контейнера
docker exec -it my-nginx bash
 
# Посмотреть процессы в контейнере
docker top my-nginx
 
# Статистика использования ресурсов
docker stats
 
# Инспектировать контейнер (подробная информация)
docker inspect my-nginx
 
# ========================================
# Очистка системы
# ========================================
 
# Удалить неиспользуемые образы, контейнеры, сети
docker system prune
 
# Удалить вообще всё неиспользуемое (включая volumes)
docker system prune -a --volumes

Dockerfile: Создание образов

Dockerfile — это рецепт для создания Docker образа. Каждая инструкция создает новый слой, который кешируется. Правильный порядок инструкций ускоряет сборку в разы.

Структура Dockerfile

# ========================================
# Базовый образ
# ========================================
FROM python:3.11-slim
 
# Метаданные
LABEL maintainer="your-email@example.com"
LABEL version="1.0"
LABEL description="My Python application"
 
# ========================================
# Переменные окружения
# ========================================
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1
 
# ========================================
# Рабочая директория
# ========================================
WORKDIR /app
 
# ========================================
# Установка системных зависимостей
# ========================================
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        libpq-dev && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
 
# ========================================
# Копирование и установка Python зависимостей
# ========================================
# Сначала копируем только requirements (для кеширования)
COPY requirements.txt .
 
RUN pip install --no-cache-dir -r requirements.txt
 
# ========================================
# Копирование кода приложения
# ========================================
COPY . .
 
# ========================================
# Создание пользователя (безопасность)
# ========================================
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app
 
USER appuser
 
# ========================================
# Проброс порта
# ========================================
EXPOSE 8000
 
# ========================================
# Команда запуска
# ========================================
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

Инструкции Dockerfile

FROM — Базовый образ

# Официальные образы
FROM python:3.11-slim         # Python (минимальный)
FROM node:20-alpine           # Node.js (Alpine Linux — самый легкий)
FROM nginx:alpine             # Nginx
FROM postgres:16              # PostgreSQL
FROM redis:7-alpine           # Redis
 
# Выбор версии ОС
FROM ubuntu:22.04             # Ubuntu
FROM debian:bookworm-slim     # Debian (минимальный)
FROM alpine:3.18              # Alpine (самый легкий, ~5MB)
 
# Multi-stage builds (базовый этап)
FROM golang:1.21 AS builder

WORKDIR — Рабочая директория

WORKDIR /app
 
# Все последующие команды (RUN, COPY, CMD) будут выполняться из /app
# Если директория не существует, она будет создана

COPY vs ADD

# COPY — простое копирование файлов (рекомендуется)
COPY requirements.txt .
COPY src/ /app/src/
COPY . .
 
# ADD — копирование + распаковка tar.gz + скачивание URL (избегайте)
ADD archive.tar.gz /app/     # Автоматически распакует
ADD https://example.com/file.txt /app/  # Скачает файл

Используйте COPY вместо ADD. ADD имеет скрытую логику (распаковка, скачивание), что делает Dockerfile менее предсказуемым.

RUN — Выполнение команд при сборке

# ❌ Плохо: каждый RUN = новый слой
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
 
# ✅ Хорошо: объединяйте команды через &&
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
 
# Форматирование для читаемости
RUN apt-get update && \
    apt-get install -y \
        build-essential \
        libpq-dev \
        libssl-dev && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

ENV — Переменные окружения

# Один вариант на строку
ENV NODE_ENV=production
ENV PORT=3000
 
# Или несколько в одной инструкции
ENV NODE_ENV=production \
    PORT=3000 \
    API_URL=https://api.example.com

EXPOSE — Документирование портов

EXPOSE 8000
 
# Не открывает порт! Только документирует.
# Реальный проброс делается через docker run -p 8000:8000

CMD vs ENTRYPOINT

# CMD — команда по умолчанию (можно переопределить)
CMD ["python", "app.py"]
# docker run my-image           → запустит python app.py
# docker run my-image bash      → переопределит, запустит bash
 
# ENTRYPOINT — фиксированная команда
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run my-image           → python app.py
# docker run my-image test.py   → python test.py (только аргументы меняются)
 
# Полная форма
ENTRYPOINT ["python", "app.py"]
# docker run my-image --debug   → python app.py --debug

Пример: Dockerfile для Python FastAPI приложения

# ========================================
# Базовый образ
# ========================================
FROM python:3.11-slim AS base
 
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1
 
WORKDIR /app
 
# ========================================
# Этап 1: Зависимости для сборки
# ========================================
FROM base AS builder
 
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        libpq-dev && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
 
COPY requirements.txt .
 
RUN pip install --user --no-cache-dir -r requirements.txt
 
# ========================================
# Этап 2: Production образ
# ========================================
FROM base AS production
 
# Копируем установленные пакеты из builder
COPY --from=builder /root/.local /root/.local
 
# Обновляем PATH
ENV PATH=/root/.local/bin:$PATH
 
# Копируем код приложения
COPY . .
 
# Создаем непривилегированного пользователя
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app
 
USER appuser
 
EXPOSE 8000
 
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD python -c "import requests; requests.get('http://localhost:8000/health')"
 
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Пример: Dockerfile для Node.js приложения

# ========================================
# Этап 1: Сборка
# ========================================
FROM node:20-alpine AS builder
 
WORKDIR /app
 
# Копируем только package*.json для кеширования
COPY package*.json ./
 
# Устанавливаем зависимости
RUN npm ci --only=production
 
# Копируем исходный код
COPY . .
 
# Если есть build step (например, TypeScript)
# RUN npm run build
 
# ========================================
# Этап 2: Production образ
# ========================================
FROM node:20-alpine AS production
 
WORKDIR /app
 
# Копируем node_modules из builder
COPY --from=builder /app/node_modules ./node_modules
 
# Копируем собранное приложение
COPY --from=builder /app .
 
# Создаем пользователя
RUN addgroup -g 1000 appuser && \
    adduser -D -u 1000 -G appuser appuser && \
    chown -R appuser:appuser /app
 
USER appuser
 
EXPOSE 3000
 
ENV NODE_ENV=production
 
CMD ["node", "server.js"]

.dockerignore — Ускорение сборки

# .dockerignore — аналог .gitignore для Docker
 
# Git
.git
.gitignore
 
# Зависимости (устанавливаются внутри контейнера)
node_modules
__pycache__
*.pyc
venv
env
 
# IDE
.vscode
.idea
*.swp
 
# Тесты (не нужны в production)
tests/
*.test.js
*.spec.js
 
# Документация
README.md
docs/
 
# CI/CD
.github
.gitlab-ci.yml
 
# Docker
Dockerfile
docker-compose.yml
.dockerignore
 
# Логи и временные файлы
*.log
tmp/
temp/
*.tmp
 
# OS
.DS_Store
Thumbs.db

Оптимизация Dockerfile

Правильный порядок инструкций — ключ к быстрой сборке. Docker кеширует слои, поэтому часто меняющиеся файлы должны копироваться в конце.

# ❌ ПЛОХО: Код приложения копируется ДО установки зависимостей
FROM python:3.11-slim
WORKDIR /app
COPY . .                          # ← При любом изменении кода весь кеш сбрасывается
RUN pip install -r requirements.txt  # ← Пересборка зависимостей при каждом изменении!
CMD ["python", "app.py"]
 
# ✅ ХОРОШО: Сначала зависимости, потом код
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .           # ← Кеш сбрасывается только при изменении requirements.txt
RUN pip install -r requirements.txt  # ← Используется кеш если requirements.txt не менялся
COPY . .                          # ← Меняется часто, но зависимости уже в кеше
CMD ["python", "app.py"]
 
# Время сборки:
# Плохой вариант:  60 секунд при каждом изменении
# Хороший вариант: 60 секунд первый раз, потом 2-3 секунды

Docker Compose: Оркестрация multi-container приложений

Docker Compose превращает набор docker run команд в один понятный YAML-файл. Идеально для локальной разработки и простых деплоев.

Важно: С 2023 года Docker Compose встроен в Docker CLI как подкоманда. Используйте docker compose (через пробел) вместо устаревшего docker-compose (через дефис). Файл по-прежнему называется docker-compose.yml, а поле version: больше не требуется.

Структура docker-compose.yml

# docker-compose.yml
# Примечание: поле version устарело и больше не требуется
 
services:
  # ========================================
  # Веб-приложение
  # ========================================
  web:
    build:
      context: .
      dockerfile: Dockerfile
    image: my-app:latest
    container_name: my-app-web
    restart: unless-stopped
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - REDIS_URL=redis://redis:6379/0
    env_file:
      - .env
    volumes:
      - ./app:/app
      - static_volume:/app/static
    depends_on:
      - db
      - redis
    networks:
      - app-network
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
 
  # ========================================
  # База данных PostgreSQL
  # ========================================
  db:
    image: postgres:16-alpine
    container_name: my-app-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  # ========================================
  # Redis
  # ========================================
  redis:
    image: redis:7-alpine
    container_name: my-app-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - app-network
    command: redis-server --appendonly yes
 
# ========================================
# Volumes (постоянное хранение данных)
# ========================================
volumes:
  postgres_data:
  redis_data:
  static_volume:
 
# ========================================
# Networks (сети для связи контейнеров)
# ========================================
networks:
  app-network:
    driver: bridge

Команды Docker Compose

# ========================================
# Запуск и остановка
# ========================================
 
# Запустить все сервисы
docker compose up
 
# Запустить в фоне (detached mode)
docker compose up -d
 
# Пересобрать образы перед запуском
docker compose up --build
 
# Запустить конкретный сервис
docker compose up web
 
# Остановить все сервисы
docker compose down
 
# Остановить и удалить volumes
docker compose down -v
 
# ========================================
# Сборка
# ========================================
 
# Собрать образы
docker compose build
 
# Собрать без кеша
docker compose build --no-cache
 
# Собрать конкретный сервис
docker compose build web
 
# ========================================
# Логи и мониторинг
# ========================================
 
# Посмотреть логи всех сервисов
docker compose logs
 
# Следить за логами в реальном времени
docker compose logs -f
 
# Логи конкретного сервиса
docker compose logs -f web
 
# ========================================
# Выполнение команд
# ========================================
 
# Выполнить команду в запущенном контейнере
docker compose exec web bash
 
# Выполнить разовую команду (создаст новый контейнер)
docker compose run web python manage.py migrate
 
# Выполнить команду без создания новых контейнеров
docker compose exec web python manage.py createsuperuser
 
# ========================================
# Масштабирование
# ========================================
 
# Запустить 3 экземпляра сервиса
docker compose up --scale web=3
 
# ========================================
# Прочее
# ========================================
 
# Посмотреть запущенные сервисы
docker compose ps
 
# Перезапустить сервис
docker compose restart web
 
# Пауза и возобновление
docker compose pause
docker compose unpause
 
# Валидация docker-compose.yml
docker compose config

Пример: Full-stack приложение

# docker-compose.yml для веб-приложения
 
services:
  # ========================================
  # Frontend (React/Next.js)
  # ========================================
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    container_name: app-frontend
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/app
      - /app/node_modules # Исключаем node_modules из синхронизации
    environment:
      - NEXT_PUBLIC_API_URL=http://localhost:8000
    networks:
      - app-network
    depends_on:
      - backend
 
  # ========================================
  # Backend (FastAPI/Django)
  # ========================================
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    container_name: app-backend
    restart: unless-stopped
    ports:
      - "8000:8000"
    volumes:
      - ./backend:/app
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/app_db
      - REDIS_URL=redis://redis:6379/0
      - CELERY_BROKER_URL=redis://redis:6379/1
    env_file:
      - ./backend/.env
    networks:
      - app-network
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
 
  # ========================================
  # Celery Worker (фоновые задачи)
  # ========================================
  celery_worker:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    container_name: app-celery-worker
    restart: unless-stopped
    volumes:
      - ./backend:/app
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/app_db
      - CELERY_BROKER_URL=redis://redis:6379/1
      - CELERY_RESULT_BACKEND=redis://redis:6379/1
    networks:
      - app-network
    depends_on:
      - db
      - redis
    command: celery -A tasks worker --loglevel=info
 
  # ========================================
  # PostgreSQL
  # ========================================
  db:
    image: postgres:16-alpine
    container_name: app-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: app_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  # ========================================
  # Redis
  # ========================================
  redis:
    image: redis:7-alpine
    container_name: app-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - app-network
    command: redis-server --appendonly yes
 
  # ========================================
  # Nginx (reverse proxy + static files)
  # ========================================
  nginx:
    image: nginx:alpine
    container_name: app-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - static_volume:/var/www/static:ro
    networks:
      - app-network
    depends_on:
      - frontend
      - backend
 
volumes:
  postgres_data:
  redis_data:
  static_volume:
 
networks:
  app-network:
    driver: bridge

Environment Variables и .env файлы

# .env файл в корне проекта
 
# Database
DATABASE_URL=postgresql://postgres:postgres@db:5432/app_db
POSTGRES_DB=app_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
 
# Redis
REDIS_URL=redis://redis:6379/0
CELERY_BROKER_URL=redis://redis:6379/1
 
# Application
SECRET_KEY=your-secret-key-here
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
 
# API Keys
STRIPE_API_KEY=sk_test_xxxxx
AWS_ACCESS_KEY_ID=AKIAXXXXX
# docker-compose.yml
 
services:
  backend:
    # Вариант 1: Подключить весь .env файл
    env_file:
      - .env
 
    # Вариант 2: Явно указать переменные
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - SECRET_KEY=${SECRET_KEY}
      - DEBUG=${DEBUG:-False}  # Значение по умолчанию
 
    # Вариант 3: Hardcode (не рекомендуется для секретов)
    environment:
      - NODE_ENV=development

НИКОГДА не коммитьте .env файлы с секретами в Git! Добавьте .env в .gitignore и создайте .env.example с примерами значений.

Volumes: Постоянное хранение данных

Контейнеры эфемерны — при удалении контейнера все данные внутри теряются. Volumes решают эту проблему, сохраняя данные вне контейнера.

Типы монтирования

services:
  app:
    volumes:
      # ========================================
      # 1. Named Volume (рекомендуется для продакшн)
      # ========================================
      - db_data:/var/lib/postgresql/data
      # Docker управляет хранением (обычно /var/lib/docker/volumes/)
      # Не привязано к файловой системе хоста
 
      # ========================================
      # 2. Bind Mount (удобно для разработки)
      # ========================================
      - ./app:/app
      # Синхронизация с локальной файловой системой
      # Изменения в ./app мгновенно видны в контейнере
 
      # Исключение поддиректорий (анонимный volume)
      - /app/node_modules # Не синхронизировать node_modules
 
      # Read-only монтирование
      - ./config:/config:ro
 
      # ========================================
      # 3. tmpfs (в памяти, для временных данных)
      # ========================================
      - type: tmpfs
        target: /tmp
        tmpfs:
          size: 100m
 
volumes:
  db_data: # Объявление named volume

Исключение node_modules: Как это работает?

Паттерн - /app/node_modules — это один из самых частых источников путаницы в Docker. Давайте разберем, что происходит под капотом.

Проблема без исключения:

# ❌ Так делать НЕ надо для Node.js проектов
services:
  frontend:
    volumes:
      - ./frontend:/app # Монтируем всю директорию

Что происходит:

Ваш компьютер (macOS/Windows):
./frontend/
  ├── package.json
  ├── src/
  └── node_modules/  ← Скомпилированы для вашей ОС
      └── (бинарники для macOS/Windows)

Docker контейнер (Linux):
/app/
  ├── package.json
  ├── src/
  └── node_modules/  ← Перезаписаны с хоста!
      └── (бинарники для macOS/Windows в Linux контейнере) ❌

Результат: Приложение падает с ошибками "module not found" или "incompatible binary"

Решение: Анонимный volume для node_modules

# ✅ Правильный способ
services:
  frontend:
    volumes:
      - ./frontend:/app # Монтируем все файлы
      - /app/node_modules # Исключаем node_modules

Что происходит теперь:

1. Docker создает контейнер и устанавливает зависимости:
   RUN npm install  → /app/node_modules (Linux бинарники)

2. Монтирование volumes происходит в таком порядке:

   Шаг 1: - ./frontend:/app
   /app/ ← перезаписывается содержимым ./frontend/

   Шаг 2: - /app/node_modules
   /app/node_modules ← создается анонимный volume, который "маскирует"
                       ./frontend/node_modules

3. Финальная структура в контейнере:
   /app/
     ├── package.json        (с хоста)
     ├── src/                (с хоста, hot-reload работает!)
     └── node_modules/       (из контейнера, не с хоста!)
         └── (Linux бинарники работают)

Визуализация слоев:

┌─────────────────────────────────────┐
│   Слой 3: Anonymous Volume         │  Приоритет: 3 (самый высокий)
│   - /app/node_modules               │
│   (из контейнера после npm install) │
└─────────────────────────────────────┘
           ▲ "маскирует"
┌─────────────────────────────────────┐
│   Слой 2: Bind Mount                │  Приоритет: 2
│   - ./frontend:/app                 │
│   (с вашего компьютера)             │
└─────────────────────────────────────┘
           ▲ "перезаписывает"
┌─────────────────────────────────────┐
│   Слой 1: Образ контейнера          │  Приоритет: 1 (самый низкий)
│   (из Dockerfile)                   │
└─────────────────────────────────────┘

Альтернативный синтаксис (именованный volume):

services:
  frontend:
    volumes:
      - ./frontend:/app
      - node_modules:/app/node_modules # Именованный volume
 
volumes:
  node_modules: # Переиспользуется между перезапусками

Когда использовать:

# Node.js приложения
- ./app:/app
- /app/node_modules
 
# Python приложения (аналогично)
- ./app:/app
- /app/__pycache__ # Исключаем кеш Python
- /app/.venv # Если используете venv внутри контейнера
 
# Go приложения
- ./app:/app
- /app/vendor # Исключаем vendor
 
# Общий паттерн:
# Монтируем весь проект, но исключаем директории с бинарниками/кешем

Проверка, что это работает:

# Запустите контейнер
docker compose up -d frontend
 
# Войдите внутрь
docker compose exec frontend sh
 
# Проверьте node_modules
ls -la /app/node_modules
# Должны быть Linux бинарники (из npm install в контейнере)
 
# Проверьте, что изменения в src/ работают
# Измените файл в ./frontend/src/
# Он должен мгновенно отобразиться в контейнере:
cat /app/src/your-file.js

Примеры использования

Разработка (hot reload):

services:
  web:
    volumes:
      - ./src:/app/src # Код синхронизируется
      - /app/node_modules # node_modules остаются в контейнере

Production (база данных):

services:
  db:
    volumes:
      - postgres_data:/var/lib/postgresql/data
 
volumes:
  postgres_data:
    driver: local

Конфигурация (read-only):

services:
  nginx:
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

Управление volumes

# Список всех volumes
docker volume ls
 
# Создать volume
docker volume create my-volume
 
# Инспектировать volume
docker volume inspect my-volume
 
# Удалить volume
docker volume rm my-volume
 
# Удалить все неиспользуемые volumes
docker volume prune
 
# Backup volume (PostgreSQL пример)
docker run --rm \
  -v postgres_data:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/db-backup.tar.gz /data
 
# Restore volume
docker run --rm \
  -v postgres_data:/data \
  -v $(pwd):/backup \
  alpine tar xzf /backup/db-backup.tar.gz -C /

Networks: Связь между контейнерами

services:
  frontend:
    networks:
      - frontend-network
 
  backend:
    networks:
      - frontend-network
      - backend-network
 
  db:
    networks:
      - backend-network
 
networks:
  frontend-network:
  backend-network:

Связь через имена сервисов:

# backend/app.py
import requests
 
# Вместо localhost используем имя сервиса из docker-compose.yml
response = requests.get('http://db:5432')

depends_on и порядок запуска контейнеров

depends_on только определяет порядок запуска контейнеров, но НЕ ждет, пока сервис будет готов принимать подключения. База данных может быть запущена, но еще не готова!

Базовый depends_on

services:
  web:
    build: .
    depends_on:
      - db
      - redis
    # Docker запустит db и redis ПЕРЕД web
    # Но НЕ будет ждать готовности БД!
 
  db:
    image: postgres:16-alpine
 
  redis:
    image: redis:7-alpine

Что происходит:

1. Docker запускает db
2. Docker запускает redis
3. Docker запускает web (даже если db еще инициализируется)
4. web пытается подключиться к db → ошибка connection refused ❌

depends_on с условиями (рекомендуется)

services:
  web:
    build: .
    depends_on:
      db:
        condition: service_healthy # Ждать health check
      redis:
        condition: service_started # Ждать только запуска
    environment:
      DATABASE_URL: postgresql://postgres:postgres@db:5432/myapp
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    # Теперь web запустится только когда БД готова ✅
 
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

Health checks для популярных сервисов

# PostgreSQL
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
  interval: 10s
  timeout: 5s
  retries: 5
 
# MySQL
healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
  interval: 10s
  timeout: 5s
  retries: 5
 
# Redis
healthcheck:
  test: ["CMD", "redis-cli", "ping"]
  interval: 5s
  timeout: 3s
  retries: 5
 
# MongoDB
healthcheck:
  test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
  interval: 10s
  timeout: 5s
  retries: 5
 
# Web приложение
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s  # Дать время на инициализацию

Альтернатива: wait-for скрипты

Если health checks не подходят, используйте wait-for скрипты:

# Dockerfile
FROM python:3.11-slim
 
# Установите wait-for-it
ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh
 
COPY . /app
WORKDIR /app
 
# Используйте wait-for-it в command
# docker-compose.yml
services:
  web:
    build: .
    command: >
      sh -c "/wait-for-it.sh db:5432 --timeout=30 --strict --
             python manage.py migrate &&
             python manage.py runserver 0.0.0.0:8000"
    depends_on:
      - db

Best Practices: Перезапуск и обновление

Частая ошибка: docker compose restart НЕ применяет изменения из docker-compose.yml! Он только останавливает и запускает существующие контейнеры.

Когда использовать какую команду

# ========================================
# 1. Изменили docker-compose.yml (env, ports, volumes)
# ========================================
 
# ❌ НЕ РАБОТАЕТ
docker compose restart web
# Перезапускает контейнер со старой конфигурацией
 
# ✅ ПРАВИЛЬНО
docker compose up -d web
# Пересоздает контейнер с новой конфигурацией
 
# ========================================
# 2. Изменили Dockerfile или код приложения
# ========================================
 
# ❌ НЕ РАБОТАЕТ
docker compose restart web
# Использует старый образ
 
# ✅ ПРАВИЛЬНО
docker compose up -d --build web
# Пересобирает образ и пересоздает контейнер
 
# Или в два этапа:
docker compose build web
docker compose up -d web
 
# ========================================
# 3. Изменили переменные окружения в .env
# ========================================
 
# ❌ НЕ РАБОТАЕТ
docker compose restart web
# Не перечитывает .env файл
 
# ✅ ПРАВИЛЬНО
docker compose up -d --force-recreate web
# Пересоздает контейнер с новыми env переменными
 
# ========================================
# 4. Просто нужно перезапустить сервис (без изменений)
# ========================================
 
# ✅ РАБОТАЕТ
docker compose restart web
# Быстрый stop + start существующего контейнера
 
# ========================================
# 5. Обновили образ на Docker Hub
# ========================================
 
# ✅ ПРАВИЛЬНО
docker compose pull web  # Скачать новый образ
docker compose up -d web # Пересоздать контейнер
 
# ========================================
# 6. Полное обновление всех сервисов
# ========================================
 
# ✅ ПРАВИЛЬНО (продакшн)
docker compose pull           # Обновить все образы
docker compose up -d          # Пересоздать изменившиеся контейнеры
docker image prune -f         # Очистить старые образы
 
# ✅ ПРАВИЛЬНО (разработка)
docker compose down           # Остановить все
docker compose build          # Пересобрать образы
docker compose up -d          # Запустить заново

Что делает каждая команда

# restart — только stop + start контейнера
# ✅ Когда: сервис завис, нужен быстрый перезапуск
# ❌ НЕ применяет: изменения конфигурации, новый код, новые env
docker compose restart web
 
# up -d — создает/пересоздает контейнеры по необходимости
# ✅ Когда: изменили docker-compose.yml, .env, volumes
# ✅ Применяет: новую конфигурацию, новые env
# ❌ НЕ применяет: изменения в Dockerfile (нужен --build)
docker compose up -d web
 
# up -d --build — пересобирает образ и пересоздает контейнер
# ✅ Когда: изменили Dockerfile, код приложения
# ✅ Применяет: все изменения в коде и конфигурации
docker compose up -d --build web
 
# up -d --force-recreate — принудительно пересоздает контейнер
# ✅ Когда: изменили .env, нужна гарантия пересоздания
# ✅ Применяет: новые env, конфигурацию
docker compose up -d --force-recreate web
 
# down + up — полный пересоздание с очисткой
# ✅ Когда: радикальные изменения, проблемы с сетями/volumes
# ⚠️ Удаляет: контейнеры, сети (volumes остаются)
docker compose down
docker compose up -d
 
# down -v + up — полная очистка включая данные
# ✅ Когда: нужно сбросить БД, начать с чистого листа
# ⚠️ Удаляет: контейнеры, сети, volumes (ВСЕ ДАННЫЕ!)
docker compose down -v
docker compose up -d

Workflow для разработки

# ========================================
# Первый запуск проекта
# ========================================
docker compose up -d
docker compose logs -f  # Проверить логи
 
# ========================================
# Изменили код приложения
# ========================================
# Если есть hot-reload (volumes монтированы) — ничего не делать
# Если нет hot-reload:
docker compose up -d --build web
 
# ========================================
# Изменили зависимости (package.json, requirements.txt)
# ========================================
docker compose build web  # Пересобрать с новыми зависимостями
docker compose up -d web
 
# ========================================
# Изменили docker-compose.yml (порты, env)
# ========================================
docker compose up -d  # Применить изменения
 
# ========================================
# Добавили новый сервис
# ========================================
docker compose up -d  # Запустит только новый сервис
 
# ========================================
# Полная перезагрузка (если что-то сломалось)
# ========================================
docker compose down
docker compose up -d --build
 
# ========================================
# Очистить все (включая данные БД)
# ========================================
docker compose down -v
docker compose up -d --build

Чек-лист перед применением изменений

# 1. Проверить синтаксис docker-compose.yml
docker compose config
 
# 2. Проверить, какие сервисы будут пересозданы (dry-run)
docker compose up -d --dry-run
 
# 3. Посмотреть логи перед изменениями
docker compose logs --tail=50 web
 
# 4. Создать backup БД (если есть важные данные)
docker compose exec db pg_dump -U postgres mydb > backup.sql
 
# 5. Применить изменения
docker compose up -d
 
# 6. Проверить, что все запустилось
docker compose ps
docker compose logs -f web
 
# 7. Проверить health checks
docker compose ps  # STATUS должен быть healthy

Частые ошибки

# ❌ ОШИБКА: Изменили код, но не пересобрали образ
docker compose restart web
# Решение:
docker compose up -d --build web
 
# ❌ ОШИБКА: Изменили .env, но не пересоздали контейнер
docker compose restart web
# Решение:
docker compose up -d --force-recreate web
 
# ❌ ОШИБКА: Приложение стартует раньше БД
depends_on:
  - db  # Недостаточно!
# Решение: добавить healthcheck
depends_on:
  db:
    condition: service_healthy
 
# ❌ ОШИБКА: Volumes не обновляются
docker compose down
docker compose up -d  # Volumes остались старыми
# Решение (если нужно очистить):
docker compose down -v  # ⚠️ Удалит все данные!

Production Best Practices

Разработка и продакшн — разные миры. Никогда не используйте --reload, bind mounts и debug режим в production!

Multi-stage Builds

# ========================================
# Stage 1: Builder (установка зависимостей)
# ========================================
FROM node:20-alpine AS builder
 
WORKDIR /app
 
COPY package*.json ./
RUN npm ci --only=production
 
COPY . .
RUN npm run build
 
# ========================================
# Stage 2: Production (минимальный образ)
# ========================================
FROM node:20-alpine AS production
 
WORKDIR /app
 
# Копируем только необходимое из builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .
 
RUN addgroup -g 1000 appuser && \
    adduser -D -u 1000 -G appuser appuser && \
    chown -R appuser:appuser /app
 
USER appuser
 
EXPOSE 3000
 
CMD ["node", "dist/server.js"]

Результат:

Builder stage: 800MB
Production stage: 150MB  ← В 5 раз меньше!

docker-compose.prod.yml

# docker-compose.prod.yml
 
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.prod
    restart: always
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
    # ❌ Убираем bind mounts
    # ❌ Убираем --reload
    # ✅ Добавляем health checks
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    # ✅ Ограничиваем ресурсы
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2G
        reservations:
          cpus: "0.5"
          memory: 512M
 
  db:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
 
secrets:
  db_password:
    file: ./secrets/db_password.txt
 
volumes:
  postgres_data:
    driver: local

Запуск:

docker compose -f docker-compose.prod.yml up -d

Security Best Practices

Безопасность — не опциональная фича. Один взломанный контейнер может скомпрометировать весь хост. Следуйте этим практикам с самого начала.

1. Выбор базового образа

Сравнение типов образов:

ТипРазмерБезопасностьUse CaseПример
Full800-1000MB⚠️ НизкаяDevelopment, legacy appspython:3.11
Slim150-200MB✅ СредняяProduction appspython:3.11-slim
Alpine50-80MB✅ ВысокаяМинимал истичные appspython:3.11-alpine
Distroless20-50MB✅✅ Очень высокаяSecurity-criticalgcr.io/distroless/python3
Scratch0MB (бинарник)✅✅✅ МаксимальнаяCompiled apps (Go, Rust)FROM scratch
# ❌ Плохо: полный образ с ненужными пакетами
FROM python:3.11
# 1GB, содержит gcc, make, git и сотни других пакетов
 
# ✅ Хорошо: slim для баланса
FROM python:3.11-slim
# 150MB, минимальный набор для Python
 
# ✅✅ Отлично: distroless для максимальной безопасности
FROM gcr.io/distroless/python3-debian12
# 50MB, только Python runtime, нет shell, нет package manager

Когда использовать:

  • Full — только для development с активной отладкой
  • Slim — оптимальный выбор для большинства production приложений
  • Alpine — когда критичен размер (embedded, edge computing)
  • Distroless — финансовые, медицинские, security-critical системы
  • Scratch — statically compiled Go/Rust бинарники

2. Непривилегированный пользователь

Запуск от root — критическая уязвимость. Если атакующий получит доступ к контейнеру, он сможет читать /etc/shadow, модифицировать системные файлы и потенциально выйти из контейнера на хост.

Демонстрация проблемы:

# ❌ Опасно: запуск от root (по умолчанию)
FROM python:3.11-slim
COPY app.py .
CMD ["python", "app.py"]
 
# Проверка:
# docker exec -it container whoami  # root 💀
# docker exec -it container cat /etc/shadow  # работает! 💀

Правильное решение (Debian/Ubuntu):

FROM python:3.11-slim
 
WORKDIR /app
 
# Установка зависимостей (требует root)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
# Копирование кода
COPY . .
 
# Создание непривилегированного пользователя
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app
 
# Переключение на непривилегированного пользователя
USER appuser
 
CMD ["python", "app.py"]

Для Alpine Linux:

FROM python:3.11-alpine
 
WORKDIR /app
 
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
# Alpine использует adduser вместо useradd
RUN addgroup -g 1000 appuser && \
    adduser -D -u 1000 -G appuser appuser && \
    chown -R appuser:appuser /app
 
USER appuser
 
CMD ["python", "app.py"]

3. Сканирование уязвимостей

Trivy (рекомендуется — быстрый, бесплатный, точный):

# Установка
brew install trivy  # macOS
# или
apt-get install trivy  # Ubuntu/Debian
 
# Сканирование образа
trivy image python:3.11-slim
 
# Только критические и высокие
trivy image --severity HIGH,CRITICAL python:3.11-slim
 
# CI/CD: fail при наличии критических уязвимостей
trivy image --severity CRITICAL --exit-code 1 my-app:latest
 
# Сканирование Dockerfile (до сборки)
trivy config Dockerfile
 
# Игнорирование конкретных CVE (если нет fix)
trivy image --ignorefile .trivyignore my-app:latest
 
# .trivyignore
# CVE-2024-12345 (no fix available yet)

GitHub Actions интеграция:

name: Security Scan
 
on: [push, pull_request]
 
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Build image
        run: docker build -t my-app:${{ github.sha }} .
 
      - name: Run Trivy scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: "my-app:${{ github.sha }}"
          severity: "CRITICAL,HIGH"
          exit-code: "1" # Fail build при уязвимостях
          format: "sarif"
          output: "trivy-results.sarif"
 
      - name: Upload results to GitHub Security
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: "trivy-results.sarif"

Docker Scout (встроенный в Docker Desktop):

# Включить Docker Scout
docker scout enable
 
# Сканирование
docker scout cves my-app:latest
 
# Рекомендации по исправлению
docker scout recommendations my-app:latest
 
# Сравнение версий
docker scout compare my-app:v1.0 my-app:v1.1

Snyk (альтернатива с хорошим UI):

# Установка
npm install -g snyk
 
# Авторизация
snyk auth
 
# Сканирование
snyk container test my-app:latest
 
# Мониторинг (уведомления о новых CVE)
snyk container monitor my-app:latest

4. Secrets Management

НИКОГДА не коммитьте секреты в код или Dockerfile. Даже если репозиторий приватный. Даже "временно". История Git помнит всё.

❌ Антипаттерны (что НЕ делать):

# ❌ 1. Hardcoded секреты
ENV DATABASE_PASSWORD=superSecret123
ENV API_KEY=sk_live_xxxxxxxxxxxx
 
# ❌ 2. ARG не защищает (остается в docker history)
ARG SECRET_KEY
ENV SECRET_KEY=$SECRET_KEY
 
# docker history показывает значение! 💀
 
# ❌ 3. COPY секретов в образ
COPY .env /app/.env
COPY secrets.json /app/config/
 
# docker cp может извлечь! 💀

Проверка утечки:

# Проверить, нет ли секретов в образе
docker history my-app:latest
 
# Проверить environment variables
docker inspect my-app:latest | jq '.[].Config.Env'
 
# Поиск секретов в коде (truffleHog)
docker run --rm -v $(pwd):/pwd trufflesecurity/trufflehog:latest filesystem /pwd

✅ Правильные подходы:

Development: .env файлы (НЕ коммитить в Git!)

# .gitignore
.env
.env.local
*.secret
 
# .env.example (коммитить)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
SECRET_KEY=your-secret-key-here
API_KEY=your-api-key-here
# docker-compose.yml
services:
  app:
    env_file:
      - .env # Файл НЕ в Git!
    environment:
      - NODE_ENV=development

Production: Docker Secrets (Swarm/Compose)

# Создание secret
echo "my-db-password" | docker secret create db_password -
 
# Или из файла
docker secret create db_password ./db_password.txt
# docker-compose.yml (Swarm mode)
services:
  app:
    secrets:
      - db_password
      - api_key
    environment:
      # Путь к secret файлу
      DB_PASSWORD_FILE: /run/secrets/db_password
      API_KEY_FILE: /run/secrets/api_key
 
secrets:
  db_password:
    external: true
  api_key:
    external: true
# app.py
import os
 
def read_secret(secret_name):
    try:
        with open(f'/run/secrets/{secret_name}', 'r') as f:
            return f.read().strip()
    except FileNotFoundError:
        # Fallback для development
        return os.getenv(secret_name.upper())
 
db_password = read_secret('db_password')
api_key = read_secret('api_key')

Enterprise: HashiCorp Vault

# Использование Vault
import hvac
 
client = hvac.Client(url='https://vault.example.com')
client.token = os.getenv('VAULT_TOKEN')
 
secret = client.secrets.kv.v2.read_secret_version(
    path='prod/database',
)
 
db_password = secret['data']['data']['password']

Cloud Secrets Manager (AWS/GCP/Azure)

# AWS Secrets Manager
import boto3
 
client = boto3.client('secretsmanager', region_name='us-east-1')
 
response = client.get_secret_value(SecretId='prod/db/password')
db_password = response['SecretString']

Сравнение методов:

МетодБезопасностьСложностьСтоимостьUse Case
.env файлы⚠️ НизкаяПростаяFreeDevelopment only
Docker Secrets✅ СредняяСредняяFreeSwarm production
Vault✅✅ ВысокаяВысокаяSelf-hostedEnterprise on-premise
AWS Secrets Manager✅✅ ВысокаяСредняя~$0.40/secret/monthAWS production
GCP Secret Manager✅✅ ВысокаяСредняя~$0.06/secret/monthGCP production

5. Hardening образов

Multi-stage builds для минимизации attack surface:

# ========================================
# Stage 1: Builder с инструментами
# ========================================
FROM python:3.11 AS builder
 
WORKDIR /app
 
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
 
# ========================================
# Stage 2: Distroless production
# ========================================
FROM gcr.io/distroless/python3-debian12
 
WORKDIR /app
 
# Копируем только runtime зависимости
COPY --from=builder /root/.local /root/.local
COPY app.py .
 
ENV PATH=/root/.local/bin:$PATH
 
CMD ["python", "app.py"]
 
# Результат: 50MB вместо 1GB, нет shell, нет package manager

Read-only filesystem:

services:
  app:
    image: my-app:latest
    read_only: true # Файловая система только для чтения
    tmpfs:
      - /tmp:size=100M # Временные файлы в RAM
      - /var/run:size=10M
    volumes:
      - app_uploads:/app/uploads # Только явные write volumes

Capabilities (минимальные привилегии):

services:
  app:
    cap_drop:
      - ALL # Удаляем ВСЕ capabilities
    cap_add:
      - NET_BIND_SERVICE # Добавляем только необходимые (порты < 1024)
    security_opt:
      - no-new-privileges:true # Запрещаем повышение привилегий

6. Runtime Security

services:
  app:
    # Минимальные capabilities
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
 
    # Security options
    security_opt:
      - no-new-privileges:true
      - apparmor=docker-default # AppArmor profile
      - seccomp=./seccomp-profile.json # Syscall filtering
 
    # Resource limits (защита от DoS)
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2G
        reservations:
          cpus: "0.5"
          memory: 512M
 
    # PID limits
    pids_limit: 100
 
    # Read-only root filesystem
    read_only: true
    tmpfs:
      - /tmp
      - /var/run

7. Production Security Checklist

Используйте этот чек-лист перед каждым production деплоем. Один пропущенный пункт может стоить безопасности всей системы.

Pre-Deployment (за 48 часов до релиза):

  • Образы от официальных источников или внутреннего реестра
  • Все образы просканированы Trivy/Snyk, критических CVE нет
  • Нет секретов в docker history my-app:latest
  • Нет секретов в docker inspect my-app:latest
  • Используется непривилегированный пользователь (USER appuser)
  • Базовый образ: slim/alpine/distroless (не full)
  • Multi-stage build (отделен builder от runtime)
  • Health checks настроены (HEALTHCHECK или в compose)
  • Resource limits установлены (CPU, memory)
  • Логирование в stdout/stderr (не в файлы)
  • Секреты через Secrets Manager/Vault (не .env)
  • Read-only filesystem где возможно
  • Capabilities минимизированы (cap_drop: ALL)
  • .dockerignore исключает .git, .env, secrets
  • SBOM сгенерирован и сохранен

Post-Deployment (первые 24 часа):

  • Smoke tests прошли успешно
  • Метрики в норме (CPU < 70%, Memory < 80%)
  • Логи не содержат ERROR/CRITICAL
  • Health checks работают (все контейнеры healthy)
  • Backup/restore процедура протестирована
  • Мониторинг алертов настроен (Prometheus/Grafana)
  • Логи централизованы (ELK/Loki)

Weekly Maintenance:

  • Обновление патч-версий базовых образов
  • Повторное сканирование на уязвимости
  • Ротация секретов (согласно политике)
  • Проверка логов на подозрительную активность
  • Обновление SBOM

8. Security Auditing Tools

# ========================================
# Docker Bench Security (CIS Benchmarks)
# ========================================
 
# Запуск аудита хоста
docker run --rm --net host --pid host --userns host --cap-add audit_control \
  -v /var/lib:/var/lib \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /etc:/etc:ro \
  docker/docker-bench-security
 
# Проверяет:
# - Конфигурацию Docker daemon
# - Настройки безопасности хоста
# - Конфигурацию контейнеров
# - Security operations
 
# ========================================
# Hadolint (Dockerfile linter)
# ========================================
 
# Установка
brew install hadolint
 
# Проверка Dockerfile
hadolint Dockerfile
 
# Автофикс (где возможно)
hadolint --format json Dockerfile | jq
 
# CI/CD интеграция
docker run --rm -i hadolint/hadolint < Dockerfile
 
# ========================================
# Dockle (Image linter)
# ========================================
 
# Установка
brew install goodwithtech/r/dockle
 
# Проверка образа
dockle my-app:latest
 
# Только критичные проблемы
dockle --exit-code 1 --exit-level fatal my-app:latest
 
# ========================================
# Docker Content Trust (подписи образов)
# ========================================
 
# Включить проверку подписей
export DOCKER_CONTENT_TRUST=1
 
# Теперь docker pull проверяет подписи
docker pull my-app:latest  # Fail если нет подписи
 
# Подписать образ при push
docker trust sign my-app:latest

9. SBOM (Software Bill of Materials)

SBOM — это список всех компонентов вашего софта. Критичен для отслеживания уязвимостей и compliance (NIST, EU Cyber Resilience Act).

# ========================================
# Syft — генерация SBOM
# ========================================
 
# Установка
brew install syft
 
# Генерация SBOM в SPDX формате
syft packages my-app:latest -o spdx-json > sbom.json
 
# CycloneDX формат
syft packages my-app:latest -o cyclonedx-json > sbom.cdx.json
 
# SBOM для Dockerfile (до сборки)
syft packages dir:. -o spdx-json > sbom-source.json
 
# ========================================
# Grype — проверка уязвимостей в SBOM
# ========================================
 
# Установка
brew install grype
 
# Проверка SBOM
grype sbom:sbom.json
 
# Проверка образа напрямую
grype my-app:latest
 
# Только HIGH/CRITICAL
grype --fail-on high my-app:latest
 
# ========================================
# CI/CD Pipeline
# ========================================
 
# GitHub Actions
- name: Generate SBOM
  run: |
    syft packages my-app:latest -o spdx-json > sbom.json
 
- name: Scan SBOM for vulnerabilities
  run: |
    grype sbom:sbom.json --fail-on critical
 
- name: Upload SBOM as artifact
  uses: actions/upload-artifact@v3
  with:
    name: sbom
    path: sbom.json

10. Реальные примеры атак и защита

Эти атаки происходят в реальности. Каждый пример основан на реальных CVE и инцидентах.

Атака 1: Container Breakout через Docker socket

# ❌ КРИТИЧЕСКАЯ УЯЗВИМОСТЬ
services:
  app:
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
 
# Что может сделать атакующий:
# 1. Запустить привилегированный контейнер
# 2. Получить root на хосте
# 3. Читать файлы хоста
# 4. Запускать любые команды на хосте

Эксплуатация:

# Внутри скомпрометированного контейнера
docker run -it --privileged --net=host --pid=host --ipc=host \
  -v /:/host alpine chroot /host
 
# Теперь атакующий имеет root на хосте! 💀

✅ Защита:

# НИКОГДА не монтируйте docker.sock в production
# Если необходимо (CI/CD runners):
services:
  gitlab-runner:
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro # Read-only
    # + используйте rootless Docker
    # + изолируйте в отдельную сеть
    # + мониторьте docker API вызовы

Атака 2: Secrets в docker history

# ❌ Уязвимость
FROM python:3.11-slim
ENV API_KEY=sk_live_51234567890abcdef  # Секрет в слое!
RUN pip install requests
CMD ["python", "app.py"]

Эксплуатация:

docker history my-app:latest
 
# OUTPUT:
# ENV API_KEY=sk_live_51234567890abcdef  # Видно в plain text! 💀
 
# Даже если удалить ENV позже, он останется в слоях:
docker save my-app:latest | tar -xO | grep "API_KEY"

✅ Защита:

# Используйте multi-stage build + secrets mount
FROM python:3.11-slim
 
# Секрет передается через build-time secret (не остается в слоях)
RUN --mount=type=secret,id=api_key \
    pip install requests
 
CMD ["python", "app.py"]
# Build с секретом
echo "sk_live_51234567890abcdef" > /tmp/api_key.txt
docker build --secret id=api_key,src=/tmp/api_key.txt -t my-app .
rm /tmp/api_key.txt
 
# Секрет НЕ остается в образе
docker history my-app:latest  # API_KEY не видно ✅

Атака 3: Supply Chain Attack

# ❌ Опасно
FROM random-username/python:latest
 
# Проблемы:
# 1. Неизвестный автор (не Docker Official)
# 2. latest тег (может измениться)
# 3. Нет проверки подписи

Реальный инцидент: В 2023 году атакующие загрузили malware в популярные Docker образы на Docker Hub. Образы скачали тысячи раз.

✅ Защита:

# 1. Официальные образы
FROM python:3.11.7-slim
 
# 2. Точный digest (immutable)
FROM python:3.11.7-slim@sha256:abc123def456...
 
# 3. Внутренний registry (proxy Docker Hub)
FROM registry.company.com/python:3.11.7-slim
 
# 4. Verify образы с Docker Content Trust
# Включить проверку подписей
export DOCKER_CONTENT_TRUST=1
 
# Сканирование перед использованием
trivy image python:3.11-slim
 
# Проверка SBOM
syft packages python:3.11-slim | grep malware

Золотое правило безопасности: Trust but verify. Даже официальные образы могут содержать уязвимости. Всегда сканируйте, всегда обновляйте, всегда минимизируйте.

Troubleshooting: Распространенные проблемы

90% проблем с Docker решаются через логи (docker logs) и inspect (docker inspect). Используйте дерево диагностики ниже для быстрой навигации.

Дерево диагностики

Что не работает?
│
├─ Контейнер не запускается / сразу падает
│  ├─ Exit code 0 → См. Проблема 1 (CMD завершился успешно, но это ошибка)
│  ├─ Exit code 1 → См. Проблема 1 (ошибка приложения, смотри logs)
│  ├─ Exit code 137 → См. Проблема 11 (OOM killed - не хватает памяти)
│  └─ Exit code 139 → Segmentation fault (проблема в коде приложения)
│
├─ Контейнер запущен, но приложение не работает
│  ├─ Не отвечает на запросы → См. Проблема 3 (порты)
│  ├─ Ошибки подключения к БД → См. Проблема 2, 9 (networks, health checks)
│  └─ Медленно работает → См. Проблема 5, 11 (I/O, ресурсы)
│
├─ docker-compose.yml не применяется
│  └─ Изменения не видны → См. Проблема 6 (restart vs up -d)
│
├─ Проблемы с данными
│  ├─ Данные пропали после restart → См. Проблема 4 (volumes)
│  ├─ Изменения кода не видны → См. Проблема 4, 8 (volumes, cache)
│  └─ Логи заполнили диск → См. Проблема 12 (log rotation)
│
└─ Другое
   ├─ Высокое потребление ресурсов → См. Проблема 11
   ├─ Контейнер "завис" → См. Проблема 10 (deadlock)
   └─ Permission denied → См. Проблема 7 (CRLF, права)

Проблема 1: Контейнер сразу останавливается

# Посмотреть логи
docker compose logs web
 
# Типичные причины:
# - Ошибка в CMD/ENTRYPOINT
# - Приложение падает при запуске
# - Неправильные переменные окружения
 
# Проверить, запущен ли процесс
docker compose exec web ps aux

Проблема 2: Контейнеры не видят друг друга

# Проверить, что контейнеры в одной сети
docker network inspect my-app_app-network
 
# Проверить DNS резолвинг
docker compose exec web ping db
 
# Типичные причины:
# - Неправильное имя сервиса в URL
# - Разные networks у сервисов
# - Сервис еще не запущен (нет depends_on)

Проблема 3: Порт уже занят

# Error: port is already allocated
 
# Найти процесс на порту
lsof -i :8000
 
# Или использовать другой порт
ports:
  - "8001:8000"  # host:container

Проблема 4: Changes не отображаются

# 1. Проверить volumes
docker compose exec web ls -la /app
 
# 2. Пересобрать образ
docker compose build --no-cache web
 
# 3. Перезапустить с пересборкой
docker compose up --build
 
# 4. Проверить .dockerignore
cat .dockerignore

Проблема 5: Медленная работа (macOS/Windows)

# Проблема: Bind mounts медленные на macOS/Windows
 
# ❌ Медленно
volumes:
  - ./app:/app
 
# ✅ Быстрее: delegated mode (macOS)
volumes:
  - ./app:/app:delegated
 
# ✅ Или используйте named volume для node_modules
volumes:
  - ./app:/app
  - /app/node_modules

Проблема 6: docker-compose.yml не применяется

Симптомы:

  • Изменили docker-compose.yml (порты, env, volumes)
  • Запустили docker compose restart
  • Изменения не применились

Причина: docker compose restart только останавливает и запускает существующие контейнеры. Не пересоздает их с новой конфигурацией!

Решение:

# ❌ НЕ РАБОТАЕТ
docker compose restart web
 
# ✅ ПРАВИЛЬНО: пересоздать контейнер
docker compose up -d web
 
# Если изменили Dockerfile:
docker compose up -d --build web
 
# Если изменили .env:
docker compose up -d --force-recreate web

Проблема 7: "No such file or directory" в Linux контейнере

Симптомы:

Error: /bin/sh: ./start.sh: No such file or directory

Причины:

  1. CRLF line endings (Windows → Linux)
  2. Permissions (файл не исполняемый)

Диагностика:

# Проверить line endings
file start.sh
# Должно быть: "ASCII text" (LF)
# НЕ должно быть: "ASCII text, with CRLF" (Windows)
 
# Проверить permissions
ls -la start.sh
# Должно быть: -rwxr-xr-x (executable)

Решение:

# 1. Конвертировать CRLF → LF
dos2unix start.sh
 
# Или в Git (автоматически):
# .gitattributes
*.sh text eol=lf
*.py text eol=lf
 
# 2. Сделать исполняемым
chmod +x start.sh
# В Dockerfile (гарантировать permissions)
COPY start.sh /app/
RUN chmod +x /app/start.sh
CMD ["/app/start.sh"]

Проблема 8: Зависимости не обновились

Симптомы:

  • Изменили requirements.txt / package.json
  • Пересобрали образ (docker compose build)
  • Старые зависимости все еще используются

Причина: Docker использует кеш слоев. Если COPY . . выполняется ДО pip install, кеш не сбрасывается.

Решение:

# Пересобрать без кеша
docker compose build --no-cache web
 
# Или удалить старый образ
docker rmi my-app:latest
docker compose build web

Правильный Dockerfile (для избежания проблемы):

# ✅ Сначала зависимости (кешируются)
COPY requirements.txt .
RUN pip install -r requirements.txt
 
# Потом код (меняется часто)
COPY . .

Проблема 9: Health check failed

Симптомы:

docker compose ps
# STATUS: unhealthy

Диагностика:

# Посмотреть детали health check
docker inspect web | jq '.[].State.Health'
 
# Логи health check
docker inspect web | jq '.[].State.Health.Log'
 
# Попробовать health check вручную
docker exec web curl -f http://localhost:8000/health

Типичные причины:

  1. Приложение не слушает на 0.0.0.0 (слушает на 127.0.0.1)
  2. Health endpoint не существует
  3. Health check слишком быстрый (приложение не успело стартовать)

Решение:

services:
  web:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s # Проверка каждые 30 секунд
      timeout: 10s # Таймаут ответа
      retries: 3 # Количество попыток
      start_period: 40s # ← ВАЖНО: дать время на старт приложения

Проблема 10: Контейнер "завис" (deadlock)

Симптомы:

  • Контейнер запущен (docker ps показывает STATUS: Up)
  • Приложение не отвечает
  • CPU/Memory в норме

Диагностика:

# Проверить процессы внутри
docker exec web ps aux
 
# Проверить, слушает ли приложение на порту
docker exec web netstat -tulpn
 
# Проверить логи (может быть ошибка без crash)
docker logs web --tail 100
 
# Strace (если доступен)
docker exec web strace -p 1

Решение:

# Перезапустить контейнер
docker compose restart web
 
# Если не помогает — пересоздать
docker compose up -d --force-recreate web
 
# Посмотреть внутрь (debugging)
docker exec -it web bash

Проблема 11: Высокое потребление ресурсов

Симптомы:

  • CPU 100% или Memory 90%+
  • Exit code 137 (OOM killed)

Диагностика:

# Real-time мониторинг
docker stats web
 
# Лимиты контейнера
docker inspect web | jq '.[].HostConfig.Memory'
docker inspect web | jq '.[].HostConfig.NanoCpus'

Решение:

services:
  web:
    deploy:
      resources:
        limits:
          cpus: "2.0" # Максимум 2 CPU
          memory: 2G # Максимум 2GB RAM
        reservations:
          cpus: "0.5" # Минимум 0.5 CPU
          memory: 512M # Минимум 512MB RAM

Memory leak detection:

# Мониторить memory во времени
watch -n 1 'docker stats web --no-stream'
 
# Профилировать приложение (Python example)
docker exec web python -m memory_profiler app.py

Проблема 12: Логи заполняют диск

Симптомы:

df -h  # Диск заполнен
du -sh /var/lib/docker/containers/*/*-json.log  # Огромные лог-файлы

Решение:

services:
  web:
    logging:
      driver: "json-file"
      options:
        max-size: "10m" # Максимум 10MB на файл
        max-file: "3" # Максимум 3 файла
        # Итого: максимум 30MB логов на контейнер

Очистка существующих логов:

# Найти большие лог-файлы
find /var/lib/docker/containers/ -name "*-json.log" -size +100M
 
# Очистить логи всех остановленных контейнеров
docker container prune
 
# Очистить логи конкретного контейнера
truncate -s 0 $(docker inspect --format='{{.LogPath}}' web)

Debug команды и техники

# ========================================
# docker inspect (продвинутое использование)
# ========================================
 
# Получить IP адрес
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web
 
# Получить environment variables
docker inspect -f '{{.Config.Env}}' web
 
# Health check status
docker inspect -f '{{.State.Health.Status}}' web
 
# Все mounts
docker inspect -f '{{json .Mounts}}' web | jq
 
# Exit code последнего запуска
docker inspect -f '{{.State.ExitCode}}' web
 
# ========================================
# docker stats
# ========================================
 
# Real-time мониторинг
docker stats
 
# Без stream (один snapshot)
docker stats --no-stream web
 
# Кастомный формат
docker stats --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}"
 
# ========================================
# docker events
# ========================================
 
# Все события real-time
docker events
 
# Фильтр по типу
docker events --filter type=container
 
# Фильтр по контейнеру
docker events --filter container=web
 
# ========================================
# docker logs (продвинутые опции)
# ========================================
 
# Последние N строк
docker logs --tail 100 web
 
# Логи за период
docker logs --since 2024-01-01 web
docker logs --since 10m web
 
# С timestamps
docker logs --timestamps web
 
# Follow + grep
docker logs -f web | grep ERROR

FAQ: Быстрые ответы

Q: Как полностью очистить Docker?

docker system prune -a --volumes
# ⚠️ Удалит ВСЁ: контейнеры, образы, volumes!

Q: Как войти в контейнер от root?

docker exec -u root -it web bash

Q: Как скопировать файлы из/в контейнер?

docker cp web:/app/logs/error.log ./
docker cp ./config.json web:/app/

Q: Как узнать, почему контейнер упал?

docker logs web
docker inspect web | jq '.State'

Q: Как проверить, что volume содержит данные?

docker volume inspect my-volume
docker run --rm -v my-volume:/data alpine ls -la /data

Q: Как ограничить размер логов?

logging:
  options:
    max-size: "10m"
    max-file: "3"

Q: Как узнать размер слоев образа?

docker history my-app:latest
# Или используйте dive:
dive my-app:latest

Дополнительные инструменты

# ctop — top для контейнеров (TUI)
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  quay.io/vektorlab/ctop:latest
 
# dive — анализ слоев образов
brew install dive
dive my-app:latest
 
# lazydocker — TUI для управления Docker
brew install lazydocker
lazydocker
 
# docker-slim — минимизация образов
docker-slim build my-app:latest

Практические примеры

Пример 1: Django + PostgreSQL + Redis

# docker-compose.yml
 
services:
  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/django_db
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: django_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
 
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
 
volumes:
  postgres_data:
  redis_data:
# Dockerfile
 
FROM python:3.11-slim
 
ENV PYTHONUNBUFFERED=1
 
WORKDIR /code
 
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
EXPOSE 8000

Команды:

# Первый запуск
docker compose up -d
docker compose exec web python manage.py migrate
docker compose exec web python manage.py createsuperuser
 
# Разработка
docker compose logs -f web
 
# Остановка
docker compose down

Пример 2: Next.js + Node.js API + MongoDB

services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    environment:
      - NEXT_PUBLIC_API_URL=http://localhost:4000
    depends_on:
      - backend
 
  backend:
    build: ./backend
    ports:
      - "4000:4000"
    volumes:
      - ./backend:/app
      - /app/node_modules
    environment:
      - MONGODB_URI=mongodb://mongo:27017/myapp
      - PORT=4000
    depends_on:
      - mongo
 
  mongo:
    image: mongo:7
    ports:
      - "27017:27017"
    volumes:
      - mongo_data:/data/db
    environment:
      - MONGO_INITDB_DATABASE=myapp
 
volumes:
  mongo_data:

Практические упражнения

Лучший способ изучить Docker — делать. Эти упражнения покрывают все ключевые концепции из материала. Решайте последовательно, проверяя каждый критерий.

Упражнение 1: Первый production-ready Dockerfile (Уровень: ⭐)

Задача: Создайте Dockerfile для простого Python скрипта, который выполняется раз в минуту и записывает текущее время в лог.

Требования:

  • Базовый образ: python:3.11-slim
  • Размер итогового образа < 200MB
  • Непривилегированный пользователь (не root)
  • Установлены только необходимые пакеты
  • Правильный .dockerignore

Критерии проверки:

# 1. Собрать образ
docker build -t my-cron-app .
 
# 2. Проверить размер
docker images my-cron-app
# Должен быть < 200MB
 
# 3. Проверить, что пользователь не root
docker inspect my-cron-app | jq '.[].Config.User'
# Должен быть "appuser" или "1000"
 
# 4. Запустить и проверить логи
docker run --name test-cron my-cron-app
docker logs test-cron
# Должен писать время каждую секунду
 
# 5. Очистка
docker rm -f test-cron
💡 Подсказка
  • Используйте python:3.11-slim, не python:3.11 (slim на 500MB меньше)
  • Создайте пользователя с useradd -m -u 1000 appuser
  • Не забудьте USER appuser перед CMD
  • В .dockerignore добавьте .git, __pycache__, *.pyc
✅ Решение

app.py:

import time
from datetime import datetime
 
while True:
    print(f"[{datetime.now()}] Cron job running...")
    time.sleep(60)

Dockerfile:

FROM python:3.11-slim
 
WORKDIR /app
 
# Копируем только необходимые файлы
COPY app.py .
 
# Создаем непривилегированного пользователя
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app
 
USER appuser
 
CMD ["python", "-u", "app.py"]

.dockerignore:

.git
.gitignore
__pycache__
*.pyc
*.pyo
.vscode
.idea
README.md

Объяснение:

  • python:3.11-slim — 150MB вместо 1GB
  • -u флаг в python — unbuffered output (логи видны сразу)
  • useradd -m — создает home directory
  • chown — даем права appuser на /app
  • .dockerignore — ускоряет сборку и уменьшает образ

Упражнение 2: Docker Compose multi-container приложение (Уровень: ⭐⭐)

Задача: Создайте Flask API с PostgreSQL и Redis. API должен иметь 3 эндпоинта:

  • GET /health — health check
  • GET /visits — счетчик посещений (Redis)
  • GET /users — список из БД (PostgreSQL)

Требования:

  • Health checks для всех сервисов
  • depends_on с условиями (не просто - db)
  • Приложение не стартует, пока БД не готова
  • Volumes для постоянства данных
  • Один network для всех сервисов

Критерии проверки:

# 1. Запустить
docker compose up -d
 
# 2. Проверить health checks
docker compose ps
# Все сервисы должны быть healthy
 
# 3. Тестировать эндпоинты
curl http://localhost:5000/health
# {"status": "healthy"}
 
curl http://localhost:5000/visits
# {"visits": 1}
 
curl http://localhost:5000/visits
# {"visits": 2}
 
curl http://localhost:5000/users
# [{"id": 1, "name": "Alice"}, ...]
 
# 4. Проверить persist данных
docker compose down
docker compose up -d
curl http://localhost:5000/visits
# {"visits": 3}  ← Счетчик сохранился!
 
# 5. Очистка
docker compose down -v
💡 Подсказка
  • Используйте depends_on: db: condition: service_healthy
  • Health check для PostgreSQL: pg_isready -U postgres
  • Health check для Redis: redis-cli ping
  • Redis для счетчика: INCR visits
  • PostgreSQL: создайте таблицу в init.sql
✅ Решение

app.py:

from flask import Flask, jsonify
import psycopg2
import redis
import os
 
app = Flask(__name__)
 
# Redis connection
r = redis.from_url(os.getenv('REDIS_URL', 'redis://redis:6379/0'))
 
# PostgreSQL connection
def get_db():
    return psycopg2.connect(os.getenv('DATABASE_URL'))
 
@app.route('/health')
def health():
    try:
        # Check Redis
        r.ping()
        # Check PostgreSQL
        conn = get_db()
        conn.close()
        return jsonify({"status": "healthy"})
    except:
        return jsonify({"status": "unhealthy"}), 500
 
@app.route('/visits')
def visits():
    count = r.incr('visits')
    return jsonify({"visits": count})
 
@app.route('/users')
def users():
    conn = get_db()
    cur = conn.cursor()
    cur.execute('SELECT id, name FROM users')
    users = [{"id": row[0], "name": row[1]} for row in cur.fetchall()]
    conn.close()
    return jsonify(users)
 
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

docker-compose.yml:

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/app_db
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: app_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app-network
 
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    command: redis-server --appendonly yes
    networks:
      - app-network
 
volumes:
  postgres_data:
  redis_data:
 
networks:
  app-network:
    driver: bridge

init.sql:

CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);
 
INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');

Объяснение:

  • condition: service_healthy — web ждет готовности БД
  • Health checks — гарантируют готовность сервисов
  • Named volumes — данные переживают docker compose down
  • Single network — все сервисы видят друг друга

Упражнение 3: Multi-stage Build (Уровень: ⭐⭐)

Задача: Оптимизируйте Node.js приложение с 800MB до < 150MB используя multi-stage build.

Исходный (плохой) Dockerfile:

FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

Требования:

  • Итоговый образ < 150MB
  • Только production зависимости
  • Alpine Linux в production stage
  • Непривилегированный пользователь

Критерии проверки:

# Собрать плохой вариант
docker build -t node-bad -f Dockerfile.bad .
docker images node-bad
# ~800MB 💀
 
# Собрать оптимизированный
docker build -t node-optimized .
docker images node-optimized
# ~120-140MB ✅
 
# Проверить, что работает
docker run -d -p 3000:3000 --name test-node node-optimized
curl http://localhost:3000
# Должен отвечать
 
docker rm -f test-node
✅ Решение
# ========================================
# Stage 1: Builder
# ========================================
FROM node:20 AS builder
 
WORKDIR /app
 
COPY package*.json ./
RUN npm ci --only=production
 
COPY . .
 
# ========================================
# Stage 2: Production
# ========================================
FROM node:20-alpine AS production
 
WORKDIR /app
 
# Копируем только node_modules и код
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app .
 
# Создаем пользователя
RUN addgroup -g 1000 appuser && \
    adduser -D -u 1000 -G appuser appuser && \
    chown -R appuser:appuser /app
 
USER appuser
 
EXPOSE 3000
 
ENV NODE_ENV=production
 
CMD ["node", "server.js"]

Результат:

  • Builder stage: ~800MB (выбрасывается!)
  • Production stage: ~120MB ✅
  • Экономия: ~680MB (85% меньше)

Почему это работает:

  • node:20 (builder) — содержит build tools
  • node:20-alpine (production) — только runtime
  • npm ci --only=production — без devDependencies
  • COPY --from=builder — только необходимые файлы

Упражнение 4: Production Security Checklist (Уровень: ⭐⭐⭐)

Задача: Подготовьте Docker приложение к production деплою, следуя security best practices.

Исходный (небезопасный) docker-compose.yml:

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - SECRET_KEY=hardcoded-secret-12345
      - DATABASE_PASSWORD=admin123

Требования:

  • Сканирование образа (Trivy)
  • Секреты через Docker Secrets или .env (не hardcoded)
  • Непривилегированный пользователь
  • Health checks
  • Resource limits
  • Read-only filesystem (где возможно)
  • Минимальные capabilities

Критерии проверки:

# 1. Сканировать образ
trivy image my-app:latest --severity HIGH,CRITICAL
# 0 критических уязвимостей
 
# 2. Проверить секреты
docker history my-app:latest | grep -i secret
docker history my-app:latest | grep -i password
# Ничего не найдено ✅
 
# 3. Проверить пользователя
docker inspect my-app:latest | jq '.[].Config.User'
# != "root" ✅
 
# 4. Проверить health check
docker compose ps
# STATUS: healthy ✅
 
# 5. Проверить resource limits
docker inspect app | jq '.[].HostConfig.Memory'
# Должен быть установлен лимит
 
# 6. Проверить capabilities
docker inspect app | jq '.[].HostConfig.CapDrop'
# ["ALL"] ✅
✅ Решение

.env: (НЕ коммитить в Git!)

SECRET_KEY=generate-random-secret-key-here
DATABASE_PASSWORD=strong-password-here

.gitignore:

.env
*.secret

.env.example: (коммитить)

SECRET_KEY=your-secret-key-here
DATABASE_PASSWORD=your-db-password-here

docker-compose.yml:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    env_file:
      - .env
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2G
        reservations:
          cpus: "0.5"
          memory: 512M
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp:size=100M

Dockerfile:

FROM python:3.11-slim@sha256:abc123...  # Exact digest
 
WORKDIR /app
 
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app
 
USER appuser
 
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8000/health || exit 1
 
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

Чек-лист выполнен:

  • ✅ Секреты в .env (не в коде)
  • ✅ .env в .gitignore
  • ✅ Unprivileged user
  • ✅ Health checks
  • ✅ Resource limits
  • ✅ Read-only filesystem
  • ✅ Minimal capabilities
  • ✅ Exact image digest
  • ✅ Security scan (Trivy)

Следующие шаги: Попробуйте объединить все упражнения в одно комплексное приложение. Создайте микросервисную архитектуру с несколькими сервисами, соблюдая все security best practices и используя multi-stage builds.

Заключение

Docker — это не просто инструмент, это новый способ мышления о деплое и окружении. Один раз настроив Docker, вы сэкономите сотни часов на «у меня не работает».

Ключевые выводы

  1. Docker решает проблему окружения — «works on my machine» больше не оправдание
  2. Dockerfile = рецепт образа — каждая инструкция создает слой
  3. Docker Compose = оркестрация — управление несколькими контейнерами через YAML
  4. Volumes сохраняют данные — контейнеры эфемерны, volumes постоянны
  5. Один контейнер = один процесс — не запускайте всё в одном контейнере
  6. Разработка ≠ Production — используйте разные конфигурации
  7. Оптимизация важна — правильный порядок инструкций ускоряет сборку в разы

Следующие шаги

  1. Контейнеризируйте существующий проект
  2. Изучите Docker Swarm или Kubernetes для оркестрации в продакшн
  3. Настройте CI/CD с автоматической сборкой образов
  4. Изучите мониторинг контейнеров (Prometheus, Grafana)
  5. Погрузитесь в безопасность Docker

Полезные ресурсы

Официальная документация:

Практика:

Инструменты:

  • Dive — анализ слоев Docker образов
  • Hadolint — линтер для Dockerfile
  • Docker Slim — минимизация образов

Помните: Docker — это инструмент. Он не сделает плохую архитектуру хорошей, но сделает хорошую архитектуру проще в деплое и масштабировании. Начните с малого, экспериментируйте и постепенно внедряйте в свой рабочий процесс.