24. Минимизация образов. scratch, distroless
Минимизация: Принцип необходимого и достаточного
В классической инженерной культуре Bell Labs и философии Unix всегда господствовал принцип умеренности: инструмент совершенен не тогда, когда в него больше нечего добавить, а тогда, когда из него больше нечего убрать. В мире контейнеризации размер артефакта имеет прямое физическое значение. Чем меньше байтов занимает образ, тем быстрее он пересылается по сети, тем меньше времени уходит на запуск новых подов в Kubernetes, тем ниже счета за облачные хранилища и, что самое главное, тем меньше поверхность потенциальной атаки.
Если для сред вроде Java или Python минимальный размер контейнера редко опускается ниже нескольких сотен мегабайт (из-за необходимости таскать за собой JRE, разделяемые библиотеки C и интерпретаторы), то компилируемый Go позволяет подойти к пределу минимализма — созданию практически «невесомых» контейнеров.
Мы переходим от вопроса «Как собрать контейнер?» к фундаментальному вопросу: «Что именно допустимо оставить в рантайме?». Наша инженерная цель — упаковать в образ только чистый машинный код вашего приложения.
scratch: Абсолютный ноль
scratch — это особое зарезервированное ключевое слово в Docker. Это не операционная система, не дистрибутив и не базовая файловая система. Образ scratch представляет собой абсолютную пустоту размером ровно 0 байт. В нем нет ни одного файла: нет оболочки /bin/sh, нет библиотек, нет даже виртуального устройства /dev/null, пока вы или контейнерный движок его не создадите.
Для Go-микросервиса это идеальная среда развертывания, если приложение скомпилировано статически.
Требования к бинарнику
Чтобы запуститься в пустом окружении scratch, бинарный файл должен быть полностью статическим. Он не должен содержать динамических ссылок на разделяемые объекты (.so файлы).
В Go это достигается гарантированным отключением CGO:
CGO_ENABLED=0 go build -ldflags="-s -w" -o main .
Подводные камни и проблемы scratch
Когда вы выбрасываете из контейнера всю операционную систему, стандартная библиотека Go лишается привычной инфраструктуры, которую программисты обычно принимают как данность:
-
SSL/TLS Сертификаты:
При выполнении исходящих HTTPS-запросов (например, к сторонним API или базе данных с TLS) пакетcrypto/x509должен проверить цепочку доверия сертификатов удаленного сервера. Для этого ему необходим локальный доверенный набор корневых сертификатов (CA Bundle).- В дистрибутивах Ubuntu и Alpine эти сертификаты хранятся в
/etc/ssl/certs. - В пустом образе
scratchэтой директории просто не существует. - Результат: фатальная ошибка
x509: certificate signed by unknown authority. - Решение: перенести файл
ca-certificates.crtсо сборочного этапа (builder).
- В дистрибутивах Ubuntu и Alpine эти сертификаты хранятся в
-
Данные о часовых поясах (Timezone Data):
Если ваш код вызываетtime.LoadLocation("Europe/Moscow")или вычисляет локальное время, пакетtimeпытается прочесть системную базу данных часовых поясов IANA (обычно расположенную в/usr/share/zoneinfo).- В
scratchэтих каталогов нет. - Результат: аварийное завершение с ошибкой, паника или принудительный сброс локали в UTC по умолчанию.
- Решение: скопировать
zoneinfo.zipиз тулчейна Go, смонтировать/usr/share/zoneinfoиз билдера или импортировать пустой пакет_ "time/tzdata"в сам код приложения.
- В
Эталонный Dockerfile для запуска в scratch:
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o main .
# --- Final Stage ---
FROM scratch
# Копируем сертификаты (путь может отличаться в зависимости от базы билдера)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Копируем данные таймзон (опционально)
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Копируем бинарник
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
distroless: Безопасность «из коробки»
Инженеры Google предложили изящное компромиссное решение, создав проект distroless. Это семейство образов, специально спроектированное для компилируемых языков (Go, Rust), которое занимает промежуточную позицию между абсолютной пустотой scratch и самодостаточным alpine.
Образы Distroless:
- Содержат предустановленные CA сертификаты и актуальную базу tzdata.
- Содержат базовые системные файлы конфигурации
/etc/passwdи/etc/groupдля управления пользователями. - Не содержат командной оболочки (Shell), пакетного менеджера и утилит операционной системы.
flowchart TD
subgraph Scratch ["FROM scratch"]
A["Бинарник"]
end
subgraph Distroless ["FROM gcr.io/distroless/static"]
B["Бинарник"]
C["CA Certs"]
D["TZ Data"]
end
subgraph Alpine ["FROM alpine"]
E["Бинарник"]
F["CA Certs"]
G["BusyBox Shell"]
H["Package Manager"]
end
style Scratch fill:#1e293b,stroke:#9ca3af,color:#fff
style Distroless fill:#1e293b,stroke:#38bdf8,color:#fff
style Alpine fill:#1e293b,stroke:#f59e0b,color:#fff
Почему distroless предпочтительнее scratch для большинства команд?
В образе scratch вам приходится вручную поддерживать скрипты копирования сертификатов и таймзон. В образе gcr.io/distroless/static-debian12 эта системная рутина уже решена авторами.
Кроме того, в distroless из коробки зарегистрирован непривилегированный системный пользователь nonroot, что позволяет легко соблюдать главную заповедь контейнерной безопасности: никогда не запускать рабочий процесс от пользователя root.
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o main .
# Используем distroless
FROM gcr.io/distroless/static-debian12:latest
# Копируем бинарник
COPY --from=builder /app/main /
# Запускаем от имени nonroot (UID 65532)
USER nonroot:nonroot
ENTRYPOINT ["/main"]
Размер имеет значение: Сравнительный анализ
Сравним типовые базовые слои для развертывания скомпилированного Go-приложения:
| Образ | Размер (сжатый) | Shell | Certs | Уровень безопасности |
|---|---|---|---|---|
golang:1.22 |
~800 MB | Да | Да | Низкий (лишние инструменты) |
alpine:latest |
~5 MB | Да (sh) |
Да | Средний (musl libc векторы атак) |
distroless/static |
~2 MB | Нет | Да | Высокий |
scratch |
0 MB | Нет | Нет | Максимальный (но требует ручной настройки) |
Итог
- Scratch — выбор для ценителей абсолютного минимализма. Он обеспечивает нулевую поверхность атаки, но перекладывает заботу о сертификатах доверия и базах таймзон на плечи инженера.
- Distroless — общепризнанный золотой стандарт промышленного продакшена, идеально балансирующий между жесткой изоляцией и наличием необходимых системных сертификатов.
- Всегда декларируйте исполнение процесса от имени непривилегированного пользователя (
USER nonroot:nonroot), нивелируя риски побега из контейнера при уязвимостях ядра. - Отсутствие командной оболочки в продакшене — это осознанное достоинство, а не ограничение: используйте эфемерные отладочные контейнеры вместо захламления боевых сред.
Мы научились упаковывать бинарник Go в ультракомпактные контейнеры. Однако если каждый запуск сборщика на CI будет выкачивать зависимости и компилировать проект с нуля, время обратной связи для разработчика быстро выйдет из-под контроля.
В следующей статье мы разберем тонкости кэширования сборок в Docker: 25. Кэширование сборки Docker.