Learning Platform
Глоссарий Troubleshooting
Урок 17.05 · 24 мин
Средний
dockerpythonproductiondata-engineering

Production шаблон Python DE-образа

В этом уроке мы соберём всё, что прошли в модулях 6, 7, 14, 15, в один production-ready Dockerfile для Python ETL. 30-40 строк, с комментариями, чек-листом «production ready» в конце. Это шаблон, который ты копируешь в новый DE-проект.


Финальный шаблон

# syntax=docker/dockerfile:1.7

# ============================================================
# Stage 1: BUILDER — собирает зависимости
# ============================================================
FROM python:3.13.0-slim AS builder

# uv — быстрый менеджер зависимостей
COPY --from=ghcr.io/astral-sh/uv:0.4.27 /uv /usr/local/bin/uv

# Системные зависимости для компиляции native extensions
# (psycopg, numpy и т.д. часто требуют gcc + libpq-dev)
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc=4:12.* \
    libpq-dev=15.* \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /build

# venv в фиксированном пути для copy в runtime stage
ENV UV_PROJECT_ENVIRONMENT=/opt/venv
RUN uv venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH

# Копируем только requirements — отдельный слой для layer cache
COPY requirements.txt .

# BuildKit cache mount: persistent кэш между сборками,
# но не попадает в финальный образ
RUN --mount=type=cache,target=/root/.cache/uv \
    uv pip install --python /opt/venv/bin/python -r requirements.txt

# ============================================================
# Stage 2: RUNTIME — минимальный образ для запуска
# ============================================================
FROM python:3.13.0-slim AS runtime

# Метаданные образа (OCI labels)
LABEL org.opencontainers.image.title="my-etl"
LABEL org.opencontainers.image.description="ETL pipeline for warehouse"
LABEL org.opencontainers.image.source="https://github.com/myorg/my-etl"
LABEL org.opencontainers.image.licenses="Apache-2.0"

