Best practices: чек-лист production Dockerfile
В предыдущих уроках разобрали по частям: multi-stage, BuildKit, USER non-root, HEALTHCHECK. Этот урок — summary всех best practices в виде чек-листа, плюс LABEL для metadata, hadolint для автоматизации проверок, и общая стратегия «один процесс на контейнер».
В этом уроке: 10 признаков production-ready Dockerfile.
Анатомия процесса — PID, address space, fd-таблица
Принцип «один процесс на контейнер»
Один из самых фундаментальных, но часто нарушаемых принципов: контейнер запускает ОДИН процесс, а не процесс-менеджер с несколькими сервисами.
Плохо (legacy подход с supervisord):
FROM ubuntu:24.04
RUN apt-get install -y nginx redis-server postgresql supervisor
COPY supervisord.conf /etc/supervisor/conf.d/
CMD ["supervisord", "-n"]
# Запускает nginx + redis + postgresql в одном контейнере
Хорошо:
- Три контейнера: nginx, redis, postgres
- Связаны через docker network
- Каждый можно независимо scale, monitor, restart
Преимущества «один процесс»:
- PID 1 это твой процесс. Сигналы (SIGTERM) идут ему напрямую — graceful shutdown работает.
- Restart granular. Postgres упал — рестартится только postgres, не вся система.
- Logs из stdout/stderr. Один процесс → понятные логи в
docker logs. - Scaling. Можно поднять 5 копий app-сервиса без 5 копий postgres.
- Resource limits. cgroup лимиты применяются к контейнеру, и если внутри несколько процессов — они дерутся за ресурсы.
Исключения:
- init-системы для zombie reaping —
tini(--initфлаг Docker, илиENTRYPOINT ["tini", "--", "main"]). Это не отдельный сервис, это обёртка. - sidecars в k8s — это отдельный контейнер в pod, не процесс внутри контейнера.
Layer ordering: от стабильного к меняющемуся
Cache invalidation работает каскадно: изменился layer N — все layers N+1, N+2… пересобираются. Поэтому самое медленное и стабильное должно быть НИЖЕ (раньше), а быстро-меняющееся — ВЫШЕ (позже).
FROM python:3.13-slim # stable: base image, меняется редко
# Stable: system deps
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 && rm -rf /var/lib/apt/lists/* # stable: меняется только при добавлении deps
# Stable: env vars
ENV PYTHONUNBUFFERED=1
# Stable: workdir, user
RUN useradd -m -u 1000 appuser
WORKDIR /app
RUN chown appuser:appuser /app
# Semi-stable: Python deps
COPY requirements.txt . # stable: requirements меняются редко
RUN pip install --no-cache-dir -r requirements.txt
# Volatile: app code
COPY --chown=appuser:appuser . . # volatile: меняется на каждый commit
USER appuser
HEALTHCHECK --interval=30s --timeout=5s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"
CMD ["python", "main.py"]
При commit с изменением кода: пересобираются только COPY . . и ниже. Pip install кэшируется.
LABEL: metadata в образе
LABEL добавляет metadata, видимую через docker inspect. Это стандартный способ описать «что это за образ».
LABEL org.opencontainers.image.source="https://github.com/myorg/data-pipeline"
LABEL org.opencontainers.image.version="v1.2.3"
LABEL org.opencontainers.image.description="ETL pipeline for daily customer data"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.authors="[email protected]"
LABEL org.opencontainers.image.created="2026-05-15T12:00:00Z"
Это OCI image annotations standard — стандартизированные имена, которые понимают registries (GHCR, ghc.io), security scanners (Trivy, Snyk), и т.д.
Особенно важен org.opencontainers.image.source — это URL git-репозитория. GHCR использует его для связывания image с repository (показывает image в Packages tab).
Через ARG можно подставлять значения при build:
ARG GIT_SHA
ARG VERSION
ARG BUILD_DATE
LABEL org.opencontainers.image.revision="${GIT_SHA}" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}"
docker build \
--build-arg GIT_SHA=$(git rev-parse HEAD) \
--build-arg VERSION=$(git describe --tags) \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-t app:v1.2.3 .
# Проверим metadata
docker inspect app:v1.2.3 --format '{{json .Config.Labels}}' | jq
Это даёт ответ на вопрос «откуда этот image, какой commit, когда собран» без необходимости лезть в CI history.
Hadolint: автоматическая проверка Dockerfile
Hadolint это линтер для Dockerfile. Запускается локально или в CI:
# Через docker
docker run --rm -i hadolint/hadolint < Dockerfile
# С конфигом
docker run --rm -v $PWD:/work hadolint/hadolint hadolint Dockerfile
Типичные правила, которые ловит:
- DL3002 — Last USER should not be root
- DL3007 — Don’t use
latesttag - DL3008 — Pin versions in
apt-get install(e.g.curl=7.74.0-1.3+deb11u7) - DL3013 — Pin versions in pip
- DL3015 — Use
--no-install-recommends - DL3020 — Use COPY instead of ADD
- DL3025 — Use exec form of CMD/ENTRYPOINT
- DL3045 — Use absolute WORKDIR
- DL3059 — Multiple RUN can be merged
Конфиг в .hadolint.yaml:
# .hadolint.yaml
ignored:
- DL3008 # позволить unpinned apt versions для DE-проектов
trustedRegistries:
- docker.io
- ghcr.io
- public.ecr.aws
failure-threshold: warning # CI падает на warnings и хуже
В CI:
# .github/workflows/lint.yml
- name: Hadolint
uses: hadolint/hadolint-action@v3
with:
dockerfile: Dockerfile
failure-threshold: warning
Чек-лист production Dockerfile
Применять каждый раз перед production deploy:
1. FROM указан с конкретной версией (не latest), pinned digest для прода
FROM python:3.13.1-slim # ok для staging
FROM python@sha256:abc123... # лучше для prod
2. Multi-stage build, если есть compilation
builder с build-essential, runtime с slim/distroless.
3. RUN объединены через && и cleanup в том же RUN
RUN apt-get update && \
apt-get install -y --no-install-recommends pkg1 pkg2 && \
rm -rf /var/lib/apt/lists/*
4. .dockerignore правильно настроен
.git, .venv, data/, tests/fixtures/, *.pem, .env все игнорируются.
5. WORKDIR — абсолютный путь
WORKDIR /app # не "cd /app" в RUN
6. COPY с правильным порядком (requirements перед кодом)
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
7. USER non-root
RUN useradd -m -u 1000 appuser
USER appuser
8. CMD/ENTRYPOINT в exec form
CMD ["python", "main.py"] # exec form
# не: CMD python main.py # shell form
9. HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"
10. LABEL с OCI annotations
LABEL org.opencontainers.image.source="https://github.com/org/repo" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${GIT_SHA}"
Bonus: secrets не в ENV, no-cache-dir, —no-install-recommends, использование BuildKit cache mounts.
Полный production-ready Dockerfile
Собираем всё воедино:
# syntax=docker/dockerfile:1.7
ARG PYTHON_VERSION=3.13.1
# ── Stage 1: builder ───────────────────────────────────────────
FROM python:${PYTHON_VERSION}-slim AS builder
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libpq-dev && \
rm -rf /var/lib/apt/lists/*
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /build
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
# ── Stage 2: runtime ───────────────────────────────────────────
FROM python:${PYTHON_VERSION}-slim
ARG GIT_SHA=unknown
ARG VERSION=dev
ARG BUILD_DATE
LABEL org.opencontainers.image.source="https://github.com/myorg/data-pipeline" \
org.opencontainers.image.revision="${GIT_SHA}" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.description="DE data pipeline" \
org.opencontainers.image.licenses="MIT"
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libpq5 \
curl && \
rm -rf /var/lib/apt/lists/*
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PATH="/opt/venv/bin:$PATH"
RUN groupadd --gid 1000 appgroup && \
useradd --uid 1000 --gid 1000 --create-home --shell /bin/bash appuser
COPY --from=builder /opt/venv /opt/venv
WORKDIR /app
RUN chown appuser:appgroup /app
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -fs http://localhost:8080/health || exit 1
CMD ["python", "-m", "etl.server"]
Свойства:
- Multi-stage с venv для маленького финального образа
- BuildKit cache mount для pip
- Non-root user 1000
- HEALTHCHECK с curl
- OCI LABEL для traceability
- Все best practices в одном файле
CI workflow для Dockerfile
Типичный CI для DE-проекта:
# .github/workflows/docker.yml
name: Docker
on:
pull_request:
paths: [Dockerfile, requirements.txt, src/**, .github/workflows/docker.yml]
push:
branches: [main]
tags: ['v*']
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hadolint/hadolint-action@v3
with:
dockerfile: Dockerfile
failure-threshold: warning
build:
runs-on: ubuntu-latest
needs: lint
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=sha,prefix=git-
type=ref,event=branch
- uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
VERSION=${{ github.ref_name }}
BUILD_DATE=${{ github.event.repository.updated_at }}
cache-from: type=gha
cache-to: type=gha,mode=max
- uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }}
format: sarif
output: trivy-results.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
Что здесь:
- hadolint на PR (блокирует merge при warnings)
- buildx + build-push-action для push
- metadata-action для consistent теггинга (semver + git-sha + branch)
- GitHub Actions cache через
type=gha - Trivy для vulnerability scanning, результаты в Security tab
Это и есть production CI workflow для DE-команды.
Попробуй сам
Прогони hadolint на любом своём Dockerfile:
# Создадим намеренно плохой Dockerfile
cat > Dockerfile.bad <<'EOF'
FROM python:latest
ADD https://example.com/binary /usr/local/bin/
RUN apt-get update
RUN apt-get install -y curl
COPY . .
CMD python app.py
EOF
# Hadolint
docker run --rm -i hadolint/hadolint < Dockerfile.bad
# Увидишь:
# DL3007 warning: Using latest is prone to errors...
# DL3020 error: Use COPY instead of ADD for files and folders
# DL3008 warning: Pin versions in apt-get install
# DL3009 info: Delete the apt-get lists after installing something
# DL3025 warning: Use arguments JSON notation for CMD and ENTRYPOINT
# DL3045 warning: COPY to relative WORKDIR
rm Dockerfile.bad
Также автоматизируй local pre-commit hook:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/AleksaC/hadolint-py
rev: v2.12.1b3
hooks:
- id: hadolint
args: ['--failure-threshold', 'warning']
После pre-commit install каждый коммит, изменяющий Dockerfile, будет автоматически проверяться.