Когда использовать Docker
Контейнеры — это инструмент, а не серебряная пуля. Они отлично решают одни задачи и плохо подходят для других. В этом уроке посмотрим конкретные DE-сценарии, где Docker почти всегда правильный выбор, и пограничные случаи, где он становится anti-pattern’ом.
Цель урока — чтобы после него у тебя в голове был чек-лист: «если задача такая — берём Docker без вопросов; если такая — берём bare-metal или managed-сервис; если такая — берём k8s, не Docker напрямую».
DE-кейсы, где Docker — стандарт
Кейс 1: воспроизводимая среда
Команда разрабатывает ETL pipeline. Все используют Postgres 16.4. Без Docker это означало бы: кто-то на Mac ставит через brew, кто-то на Linux через apt, кто-то на WSL — и версии разные (16.2, 16.4, 16.5), config разный, locale разный. Через две недели появляется баг «у меня работает, у тебя нет».
С Docker:
docker run -d --name pg -p 5432:5432 \
-e POSTGRES_PASSWORD=secret \
postgres:16.4
У всех в команде — байт-в-байт одна и та же Postgres. У этого образа фиксированный SHA. Image pull с Docker Hub приносит ровно тот файл, что лежит на сервере.
Это и есть «воспроизводимая среда». Не для prod (там обычно managed Postgres от облака), а для dev и CI.
Кейс 2: локальная разработка DAG’ов Airflow / dbt
Airflow — сложный софт. Чтобы он работал, нужны Postgres (метастор), Redis или Celery (очередь), плюс сам Airflow webserver, scheduler, worker. Без Docker поднимать это локально — день работы и зоопарк конфигов.
С Docker Compose это compose up и через 30 секунд у тебя локальный Airflow на http://localhost:8080. С тем же Python (например, 3.12), теми же providers (например, apache-airflow-providers-postgres 5.x), теми же DAG-файлами, что и в проде.
То же для dbt: dbt-postgres контейнер с твоей dbt-моделью, тот же подключаемый Postgres, тот же seed. Локальный run в один шаг.
Свой systemd-сервис: пишем .service unit для Python ETL daemon
Кейс 3: integration tests через testcontainers
Тебе нужно проверить, что твой pipeline корректно пишет в Postgres. Варианты:
- Mock базу. Тест проходит, но не доказывает ничего. На проде ловишь баг.
- Shared CI Postgres. Тесты конфликтуют (один тест видит данные другого), флаки, страдания.
- Контейнер на лету.
testcontainers-pythonподнимает Postgres именно для этого теста. Test проходит, контейнер уходит, нет следов.
Пример:
import psycopg
from testcontainers.postgres import PostgresContainer
def test_etl_writes_correctly():
with PostgresContainer("postgres:16.4") as pg:
conn = psycopg.connect(pg.get_connection_url())
# ... твой ETL код ...
# ... проверка через SELECT ...
Каждый тест — свой контейнер. Параллельно тесты не пересекаются. На CI работает out of the box, потому что GitHub Actions runner — это уже Docker-совместимая среда.
Подробнее testcontainers будет в модуле 18.
Кейс 4: CI image как артефакт
Современный DE pipeline в CI выглядит так:
- Commit -> GitHub Actions.
- Actions собирает Docker image с твоим Python-кодом (ETL + зависимости).
- Image пушится в registry (GHCR, ECR, Harbor).
- В Airflow задача — это
KubernetesPodOperator(image="ghcr.io/myorg/etl-job:v1.2.3"). - В нужное время k8s поднимает контейнер из этого image, запускает джоб.
Преимущество: один артефакт от коммита до prod. Версия Python зафиксирована в image, версии зависимостей зафиксированы в image, твой код зафиксирован в image. Деплой — это смена тэга image.
Это совсем не возможно без контейнеров. Без них пришлось бы либо ставить зависимости на каждый Airflow worker (тогда worker’ы становятся stateful), либо упаковывать всё в один большой Python wheel (хрупкое и сложно поддерживаемое).
Кейс 5: sandbox для экспериментов
Хочешь попробовать новую базу — Clickhouse, DuckDB, DragonflyDB? Без Docker — это часы на установку, потом разбор с конфигурацией, потом мучение с удалением. С Docker:
docker run --rm -it clickhouse/clickhouse-server
# поиграл, прервал Ctrl+C — всё ушло
Никаких следов на хосте. Идеально для «надо посмотреть, не подходит — забываем».
Когда Docker — anti-pattern
Анти-кейс 1: stateful production без orchestrator
Распространённый ход для junior’а — поднял Postgres в Docker на VPS, и думает, что это prod. Проблемы:
- Host upgrade. Когда VPS перезагружают для обновления ядра — контейнер исчезает. Если ты не настроил
--restart=unless-stoppedи volumes на правильный путь — данные могут потеряться. - Нет HA. Если VPS упал — Postgres недоступен, пока ты не перезапустишь руками.
- Нет автоматического failover.
- Бэкапы — вручную.
Production stateful система — это либо managed (RDS, Cloud SQL, Supabase, Neon), либо kubernetes с StatefulSet и оператором (CrunchyData PGO, Zalando postgres-operator). Чистый Docker — это локальная разработка, а не prod.
Анти-кейс 2: огромный compose-стенд в проде
Compose отлично работает для 5-15 сервисов на одной машине. Для 50 сервисов на 10 машинах — это уже Kubernetes.
В мире DE граница такая: один dev-стенд для команды — Compose ок. Production стек, в который попадает реальный трафик клиентов — k8s или managed-сервисы.
Анти-кейс 3: multi-tenant изоляция
Если ты хостишь чужой код (CI для public-репозиториев, serverless для внешних пользователей, sandbox для студентов) — простой Docker недостаточно. Контейнерная изоляция слабее VM, container escape через kernel-уязвимости возможен. Для multi-tenant нужны:
- gVisor — sandbox-runtime, который перехватывает syscalls.
- Kata Containers — лёгкие VM вместо контейнеров.
- Firecracker — то, что использует AWS Lambda и Fargate.
Junior DE редко это пишет, но знать стоит.
Анти-кейс 4: GPU без подготовки
ML inference на GPU в контейнере — это рабочая история, но требует nvidia-container-toolkit, правильных образов от NVIDIA, и понимания, как драйвер шарится между host и контейнером. Для серьёзной ML-инфраструктуры — обычно k8s + GPU operator. Для одиночной задачи на одной машине — иногда проще запустить bare-metal или взять managed-сервис (SageMaker, Vertex AI).
Чек-лист «Docker или нет»
Когда смотришь на новую задачу:
- Это локально / dev / CI? -> Почти всегда Docker. Compose для multi-service сценариев.
- Это production stateless (HTTP API, ETL worker)? -> Docker, но обычно через k8s или managed (ECS, Cloud Run, Fly.io).
- Это production stateful (база, очередь)? -> Не чистый Docker. Managed-сервис или k8s + operator.
- Это запуск чужого недоверенного кода? -> Не чистый Docker. gVisor, Firecracker, или managed serverless.
- Это интеграционный тест в CI? -> testcontainers. Это и есть Docker, но в удобной обёртке.
Попробуй сам
Если у тебя установлен Docker, сделай мини-эксперимент с воспроизводимостью:
docker run --rm postgres:16.4 postgres --version
docker run --rm postgres:16.5 postgres --version
Вторая команда сначала скачает образ 16.5, потом выведет точную версию. Обрати внимание: версия точно та, что в тэге. Никаких «приблизительно 16».
Дальше можно глянуть, как образ зафиксирован по digest:
docker inspect postgres:16.4 --format='{{index .RepoDigests 0}}'
Вывод будет вроде postgres@sha256:abc123.... Этот SHA — это контентный хэш всего образа. Если бы Docker Hub попытался незаметно подменить файлы, SHA бы изменился, и ты бы это заметил. Это и есть основа воспроизводимости.
Прибери (если ничего не оставалось):
docker image rm postgres:16.4 postgres:16.5
Что дальше
В следующем модуле — установка Docker и обзор альтернатив (Docker Engine vs Desktop vs OrbStack vs Rancher vs Podman). После того, как у тебя на машине что-то рабочее — переходим к первым контейнерам.