# Только runtime libs — libpq5 вместо libpq-dev
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5=15.* \
    && rm -rf /var/lib/apt/lists/*

# Non-root пользователь
RUN useradd -m -u 1000 -s /bin/bash app

WORKDIR /app

# Копируем venv с зависимостями из builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

# Код приложения с правильным владельцем
COPY --chown=app:app . .

USER app

# Healthcheck — приложение должно отвечать что оно живо
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import sys; sys.exit(0)" || exit 1

# JSON-form CMD для правильной обработки сигналов
CMD ["python", "main.py"]

40 строк, всё необходимое для production.


Разберём по строкам

# syntax=docker/dockerfile:1.7

Версия Dockerfile frontend для BuildKit. Без неё --mount=type=cache и --mount=type=secret могут не работать в старых Docker.

Stage 1: builder

FROM python:3.13.0-slim AS builder

Pin до patch-версии — воспроизводимость через год. slim — стандарт DE.

COPY --from=ghcr.io/astral-sh/uv:0.4.27 /uv /usr/local/bin/uv

uv пин на конкретную версию. Это один из секретов скорости — бинарь uv копируется готовым, без pip install.

RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc=4:12.* \
    libpq-dev=15.* \
    && rm -rf /var/lib/apt/lists/*

Build deps. Pin до major version (gcc=4:12.*). --no-install-recommends — не тянуть необязательные пакеты. rm -rf /var/lib/apt/lists/* в том же RUN — убираем apt cache из слоя. Все эти правила hadolint объяснял.

ENV UV_PROJECT_ENVIRONMENT=/opt/venv
RUN uv venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH

Создаём venv в фиксированном пути. /opt/venv чтобы потом скопировать одним COPY.

COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/uv \
    uv pip install --python /opt/venv/bin/python -r requirements.txt

Установка с BuildKit cache. requirements.txt отдельно от кода — layer cache для pip install.

Stage 2: runtime

LABEL org.opencontainers.image.title="my-etl"
LABEL org.opencontainers.image.description="..."
LABEL org.opencontainers.image.source="https://github.com/..."
LABEL org.opencontainers.image.licenses="Apache-2.0"

OCI standard labels. Помогают registry, docker scout, инструментам отслеживания. Не обязательны, но profession.

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5=15.* \
    && rm -rf /var/lib/apt/lists/*

Runtime-only libs: libpq5, не libpq-dev. Без gcc.

RUN useradd -m -u 1000 -s /bin/bash app

Non-root пользователь с предсказуемым UID 1000.

COPY --from=builder /opt/venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

Копируем готовый venv одним COPY. ENV-блок:

  • PATH=/opt/venv/bin:$PATH — Python из venv.
  • PYTHONUNBUFFERED=1 — stdout без буферизации (logs идут моментально).
  • PYTHONDONTWRITEBYTECODE=1 — не писать .pyc файлы (экономия диска, скорость старта).
COPY --chown=app:app . .
USER app

Код от app:app. USER переключает на non-root.

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import sys; sys.exit(0)" || exit 1

Healthcheck — раз в 30 секунд проверяем, что Python запускается. --start-period=10s — первые 10 секунд healthcheck не считается failure (приложению нужно стартовать). Если приложение имеет HTTP — лучше curl localhost:8000/health. Если CLI/cron-style — что-нибудь minimalistic.

CMD ["python", "main.py"]
Сигналы POSIX и почему JSON form CMD важен

JSON form (массив), не shell form (CMD python main.py). JSON form означает, что Python — это PID 1, получает signals напрямую (docker stop -> SIGTERM -> Python обрабатывает). Shell form запускает sh -c "python main.py", и signals идут в sh, который их не пробрасывает в Python.


requirements.txt

В простейшем виде:

# Pinned для воспроизводимости
pandas==2.2.3
sqlalchemy==2.0.36
psycopg[binary]==3.2.3
polars==1.18.0
boto3==1.35.50

Generate через pip-compile или uv:

uv pip compile requirements.in -o requirements.txt

Альтернативно — pyproject.toml + uv.lock (см. урок 3).


.dockerignore

Не забываем:

.env
.env.*
*.key
*.pem

.git
.gitignore

__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/

.venv/
venv/

.DS_Store
.idea/
.vscode/

docker-compose*.yml
Dockerfile*

tests/
docs/
*.md

Это исключит файлы из контекста сборки. Меньше байт на копирование, защита от случайного COPY секретов.


Сборка и проверка

docker build -t my-etl:1.0 .

# Размер
docker images my-etl:1.0
# ~220-250 МБ

# Запуск (с фейковыми ENV для теста)
docker run --rm \
  -e DATABASE_URL=postgresql://test:[email protected]:5432/test \
  my-etl:1.0 \
  python -c "import pandas, psycopg; print('OK')"

# Hadolint
docker run --rm -i hadolint/hadolint < Dockerfile

# Trivy
trivy image my-etl:1.0 --severity CRITICAL,HIGH

# Проверка user
docker run --rm my-etl:1.0 id
# uid=1000(app) gid=1000(app)

Чек-лист «production ready»

Дочитал до этого момента — пробегайся по списку перед PR в production.

Базовое

  • # syntax=docker/dockerfile:1.7 в самом верху.
  • FROM с pin’ом до patch-версии (python:3.13.0-slim, не python:3.13-slim).
  • Multi-stage: builder для сборки, runtime для запуска.
  • .dockerignore существует, исключает .env, .git, __pycache__.

Зависимости

  • requirements.txt (или pyproject.toml + uv.lock) с pin’нутыми версиями.
  • BuildKit --mount=type=cache для pip/uv cache.
  • requirements.txt копируется отдельно от кода (layer cache).
  • Build deps (gcc, libpq-dev) только в builder stage.
  • Runtime deps (libpq5) только в runtime stage.
  • --no-install-recommends для apt.
  • rm -rf /var/lib/apt/lists/* в том же RUN.

Безопасность

  • useradd -u 1000 app + USER app.
  • COPY --chown=app:app для кода.
  • chown перед USER, если используется RUN chown.
  • Нет COPY .env, нет ENV PASSWORD=, нет ARG SECRET=.
  • BuildKit --mount=type=secret для build-time секретов.
  • hadolint Dockerfile без warnings.
  • trivy image без CRITICAL.

Runtime

  • HEALTHCHECK задан.
  • CMD в JSON-form (["python", "main.py"]).
  • PYTHONUNBUFFERED=1 для логов.
  • PYTHONDONTWRITEBYTECODE=1 для производительности.
  • OCI LABEL’ы (org.opencontainers.image.*).

CI

  • hadolint в pre-commit.
  • trivy в CI с --exit-code 1 --severity CRITICAL,HIGH.
  • Сборка multi-arch (если нужно ARM64 + x86_64).
  • --cache-to type=registry для CI cache.

Production Dockerfile архитектура
builder stagepython:3.13.0-slim + uv + gcc + libpq-dev. Здесь всё для сборки, ничего лишнего
uv pip install в /opt/venvCache mount /root/.cache/uv для скорости. Установка в фиксированный путь
runtime stagepython:3.13.0-slim + libpq5 (без -dev). Минимум для запуска
COPY --from
/opt/venv копируетсяОдин COPY переносит все зависимости. Path-агностичный venv
non-root useruseradd 1000 app, USER app. Безопасность
HEALTHCHECK + LABELs + CMDProduction-grade: healthcheck, OCI labels, JSON CMD для signal handling

Compose-пример с этим образом

services:
  app:
    build: .
    image: my-etl:latest
    environment:
      DATABASE_URL: postgresql://de:secret@postgres:5432/warehouse
      S3_ENDPOINT_URL: http://minio:9000
      AWS_ACCESS_KEY_ID: admin
      AWS_SECRET_ACCESS_KEY: admin12345
    depends_on:
      postgres:
        condition: service_healthy
      minio:
        condition: service_started
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    restart: unless-stopped

  postgres:
    image: postgres:16.3
    # ... (см. модуль 13)

  minio:
    image: minio/minio:RELEASE.2026-04-15T19-23-45Z
    # ...

Образ деплоится в compose-стенд DE, healthcheck следит за живостью, restart policy unless-stopped перезапускает если упал но не если ты сам остановил.


Дополнительные шаги

CI: автоматическая сборка

GitHub Actions:

name: Build and push image

on:
  push:
    branches: [main]
    tags: ["v*"]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Lint Dockerfile
        run: docker run --rm -i hadolint/hadolint --failure-threshold warning < Dockerfile

      - name: Build
        uses: docker/build-push-action@v6
        with:
          tags: my-registry/my-etl:${{ github.sha }}
          cache-from: type=registry,ref=my-registry/my-etl:cache
          cache-to: type=registry,ref=my-registry/my-etl:cache,mode=max
          platforms: linux/amd64,linux/arm64

      - name: Scan with Trivy
        uses: aquasecurity/[email protected]
        with:
          image-ref: my-registry/my-etl:${{ github.sha }}
          severity: CRITICAL
          exit-code: 1

      - name: Push to registry
        if: success()
        run: docker push my-registry/my-etl:${{ github.sha }}

PR: lint -> build -> scan -> push. Только при чистом scan’е образ попадает в registry.

Версионирование

Tag по git describe или SHA:

TAG=$(git describe --tags --always --dirty)
docker build -t my-etl:$TAG .
docker tag my-etl:$TAG my-etl:latest

В production деплое — pin до конкретного TAG, не latest. В compose это:

services:
  app:
    image: my-registry/my-etl:v1.2.3

Не :latest. :latest в проде — это игра в рулетку.


Попробуй сам

  1. Возьми шаблон из этого урока, адаптируй под свой простой Python-проект (например, requirements.txt с pandas + psycopg, main.py с одним print’ом).
  2. Запусти docker build .. Замерь время.
  3. Сделай правку в main.py (только print). Запусти docker build . ещё раз — должно быть очень быстро (только последний слой ребилдится).
  4. Добавь зависимость в requirements.txt, пересобери. Должно быть быстрее чем cold build, но медленнее чем code-only ребилд.
  5. Пройдись по чек-листу «production ready». Поправь то, что у тебя не закрыто.
  6. Прогони hadolint Dockerfile и trivy image my-etl:test --severity CRITICAL,HIGH. Должно быть чисто.

Проверка знанийKnowledge check
В production-ready Dockerfile для Python ETL шесть критичных элементов: BuildKit syntax, multi-stage, BuildKit cache mount, non-root user, healthcheck, JSON form CMD. Почему каждый из этих шести важен — что произойдёт если убрать каждый?
ОтветAnswer
1) # syntax=docker/dockerfile:1.7 — без этого BuildKit фичи (--mount=type=cache, --mount=type=secret) могут не работать на старых Docker daemon. Сборка может пройти, но без преимуществ — кеш не сохранится, секреты могут утечь в слои. 2) Multi-stage — без него gcc, libpq-dev, python3-dev (300 МБ build-tools) попадают в финальный образ. Образ 1.2 ГБ вместо 220 МБ. CI медленнее, registry storage растёт, security attack surface больше. 3) BuildKit cache mount (--mount=type=cache,target=/root/.cache/pip) — без него каждый rebuild после изменения requirements.txt качает все пакеты с PyPI заново. 60 секунд вместо 5. На команде из 10 человек × 20 PR в день = часы простоя. 4) Non-root user (useradd + USER app) — без него приложение работает от UID 0 (root). При RCE атакующий имеет полный root в контейнере. На default Docker (не rootless) UID 0 в контейнере = UID 0 на хосте, что даёт путь к escape. Security audit fails. 5) HEALTHCHECK — без него Docker / Kubernetes / compose не знают, живо ли приложение. Если приложение зависло (не упало, но не отвечает), оркестратор не перезапустит. depends_on: service_healthy в compose не работает. Балансировщик отправляет траффик в дохлый контейнер. 6) JSON form CMD (CMD ["python", "main.py"]) vs shell form (CMD python main.py) — JSON form: Python — PID 1, получает SIGTERM напрямую от docker stop, может graceful shutdown. Shell form: PID 1 = /bin/sh -c "python main.py", sh не пробрасывает SIGTERM в child процесс. docker stop ждёт 10 секунд, потом SIGKILL — приложение убито грубо. Для DB-app это может означать незакоммиченные транзакции, повреждённые файлы, потерянные данные. Каждый элемент — реальное последствие. Шаблон с этого урока — это compressed знание модулей 6-15 в 40 строк.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. В production Dockerfile ENV PYTHONUNBUFFERED=1 и ENV PYTHONDONTWRITEBYTECODE=1. Что это делает и почему важно?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5