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