Learning Platform
Глоссарий Troubleshooting
Урок 09.05 · 22 мин
Средний
dockerbest-practiceslabelshadolintproduction

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 reapingtini (--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 latest tag
  • 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, будет автоматически проверяться.


Проверка знанийKnowledge check
Команда строит Dockerfile c одним монолитным RUN: RUN apt-get install -y --no-install-recommends build-essential libpq-dev python3-dev && pip install psycopg2 numpy pandas. Финальный размер образа 850MB. SRE говорит 'это много для DE-микросервиса'. Какие три изменения дадут максимальное снижение размера?
ОтветAnswer
(1) Multi-stage build: builder с build-essential, python3-dev, libpq-dev; runtime с libpq5 только. Экономит 300-400MB build-tools. (2) В runtime stage не COPY весь venv -- использовать --user или venv в builder и COPY --from=builder /opt/venv /opt/venv. (3) pip install --no-cache-dir или RUN --mount=type=cache,target=/root/.cache/pip -- pip cache не попадает в слой (50-100MB). После всех трёх: 850MB -> ~250MB. Дополнительно: rm -rf /var/lib/apt/lists/* в том же RUN, что apt-get update.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём суть принципа 'один процесс на контейнер' и когда его можно нарушить?

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

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

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

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