Оптимизация образов: размер, .dockerignore, non-root, distroless, chiseled

↑ Этап 2 · Контейнеры: Docker, docker-compose, Makefile

Обязательно~5 мин чтенияУровень: junior

Зачем это на собесе

Компактные и безопасные образы — признак зрелости. Спрашивают конкретные приёмы и инструменты проверки.

Почему это важно

  • скорость доставки (pull в CI, на ноды, при масштабировании);
  • стоимость хранения и трафика;
  • безопасность: меньше пакетов — меньше CVE;
  • быстрый старт подов.

Приёмы уменьшения размера

  1. Multi-stage build: в финале только рантайм и артефакты.
  2. Минимальная база: alpine, slim, distroless, chiseled, scratch.
  3. Очистка в том же слое: apt-get clean && rm -rf /var/lib/apt/lists/*, --no-install-recommends, apk add --no-cache, pip install --no-cache-dir.
  4. .dockerignore: не отправлять в контекст .git, bin, obj, node_modules, логи, .env.
  5. Только production-зависимости: npm ci --omit=dev, dotnet publish без отладочных символов (/p:DebugType=None), trimming (PublishTrimmed), ReadyToRun.
  6. Не копировать лишнее: точечный COPY вместо COPY . . в финальной стадии.
  7. Объединение RUN для слоёв с временными файлами.
  8. Сжатие артефактов и удаление документации/локалей.
  9. Native AOT / статические бинарники (Go, Rust) на scratch.
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 \
 && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

Проверка: docker images, docker history, dive (неэффективные слои), docker manifest inspect.

.dockerignore

.git
.gitignore
**/bin
**/obj
**/node_modules
**/*.md
.env
.env.*
Dockerfile*
docker-compose*.yml
.vs
.vscode
coverage

Если всё игнорируется по умолчанию, используйте белый список (* затем !src/, !package*.json).

Non-root

RUN addgroup --system --gid 10001 app && adduser --system --uid 10001 --ingroup app app
COPY --chown=app:app . .
USER 10001:10001
  • используйте числовой UID: Kubernetes проверяет runAsNonRoot по числу;
  • порты выше 1024 (8080) — непривилегированному пользователю нельзя слушать < 1024 (или дать capability);
  • права на каталоги записи (/tmp, /app/data) — только нужным;
  • readOnlyRootFilesystem: true + tmpfs для временных файлов.

Distroless и chiseled

Distroless (Google): образы без shell и пакетного менеджера, только рантайм (gcr.io/distroless/static, base, cc, java, nodejs, python3). Chiseled (Canonical/Microsoft): нарезанные Ubuntu-образы (dotnet/aspnet:9.0-noble-chiseled). Chainguard/Wolfi: минимальные образы с регулярными обновлениями (почти ноль CVE).

Плюсы: размер, поверхность атаки, non-root по умолчанию. Минусы: нет sh → отладка: docker debug (Docker Desktop), ephemeral containers в Kubernetes (kubectl debug -it pod --image=busybox --target=app), отладочные теги (:debug c busybox).

Безопасность образов

  • сканирование: Trivy, Grype, Docker Scout, Snyk (в CI, блокировать HIGH/CRITICAL с исправлениями);
  • регулярная пересборка для подхвата патчей базового образа;
  • фиксировать базовый образ по digest и обновлять автоматически (Renovate, Dependabot);
  • SBOM (Syft), подпись образов (cosign), политики допуска (Kyverno, OPA Gatekeeper);
  • не хранить секреты (BuildKit secrets), минимальные capabilities (--cap-drop ALL), no-new-privileges;
  • линтер Dockerfile: hadolint.

Скорость запуска

  • малый размер и кэш на нодах (предзагрузка образов);
  • ленивая загрузка слоёв (stargz/SOCI) в некоторых реестрах;
  • для .NET: ReadyToRun, tiered compilation, AOT; для Java: CRaC, GraalVM;
  • imagePullPolicy: IfNotPresent с неизменяемыми тегами.

Таблица «было — стало»

ПриёмЭффект (типично)
SDK → runtime (multi-stage)900 МБ → 220 МБ
Debian → alpine/chiseled220 → 100–130 МБ
PublishTrimmed, отказ от символов−20–40%
Native AOT + runtime-deps15–30 МБ
Go на scratch10–20 МБ

Вопросы с ответами