Основы окружений
~40 мин

Docker

Dockerfile, docker-compose, volumes, networks, multi-stage builds.

Docker — чтобы «у меня работает» работало везде

«У меня работает» — худшая фраза в ML-инженерии. Ты обучил модель на Ubuntu с CUDA 11.8 и Python 3.10. Коллега клонирует репозиторий на macOS — у него другая версия NumPy, нет CUDA, и модель падает с криптичным segfault. На проде CentOS с CUDA 12.1 — и всё ломается третьим, совершенно новым способом.

Docker решает эту проблему раз и навсегда: ты упаковываешь код, зависимости, конфигурации и runtime в контейнер — изолированную среду, которая запускается одинаково на любой машине. Твой ноутбук, сервер коллеги, Kubernetes-кластер в облаке — контейнер ведёт себя идентично. Это не магия, а изоляция через механизмы ядра Linux (namespaces, cgroups).

Для ML-инженера Docker — не опциональный инструмент, а обязательный. Без него невозможно воспроизводимое обучение, CI/CD пайплайн, деплой модели в прод. На собеседованиях вопрос «расскажи про Docker» — это не про DevOps, это про зрелость инженера.

Большая картина: от Dockerfile до продакшена

Весь Docker сводится к четырём понятиям, каждое из которых вытекает из предыдущего:

1. Image (образ) — неизменяемый шаблон. Это «снапшот» файловой системы: ОС + библиотеки + твой код + конфигурации. Образ не изменяется после сборки — он read-only. Аналогия: ISO-файл с операционной системой. Ты не запускаешь ISO напрямую — ты устанавливаешь с него. 2. Container (контейнер) — запущенный экземпляр image. Это живой процесс с изолированной файловой системой, сетью и процессами. Из одного image можно запустить 10 контейнеров — они не мешают друг другу. Контейнер живёт, пока работает его главный процесс. 3. Registry — хранилище образов. Как GitHub для кода, только для Docker-образов. Docker Hub, GitHub Container Registry (ghcr.io), AWS ECR. Ты пушишь образ в registry, а CI/CD или Kubernetes тянет его оттуда. 4. Orchestration — управление множеством контейнеров. Kubernetes (K8s), Docker Swarm, ECS. Когда у тебя 50 контейнеров на 10 серверах, нужен кто-то, кто следит за здоровьем, масштабирует и рестартит упавшие.

Загрузка интерактивного виджета...

Типичный путь ML-проекта: Dockerfile описывает, как собрать образ → docker build создаёт imagedocker run запускает containerdocker push отправляет образ в registry → Kubernetes или CI/CD тянет образ и запускает его на проде.

Dockerfile — рецепт для сборки образа

Dockerfile — это текстовый файл с инструкциями, которые Docker выполняет сверху вниз. Каждая инструкция создаёт новый слой (layer) в образе. Слои кешируются: если строка не изменилась — слой берётся из кеша. Это ключевая оптимизация, и порядок инструкций имеет значение.

Основные инструкции: • FROM — базовый образ. Всё начинается с него. FROM python:3.12-slim — минимальный Python-образ. • WORKDIR — рабочая директория внутри контейнера (аналог cd). • COPY — копирует файлы с хоста в контейнер. COPY requirements.txt . — из контекста сборки. • RUN — выполняет команду при сборке. RUN pip install -r requirements.txt — устанавливает зависимости. • CMD — команда при запуске контейнера. Может быть переопределена при docker run. • ENTRYPOINT — фиксированная точка входа. CMD дописывается к ENTRYPOINT как аргументы. • EXPOSE — документирует порт (не открывает! Открывает -p при docker run). • ENV — переменная окружения.

Вот Dockerfile для ML-сервиса — модель на FastAPI:

FROM python:3.12-slim
WORKDIR /app

# Сначала зависимости — этот слой кешируется, пока requirements.txt не изменится
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Потом код — при каждом изменении перестраивается ТОЛЬКО этот слой
COPY . .

