Коротко: Go компилируется в один статический бинарник, поэтому образ собирают multi-stage — итог в единицы мегабайт вместо сотен.

Коротко: Go компилируется в один статический бинарник, поэтому образ для него собирают в два этапа (multi-stage) — на первом компилируют в «толстом» образе с тулчейном, на втором копируют только готовый бинарник в минимальный образ. Итог — образ в единицы мегабайт вместо сотен. Ниже рабочий Dockerfile и нюансы.

Зачем multi-stage

Если просто собрать в golang:1.22 и оставить как есть — в образ попадёт весь Go-тулчейн, исходники и кэш (~800+ МБ). Это лишнее в проде: для запуска нужен только скомпилированный бинарник. Multi-stage решает это: компиляция в одном слое, рантайм — в другом.

Базовый Dockerfile

FROM golang:1.22 AS build
WORKDIR /src

# сначала зависимости — кэш слоёв при неизменных go.mod/go.sum
COPY go.mod go.sum ./
RUN go mod download

# затем код
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app ./cmd/server

# --- минимальный финальный образ ---
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=build /app /app
ENTRYPOINT ["/app"]

Разбор ключевых моментов

  • CGO_ENABLED=0 — отключает cgo, получаем полностью статический бинарник без зависимостей от системных библиотек. Без этого alpine может ругаться на отсутствие glibc.
  • Сначала go.mod/go.sum, потом код. Слой с зависимостями кэшируется и не пересобирается, пока не изменились зависимости — ускоряет повторные сборки.
  • ca-certificates — нужны, если бинарник ходит по HTTPS. Иначе будут ошибки TLS.
  • COPY --from=build — берём из стадии сборки только бинарник.

Ещё меньше: scratch и distroless

Если бинарник полностью статический, финальный образ можно сделать почти нулевым:

FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /app /app
ENTRYPOINT ["/app"]

scratch — пустой образ (0 байт базы). Минус: нет shell и утилит для отладки. Компромисс — gcr.io/distroless/static: чуть больше, но безопаснее и с сертификатами «из коробки».

Уменьшаем размер бинарника

Флаги линковки убирают отладочную информацию:

RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server

-s -w выкидывают таблицу символов и DWARF — бинарник становится заметно легче (ценой отсутствия отладочных символов в проде, что обычно и нужно).

.dockerignore обязателен

Чтобы в контекст сборки не уезжало лишнее (ускоряет билд и уменьшает шум):

.git
*.md
/tmp
/bin
Dockerfile
.dockerignore

Прокидывание версии в бинарник

Версию/коммит удобно зашить при сборке через ldflags:

ARG VERSION=dev
RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$VERSION" -o /app ./cmd/server

В коде это var version string в пакете main — подставится значение при компиляции.

Healthcheck и непривилегированный пользователь

Для прода стоит запускать не от root и добавить healthcheck:

FROM alpine:3.20
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 app
USER app
COPY --from=build /app /app
HEALTHCHECK --interval=30s CMD ["/app", "healthcheck"]
ENTRYPOINT ["/app"]

Multi-stage + статический бинарник — одна из причин, почему Go так приятно эксплуатировать: лёгкие образы, быстрый деплой, мало поверхности для атак. Если выстраиваете CI/CD и контейнеризацию сервисов, могу помочь на консультации или в рамках разработки.