🐳 Идеальный Dockerfile для Go. Худеем с 800 МБ до 10 МБ
Знакомая картина: джун приносит микросервис, который делает пару запросов к БД, а Docker-образ весит под гигабайт. Почему? Потому что в FROM гордо красуется golang:latest.
Вместе с вашим кодом в продакшен уезжает весь тулчейн компилятора, операционная система, куча утилит (шелл, curl) и, как бонус, пачка потенциальных уязвимостей.
Прелесть Go в том, что он компилирует всё в один независимый бинарный файл. Нам не нужен Go в рантайме! Наш путь - Multi-stage builds (Многоэтапная сборка).
Вот "золотой стандарт" Dockerfile, который работает в 99% случаев:
# === Этап 1: Сборка (Builder) ===
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Шаг 1: Кэшируем зависимости
# Этот слой пересоберется ТОЛЬКО если изменились go.mod или go.sum
COPY go.mod go.sum ./
RUN go mod download
# Шаг 2: Копируем код
COPY . .
# Шаг 3: Собираем бинарник
# CGO_ENABLED=0 - критически важно для статической линковки
# -ldflags="-w -s" - вырезаем отладочную информацию
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/bin/service ./cmd/server/main.go
# === Этап 2: Финальный образ (Runner) ===
# Используем distroless вместо alpine или scratch
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
# Забираем только готовый бинарник из первого этапа
COPY --from=builder /app/bin/service .
# Контейнер будет работать от непривилегированного пользователя
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/service"]
Разбираем нюансы под капотом (Senior Edition):
• Зачем нужен CGO_ENABLED=0? По умолчанию Go может динамически линковаться с системными C-библиотеками (например, libc). Если вы соберете такой бинарник и положите в пустой образ, он упадет при старте. Отключая CGO, мы принудительно делаем статическую линковку - всё необходимое запекается прямо внутрь файла.
• Флаги -ldflags="-w -s": Они вырезают DWARF-таблицы и символы отладки. Это бесплатно срезает ~20-30% веса бинарника. Не переживайте, трейсы паник (stack traces) всё ещё будут показывать номера строк, так что для прода это безопасно.
• Почему distroless/static, а не scratch или alpine? * В scratch (абсолютно пустом образе) нет корневых сертификатов (ca-certificates). Как только ваш сервис попытается сделать внешний HTTPS-запрос, он упадет. Там также нет таймзон (tzdata).
- В alpine есть sh (шелл) и пакетный менеджер apk - это векторы атаки для хакеров, если они найдут RCE в вашем коде.
- Google Distroless содержит только сертификаты, таймзоны и юзера nonroot. Идеальный баланс микроскопического размера (~2 МБ) и безопасности.
У кого образы до сих пор весят больше 50 МБ - срочно переписывать!
#golang #docker #devops #architecture #security
👉 @golang_lib
Знакомая картина: джун приносит микросервис, который делает пару запросов к БД, а Docker-образ весит под гигабайт. Почему? Потому что в FROM гордо красуется golang:latest.
Вместе с вашим кодом в продакшен уезжает весь тулчейн компилятора, операционная система, куча утилит (шелл, curl) и, как бонус, пачка потенциальных уязвимостей.
Прелесть Go в том, что он компилирует всё в один независимый бинарный файл. Нам не нужен Go в рантайме! Наш путь - Multi-stage builds (Многоэтапная сборка).
Вот "золотой стандарт" Dockerfile, который работает в 99% случаев:
# === Этап 1: Сборка (Builder) ===
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Шаг 1: Кэшируем зависимости
# Этот слой пересоберется ТОЛЬКО если изменились go.mod или go.sum
COPY go.mod go.sum ./
RUN go mod download
# Шаг 2: Копируем код
COPY . .
# Шаг 3: Собираем бинарник
# CGO_ENABLED=0 - критически важно для статической линковки
# -ldflags="-w -s" - вырезаем отладочную информацию
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/bin/service ./cmd/server/main.go
# === Этап 2: Финальный образ (Runner) ===
# Используем distroless вместо alpine или scratch
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
# Забираем только готовый бинарник из первого этапа
COPY --from=builder /app/bin/service .
# Контейнер будет работать от непривилегированного пользователя
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/service"]
Разбираем нюансы под капотом (Senior Edition):
• Зачем нужен CGO_ENABLED=0? По умолчанию Go может динамически линковаться с системными C-библиотеками (например, libc). Если вы соберете такой бинарник и положите в пустой образ, он упадет при старте. Отключая CGO, мы принудительно делаем статическую линковку - всё необходимое запекается прямо внутрь файла.
• Флаги -ldflags="-w -s": Они вырезают DWARF-таблицы и символы отладки. Это бесплатно срезает ~20-30% веса бинарника. Не переживайте, трейсы паник (stack traces) всё ещё будут показывать номера строк, так что для прода это безопасно.
• Почему distroless/static, а не scratch или alpine? * В scratch (абсолютно пустом образе) нет корневых сертификатов (ca-certificates). Как только ваш сервис попытается сделать внешний HTTPS-запрос, он упадет. Там также нет таймзон (tzdata).
- В alpine есть sh (шелл) и пакетный менеджер apk - это векторы атаки для хакеров, если они найдут RCE в вашем коде.
- Google Distroless содержит только сертификаты, таймзоны и юзера nonroot. Идеальный баланс микроскопического размера (~2 МБ) и безопасности.
У кого образы до сих пор весят больше 50 МБ - срочно переписывать!
#golang #docker #devops #architecture #security
👉 @golang_lib