# Non-root пользователь (безопасность)
RUN useradd -m appuser
USER appuser

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

💡 CMD vs ENTRYPOINT — в чём разница?

CMD — команда «по умолчанию». Полностью заменяется при docker run image <другая команда>. ENTRYPOINT — фиксированная точка входа. То, что передаётся в docker run, становится аргументами ENTRYPOINT. Пример: ENTRYPOINT ["python", "train.py"] + CMD ["--epochs", "10"]. docker run imagepython train.py --epochs 10 docker run image --epochs 50python train.py --epochs 50 Обычный паттерн: ENTRYPOINT для исполняемого файла, CMD для аргументов по умолчанию.

💡 Порядок COPY критичен для кеша

Docker кеширует каждый слой. Если requirements.txt не менялся — слой с pip install берётся из кеша мгновенно. Поэтому COPY requirements.txt . идёт ДО COPY . . — изменения в коде не инвалидируют кеш зависимостей. Это экономит минуты на каждом билде.

Docker Compose — несколько контейнеров одной командой

ML-проект — это не один контейнер. Модель обслуживает API, предсказания кешируются в Redis, метаданные лежат в PostgreSQL. Поднимать каждый контейнер руками (docker run ... × 3) — мучение. Docker Compose описывает всю инфраструктуру в одном YAML-файле и управляет ей одной командой.

# docker-compose.yml — ML-сервис + Redis + PostgreSQL
services:
  api:
    build: .
    ports: ["8000:8000"]
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mldb
      - REDIS_URL=redis://redis:6379
      - MODEL_PATH=/models/model.onnx
    volumes:
      - ./models:/models       # модель на хосте, не в образе
    depends_on: [db, redis]

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mldb
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  pgdata:                       # именованный volume — данные переживут docker compose down

Ключевые команды: • docker compose up -d — поднять все сервисы в фоне • docker compose ps — статус контейнеров • docker compose logs api — логи конкретного сервиса • docker compose down — остановить и удалить контейнеры (volumes сохраняются) • docker compose build && docker compose up -d — пересобрать и перезапустить (НЕ restart! Restart не подтягивает новый образ)

Обрати внимание: сервисы обращаются друг к другу по имениdb, redis, не по IP. Docker Compose автоматически создаёт сеть, где каждый сервис доступен по hostname, совпадающему с его именем в YAML. Это DNS-резолвинг через встроенный Docker DNS.

Volumes и Networking — persistence и связь контейнеров

Volumes (тома) решают проблему данных. По умолчанию всё внутри контейнера — эфемерно: остановил контейнер — данные пропали. Volumes — это постоянное хранилище, которое живёт отдельно от контейнера.

Два типа: • Named volumes — управляются Docker: volumes: [pgdata:/var/lib/postgresql/data]. Docker сам решает, где хранить данные на хосте. Идеальны для баз данных. • Bind mounts — привязка директории хоста: volumes: [./models:/models]. Файлы на хосте = файлы в контейнере. Удобно для разработки и для моделей — обновляешь файл на хосте, контейнер видит изменения сразу.

Networking. Docker Compose создаёт одну сеть для всех сервисов проекта. Контейнеры внутри неё видят друг друга по имени сервиса, но изолированы от внешнего мира (если не опубликованы порты через ports). Можно создавать несколько сетей для изоляции: frontend-сеть для API + nginx, backend-сеть для API + DB.

# Пример с двумя сетями: API видит и фронт, и БД. Фронт не видит БД.
services:
  nginx:
    image: nginx
    networks: [frontend]
  api:
    build: .
    networks: [frontend, backend]  # API — «мост» между сетями
  db:
    image: postgres:16
    networks: [backend]            # БД недоступна из frontend

networks:
  frontend:
  backend:

Best practices — чтобы образы были быстрыми и лёгкими

Multi-stage builds — паттерн, который уменьшает итоговый образ в 2-5 раз. Идея: первый этап (builder) устанавливает зависимости с gcc и прочими инструментами сборки, второй этап (runtime) копирует только готовые пакеты в чистый образ. Всё «мусор сборки» остаётся в первом этапе.

