Docker: production best practices

Большинство Dockerfile в интернете — учебные. В продакшене они опасны. Этот урок показывает как делают зрелые команды.

10 правил продакшен-Dockerfile

1. Multi-stage build — всегда

Отделяй build-зависимости от runtime. Финальный образ — только то, что нужно для запуска.

FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]

Результат: ~15 MB образ вместо 800+ MB с Go тулчейном.

2. Distroless или slim — не «полный Ubuntu»

3. Non-root user — обязательно

RUN groupadd -r app && useradd -r -g app -u 10001 app
USER 10001
# или в distroless
FROM gcr.io/distroless/static:nonroot
USER nonroot:nonroot

Если контейнер скомпрометирован — атакующий не root.

4. Pin версии и digest

# Плохо
FROM python:latest

# Лучше
FROM python:3.12-slim

# Идеально (для prod)
FROM python:3.12.5-slim@sha256:a8f3...

Digest — гарантия что образ не подменили в registry.

5. Кеш слоёв — порядок имеет значение

Часто меняющееся — в конец. Стабильные deps — в начало.

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .              # редко меняется
RUN pip install -r requirements.txt  # кешируется
COPY . .                             # часто меняется → пересборка только этого слоя

6. .dockerignore — must have

.git
.github
.venv
__pycache__
node_modules
.env
.env.*
*.log
.DS_Store
*.md
docs/
tests/
.terraform/

Не тащи в образ то, что не нужно. Безопасность + размер.

7. Чистка в том же RUN

RUN apt-get update && \
    apt-get install -y --no-install-recommends curl ca-certificates && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Если очистка в отдельном RUN — она не уменьшит слой (мусор уже в предыдущем слое).

8. HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:8080/healthz || exit 1

В K8s заменяется liveness/readiness probes, но для docker-compose и swarm — must.

9. Сигналы и PID 1

# Плохо: shell form → SIGTERM не доходит до приложения
CMD python -m app

# Хорошо: exec form → процесс становится PID 1
CMD ["python", "-m", "app"]

# Идеально для multi-process: tini
RUN apt-get install -y tini
ENTRYPOINT ["tini", "--"]
CMD ["python", "-m", "app"]

10. BuildKit secrets — никогда в layers

# syntax=docker/dockerfile:1.7
FROM python:3.12-slim
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm install
# secret монтируется только на время этого RUN, в образ не попадает
docker build --secret id=npmrc,src=$HOME/.npmrc .

Эталонный Dockerfile для Python

# syntax=docker/dockerfile:1.7

ARG PYTHON_VERSION=3.12.5

FROM python:${PYTHON_VERSION}-slim AS builder
ENV PIP_NO_CACHE_DIR=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

FROM python:${PYTHON_VERSION}-slim AS runtime

# Безопасность
RUN groupadd -r app -g 10001 && useradd -r -g app -u 10001 -m -d /home/app app
WORKDIR /app

# Только venv и код, никаких build-tools
COPY --from=builder --chown=app:app /app/.venv /app/.venv
COPY --chown=app:app src/ ./src/

USER app
ENV PATH="/app/.venv/bin:$PATH" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

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

EXPOSE 8000
ENTRYPOINT ["python", "-m", "src.main"]

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

# Multi-arch build (amd64 + arm64)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:1.0 --push .

# Анализ слоёв и размера
docker history myapp:1.0
dive myapp:1.0                  # интерактивный инструмент

# Security scan
trivy image myapp:1.0
grype myapp:1.0

# SBOM
syft myapp:1.0 -o spdx-json > sbom.json

# Linter Dockerfile
hadolint Dockerfile

# Подпись
cosign sign myapp:1.0

Anti-patterns (никогда!)

Чек-лист готовности образа к продакшену