Docker: контейнеризация без магии

dockerdevopscontainers

Когда я впервые столкнулся с Docker лет пять назад, он казался мне какой-то чёрной магией. Контейнер — это виртуалка? Не совсем. Процесс? Тоже не точно. Образ — это снимок диска? Вроде бы, но не классический. Со временем пришло понимание, что Docker — это не магия, а набор вполне конкретных механизмов Linux: namespaces, cgroups, union filesystem. И когда разберёшься в основах, всё встаёт на свои места.

В этой статье разберём Docker с практической стороны — от написания Dockerfile до отладки падающих контейнеров.

Как устроен Docker

На верхнем уровне архитектура Docker выглядит так: клиент (docker CLI) общается с демоном (dockerd), который управляет образами, контейнерами, сетями и томами. Образ — это набор read-only слоёв файловой системы (union FS), а контейнер добавляет поверх них тонкий writable-слой.

Архитектура Docker: слои образа, контейнер и взаимодействие компонентов

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

Dockerfile: пишем правильно

Начнём с типичной ошибки — Dockerfile для Python-приложения, который делает всё «в лоб»:

FROM python:3.12
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "main.py"]

Работает? Да. Проблемы? Полно. Любое изменение в коде инвалидирует кэш слоя COPY . ., и зависимости будут ставиться заново. Финальный образ тянет за собой весь build-инструментарий. Размер — полгигабайта на ровном месте.

Вот как это должно выглядеть:

# --- Build stage ---
FROM python:3.12-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

COPY . .

# --- Runtime stage ---
FROM python:3.12-slim

WORKDIR /app

COPY --from=builder /install /usr/local
COPY --from=builder /app .

RUN useradd -r -s /bin/false appuser
USER appuser

HEALTHCHECK --interval=30s --timeout=3s --retries=3 
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["python", "main.py"]

Что здесь важно:

  • Multi-stage build — разделяем сборку и рантайм. В финальный образ попадает только то, что нужно для запуска.
  • Порядок COPY — сначала requirements.txt, потом pip install, и только потом COPY . .. Зависимости кэшируются отдельно от кода.
  • slim-образpython:3.12-slim вместо полного python:3.12. Экономим 600+ МБ.
  • Non-root user — контейнер не работает из-под root. Базовая безопасность.
  • HEALTHCHECK — Docker знает, жив ли контейнер на самом деле, а не просто «процесс запущен».

.dockerignore

Не забывайте про .dockerignore. Без него COPY . . затянет в контекст сборки .git, node_modules, __pycache__ и прочий мусор:

.git
.gitignore
__pycache__
*.pyc
.env
.venv
docker-compose*.yml
Dockerfile
README.md

Docker Compose для локальной разработки

Для локальной среды docker-compose — незаменимая вещь. Вот реалистичный пример для бэкенда на FastAPI с PostgreSQL и Redis:

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: builder  # используем build-stage для dev
    volumes:
      - .:/app  # live reload
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://app:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379/0
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"]

  db:
    image: postgres:16-alpine
    volumes:
      - pg_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 5

  cache:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  pg_data:
  redis_data:

Стек сервисов Docker Compose: взаимодействие API, базы данных и кэша

Обратите внимание на depends_on с condition: service_healthy. Без этого ваш API может стартовать раньше, чем PostgreSQL будет готов принимать соединения. Классическая проблема, которая вылезает раз в пять запусков и отнимает кучу времени на дебаг.

Сети: bridge, host, overlay

Docker создаёт несколько типов сетей:

  • bridge (по умолчанию) — изолированная сеть для контейнеров на одном хосте. Каждый compose-проект получает свою bridge-сеть. Контейнеры внутри одной сети обращаются друг к другу по имени сервиса.
  • host — контейнер использует сетевой стек хоста напрямую. Полезно для максимальной производительности сети, но теряется изоляция.
  • overlay — сеть между несколькими Docker-хостами (Swarm). На практике, если вам нужен multi-host, скорее всего вы уже смотрите в сторону Kubernetes.

Частый вопрос: как связать контейнеры из разных compose-файлов? Самый чистый способ — создать внешнюю сеть:

docker network create shared-net

И подключить к ней оба проекта:

services:
  api:
    networks:
      - shared-net

networks:
  shared-net:
    external: true

Тома: named volumes vs bind mounts

Два типа монтирования, два сценария:

  • Bind mounts (./src:/app/src) — для разработки. Код на хосте синхронизирован с контейнером, работает hot reload. Но производительность на macOS с Docker Desktop оставляет желать лучшего (используйте VirtioFS или consistency: cached).
  • Named volumes (pg_data:/var/lib/postgresql/data) — для данных. Docker управляет хранением, данные переживают пересоздание контейнера, работает быстро на всех платформах.

Золотое правило: исходный код — через bind mount, данные БД и кэши — через named volumes.

Отладка контейнеров

Когда что-то идёт не так (а оно идёт), есть набор команд, который спасает:

# Логи контейнера (последние 100 строк, follow)
docker logs --tail 100 -f container_name

# Зайти внутрь работающего контейнера
docker exec -it container_name /bin/sh

# Ресурсы (CPU, RAM, I/O) в реальном времени
docker stats

# Детальная информация о контейнере (конфиги, маунты, сети)
docker inspect container_name

# Почему контейнер упал?
docker inspect --format='{{.State.ExitCode}}' container_name
docker inspect --format='{{.State.OOMKilled}}' container_name

Отдельно про OOMKilled — если контейнер молча падает без ошибок в логах, почти наверняка он вылетел по памяти. Ставьте лимиты в compose через deploy.resources.limits.memory и мониторьте через docker stats.

services:
  api:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.5"

Чего лучше избегать

Несколько антипаттернов, которые я встречаю регулярно:

  1. Хранить данные в контейнере без volume. Контейнер пересоздался — данные потеряны. Это не баг, это by design.
  2. Запускать всё от root. Уязвимость в приложении = root-доступ в контейнере. Всегда USER non-root.
  3. Не ограничивать ресурсы. Один контейнер без лимитов может положить весь хост.
  4. Использовать latest тег в продакшене. Сегодня latest — это v2.1, завтра — v3.0 с breaking changes. Пиннинг версий обязателен.
  5. Игнорировать .dockerignore. Контекст сборки в 2 ГБ — это реальность, если в проекте есть node_modules или .git.

Итог

Docker — это не сложно, если понять несколько ключевых концепций: слои и кэширование, разделение build/runtime, сети и тома. Multi-stage builds, healthcheck-и и ограничение ресурсов — это не «продвинутые техники», а базовая гигиена. Начните с этого, а уже потом переходите к оркестрации — будь то Compose для одного сервера или Kubernetes для кластера.

© 2026 Terminal Notes. Built with SvelteKit.