# Этап 1: builder — тяжёлый, с gcc и dev-зависимостями
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Этап 2: runtime — чистый, лёгкий образ
FROM python:3.12-slim
COPY --from=builder /install /usr/local
WORKDIR /app
COPY . .
RUN useradd -m appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Ещё несколько практик, которые отличают продакшн-образ от «собрал на коленке»:

  • .dockerignore — исключи .git, __pycache__, .venv, data/, notebooks/, *.pyc. Без него COPY . . копирует гигабайты мусора в контейнер
  • slim/alpine базовые образыpython:3.12-slim (~150MB) вместо python:3.12 (~1GB). Меньше размер = быстрее pull + меньше уязвимостей
  • Non-root пользовательRUN useradd -m appuser && USER appuser. Контейнер не должен работать от root — это базовая безопасность
  • Один процесс = один контейнер — API отдельно, Celery worker отдельно, не в одном контейнере. Это упрощает масштабирование и мониторинг
  • HEALTHCHECKHEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1. Docker и K8s узнают, жив ли сервис, и перезапустят при проблемах
  • Логи в stdout/stderr — Docker и K8s подхватывают их автоматически. Не пиши в файлы внутри контейнера
  • Не храни секреты в Dockerfile — пароли и ключи передавай через environment или Docker secrets, не через ENV в Dockerfile (они видны в docker history)

Docker для ML — GPU, тяжёлые образы и serving моделей

ML-контейнеры имеют свою специфику, которая отличает их от обычных веб-сервисов.

GPU в Docker. По умолчанию контейнер не видит GPU. Для доступа к видеокарте нужен NVIDIA Container Toolkit (раньше назывался nvidia-docker). После его установки контейнер запускается с флагом --gpus all (или --gpus device=0 для конкретной карты). В Compose — deploy.resources.reservations.devices.

# docker-compose.yml — ML-сервис с GPU
services:
  inference:
    build: .
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1              # или "all" для всех GPU
              capabilities: [gpu]
    volumes:
      - ./models:/models

Тяжёлые образы — боль ML. Базовый nvidia/cuda:12.1-runtime весит ~3.5GB, добавь PyTorch (~2GB) и зависимости — легко получается 8-10GB. Как бороться: • Multi-stage builds — отделяй сборку от runtime • Модель НЕ в образе — монтируй через volume или скачивай при старте из S3/GCS. Иначе каждое обновление модели = пересборка образа • Минимизируй слои RUN — объединяй apt-get install && apt-get clean в один слой • Используй .dockerignore — не тащи датасеты в контейнер

Model serving. В проде модель обслуживается через HTTP API. Типичные инструменты: FastAPI/Uvicorn (простой вариант), Triton Inference Server (NVIDIA, батчинг и multi-model), TorchServe, BentoML. Контейнер с моделью — это stateless сервис: модель загружается при старте, запросы обрабатываются без изменения состояния. Это позволяет масштабировать горизонтально — запустил 5 реплик за балансировщиком.

Docker vs VM — в чём разница и когда что

Docker — не виртуальная машина, хотя со стороны похоже. Ключевое отличие: VM эмулирует полный компьютер с собственным ядром ОС, а контейнер разделяет ядро хоста. Это делает контейнеры быстрыми и лёгкими, но менее изолированными.

  • Запуск: контейнер стартует за секунды (это просто процесс), VM — за минуты (загрузка ОС)
  • Размер: образ контейнера ~100MB-2GB, образ VM ~10-40GB
  • Изоляция: VM — полная (отдельное ядро), контейнер — на уровне namespaces (одно ядро с хостом)
  • Производительность: контейнер — почти нативная (нет гипервизора), VM — overhead виртуализации
  • Когда VM: нужна полная изоляция (multi-tenant, разные ОС), legacy-приложения, Windows на Linux
  • Когда Docker: микросервисы, CI/CD, ML-пайплайны, dev-окружения — почти всегда

