Прямой ответ
Минимальный базовый образ — самый дешёвый рычаг безопасности и скорости: меньше пакетов — меньше CVE-поверхность (Trivy-цифры — «первый скан debian-образа — 700 CVE, slim — 200, distroless — 30» — порядок, не проценты), меньше слои — быстрее pull/push (CI-скорость — деплой/масштабирование — HPA-реакция быстрее на лёгких образах); лестница минимализма (по убыванию содержимого): полные дистрибутивы (debian/ubuntu — 80–100 МБ — шелл/пакетники/утилиты — CVE-магнит, оправданы для build-стадий и «человеческих» контейнеров); slim (debian-slim — тот же glibc, минус документация/пакетник-кэш — 30–45 МБ — «обычная» база с меньшим багажом — дефолт-компромисс); alpine (musl libc вместо glibc — 5–10 МБ — минимум CVE; но musl — другая libc — C-расширения Python (numpy/pandas/psycopg-компиляция) ведут себя иначе (производительность malloc, DNS-нюансы (musl-resolver — «странные таймауты в k8s» — ndots-класс проблем — k8s-сети), wheels для musl есть не у всех пакетов — компиляция в образе — multistage-цена — «alpine для Python» — не всегда выигрыш — считать полную стоимость); distroless (Google — только приложение+рантайм+минимум библиотек — нет шелла/пакетника/утилит вообще — python3-java-node-варианты — 2–50 МБ — CVE-поверхность минимальна — «эксплойт попал в контейнер — а делать нечего: ни шелла, ни curl» — харднинг-идеал); цена distroless: отладка (docker exec -it sh невозможен — шелла нет — «зашёл в контейнер посмотреть» — прошлое — дисциплина сдвига: логи наружу (структурированные — агрегация), диагностика — ephemeral-контейнеры (kubectl debug -it pod --image=busybox --target=app — отладочный контейнер в общий namespace процесса — «шелл приходит к тебе» — k8s-практика), профилирование — из приложения (py-spy-attach из ephemeral — профили); debug-теги (distroless :debug — с busybox-шеллом — для dev-окружений — прод — чистый тег — тег-стратегии); nonroot-варианты обязательны (distroless:nonroot — uid 65532 — «root в контейнере» — минус — userns-контекст); Python-специфика distroless (gcr.io/distroless/python3 — интерпретатор без pip — зависимости — из multistage builder'а копируются готовыми (site-packages — wheels glibc — совместимость с distroless-библиотеками — debian-совместимые wheels); «pip install в distroless» — невозможен by design — всё в builder — это плюс: итоговый образ — неизменяемый артефакт — supply chain); выбор по контексту: веб-API/микросервис — distroless или slim-nonroot (максимум безопасности при нормальной цене), аналитика/numpy-тяжёлое — debian-slim (glibc-wheels без сюрпризов — pandas-контекст), build-стадия — полная (компиляторы — multistage — в прод не попадает), alpine — осознанно (musl-проверенный стек — Go-статик — идеально, Python-тяжёлое — считать); scratch — для статических Go/Rust-бинарей (вообще ничего — 5–15 МБ — CA-сертификаты копируем — «https не работает в scratch» — ca-certificates из builder — классика).
Практика: перевод сервиса на distroless без потери отлаживаемости
Dockerfile-трансформация (Python/FastAPI сервис): было — FROM python:3.11 (fat — 900 МБ — 700 CVE) → стало — две стадии: builder (FROM python:3.12-slim — pip install в /install с wheels — lock-файлы — детерминированность) → runtime (FROM gcr.io/distroless/python3-debian12:nonroot — COPY /install — entrypoint — multistage); результат: 900 МБ → 65 МБ (×14), CVE — 712 → 24 (distroless-остаток — glibc/openssl — без фикса частично — Trivy — игноры с обоснованием), pull — 12 с → 2 с; отладка после перехода (процедуры вместо «зайти шеллом»): логи — только наружу (structured logging — Loki — «print в /tmp» — мёртв — логи-практика); kubectl debug — ephemeral-контейнер (busybox/netshoot в pod — общий PID/net namespace — ps/strace/curl к приложению — сети-диагностика — «отладочный набор приходит сбоку»); dev-окружения — :debug-тег (шелл есть — локально/стейдж — локальный стек — прод — чистый); профилирование — py-spy из ephemeral (--target=app — тот же PID-space — профили) или continuous (Pyroscope — агент в приложении — отладка «изнутри» — шелл вообще не нужен); находки перехода: (а) приложение вызывало subprocess к curl (legacy-интеграция) — в distroless — нет curl — переписали на httpx (http-клиенты — «шелл-вызовы из кода» — всплыли все — польза перехода как аудита); (б) DNS — distroless без /etc/nsswitch — musl-подобных сюрпризов нет (glibc), но resolv.conf-поведение проверили в k8s (сети — тесты интеграционные — testcontainers); (в) CA-сертификаты — в distroless включены (python3-вариант), для scratch-бинарей — COPY из builder — проверка https-исходящих (TLS); (г) health-проверки — HTTP-зонды вместо exec (exec-зонд требует бинарь в образе — healthcheck-практика — k8s httpGet — стандарт); итог: поверхность атаки −95%, отлаживаемость — не упала, сменив форму (шелл «внутри» → ephemeral+логи+профили — культура observability — трейсинг — distroless подталкивает к зрелости — «отлаживать как в 2015» перестаёт работать — и хорошо).
FAQ
Alpine или distroless — что безопаснее и практичнее? По CVE-числам — сопоставимы (alpine 5–15, distroless 20–40 «сырых», но distroless без шелла/утилит — эксплуатируемая поверхность меньше — «CVE есть, вектора нет» — Trivy-контекст — «число CVE» — грубая метрика); практичность различается libc: alpine/musl — Go/статика/простые стеки (идеально — multistage), Python/glibc-зависимое — wheels-совместимость/производительность/DNS-нюансы (debian-slim или distroless-debian — glibc — без сюрпризов — numpy-контекст); отладка: alpine — шелл есть (apk add — инструменты на лету — «живой» контейнер), distroless — ephemeral-дисциплина (процедуры выше); «alpine — всегда меньше и лучше» — миф 2018 (musl-стоимость для Python-стека реальна); правило: glibc-стек → distroless/slim-nonroot, статик-бинарь → scratch/alpine, «нужен шелл в проде» → вопрос «зачем» (харднинг).
Как отлаживать контейнер, в котором нет шелла?
Инструменты: kubectl debug -it <pod> --image=nicolaka/netshoot --target=<container> — ephemeral-контейнер (общие namespaces с целевым — ps/curl/strace/py-spy — k8s — «шелл приезжает»); docker — docker run --pid=container:<id> (аналог для compose-мира — локальный стек); логи — единственный «внутренний» канал (структурированные — уровни debug по флагу/env — конфигурация — «включить debug в проде» — смена env, не «зайти в контейнер»); профилирование — агенты из приложения (Pyroscope/py-spy-attach); dev-тег с шеллом (distroless:debug — стейдж — тег-политика); сдвиг культуры: отладка «внутри контейнера» — легаси-привычка — observability-стек (логи/метрики/трейсы — три столпа) покрывает 95% случаев лучше шелла — остаток — ephemeral.
Что ставить базой для Python-сервиса в 2026? Порядок предпочтения: distroless python3 (nonroot) — сервисы с нормальной observability-дисциплиной (максимум безопасности — цена: процедуры отладки — сценарий выше); python:3.12-slim + nonroot-юзер — когда нужен шелл/пакетник в образе (миграции-инструменты внутри — alembic-контекст — компромисс), alpine — только если стек проверен на musl (или статик-компиляция — packaging); критерии: CVE-бюджет команды (Trivy-процесс — slim требует сопровожения базовых пакетов, distroless — почти нет), glibc-зависимости (numpy/pandas/torch — debian-семья — wheels), отладочная зрелость (логи/трейсы есть — distroless ок; «отлаживаем шеллом» — сначала логи, потом минимализм); «правильный» образ — не самый маленький, а минимальный достаточный с nonroot+read-only(харднинг) — размер — следствие, не цель.
Scratch-образ — когда оправдан и что в него положить? Scratch — пустой образ (0 байт — только ваш статический бинарь — Go/Rust — CGO_ENABLED=0 / musl-таргет — multistage — бинарь из builder); обязательный минимум внутри: CA-сертификаты (COPY --from=builder /etc/ssl/certs/ca-certificates.crt — https-исходящие — TLS — «забыл сертификаты» — первая шишка), временная зона (tzdata — если приложение работает с таймзонами — «время в UTC навсегда» — сюрприз), passwd-файл (nonroot-uid — /etc/passwd с 65532 — некоторые рантаймы требуют — харднинг); чего нет вообще: шелла/утилит/DNS-резолвинга тонкости (статик-Go — свой resolver — CGO-выключен — pure-Go DNS — k8s-ndots-поведение — сети — тестируем); оправдан: sidecar'ы, высокоплотные деплои (тысячи pod'ов — 5 МБ против 50 — ёмкость), maximum-security контексты; не оправдан: динамические языки (Python-интерпретатор — distroless, не scratch — «Python в scratch» — заморозка PyInstaller — хрупко — не прод-путь); итог: scratch — вершина минимализма для статик-бинарей, distroless — практический минимум для остального.
Источники: GoogleContainerTools/distroless, Alpine about, Docker multi-stage docs — ссылки в шапке.