В ML-практике: обучение и инференс — контейнеры (Docker/K8s). VM используются как хосты, на которых крутятся контейнеры (EC2 instance → Docker → твой сервис). На собеседовании достаточно знать: «контейнер — изоляция на уровне процессов, разделяет ядро с хостом; VM — полная виртуализация с отдельным ядром».

Как это выглядит в реальной работе

Разработка (локально): docker compose up поднимает API + PostgreSQL + Redis. Разрабатываешь код, тестируешь. Bind mount (./src:/app/src) позволяет видеть изменения без пересборки образа. CI/CD: при merge в main CI-сервер (GitHub Actions, GitLab CI) запускает docker build, прогоняет тесты внутри контейнера, пушит образ в registry с тегом (commit SHA или version). Прод: Kubernetes тянет образ из registry, запускает нужное количество реплик, следит за здоровьем через health checks. Модель подгружается из S3 при старте контейнера, а не вшита в образ — обновление модели не требует пересборки.

🎯 На собеседовании

Junior

Что такое Docker и зачем он нужен? Инструмент для упаковки приложения с зависимостями в изолированный контейнер. Решает проблему «у меня работает» — контейнер запускается одинаково везде. • Image vs Container? Image — неизменяемый шаблон (read-only). Container — запущенный экземпляр image. Из одного image можно создать много контейнеров. • Что такое Dockerfile? Текстовый файл с инструкциями для сборки образа: FROM (базовый образ), COPY (файлы), RUN (команды при сборке), CMD (команда при запуске). • Docker vs VM? Контейнер разделяет ядро с хостом (быстрый, лёгкий), VM — полная виртуализация с собственным ядром (тяжёлая, но полная изоляция).

Middle

Как упаковать ML-модель в Docker? Multi-stage build: builder устанавливает зависимости, runtime — чистый образ. Модель через volume или S3, не вшита в image. Non-root user, healthcheck, .dockerignore. • CMD vs ENTRYPOINT? ENTRYPOINT — фиксированная команда, CMD — аргументы по умолчанию. При docker run CMD заменяется, ENTRYPOINT — нет. • Как оптимизировать размер образа? Multi-stage build, slim-базы, .dockerignore, объединение RUN-команд, очистка кешей pip/apt. • Зачем volumes? Данные внутри контейнера эфемерны. Volumes сохраняют данные между перезапусками. Named volumes для БД, bind mounts для dev-окружения и моделей.

Senior

Как Docker видит GPU? NVIDIA Container Toolkit + --gpus флаг. В Compose — deploy.resources.reservations.devices. Без Toolkit контейнер не видит видеокарту. • Как организовать Docker-сеть для микросервисов? Отдельные networks для изоляции (frontend/backend). Сервисы общаются по имени через встроенный DNS. Публикуются только нужные порты. • Layer caching — как работает и как сломать? Каждая инструкция = слой. Если строка изменилась — все последующие слои пересобираются. COPY requirements.txt до COPY . — зависимости не пересобираются при изменении кода. • Безопасность контейнеров? Non-root user, минимальные базовые образы (меньше attack surface), секреты через Docker secrets (не ENV), read-only fs (--read-only), security scanning (Trivy, Snyk).

Собираем всё вместе

Docker — это инструмент, который превращает «у меня работает» в «у всех работает». Dockerfile описывает среду, docker build создаёт образ, docker run запускает контейнер, registry хранит образы для деплоя. Docker Compose оркестрирует несколько сервисов. Volumes сохраняют данные, networks обеспечивают связь.

Если запомнить одну вещь из этой ноды: Docker = воспроизводимая среда в одной команде. Это фундамент CI/CD, деплоя и командной работы. ML без Docker — это возврат в эру «скинь мне свой requirements.txt и помолимся».

Дальше на роадмапе: MLOps покажет, как Docker вписывается в полный ML-пайплайн, а Model Serving расскажет про инференс в контейнерах — Triton, TorchServe, масштабирование.