golang

Как я уменьшил Docker-образ Go-приложения с 1.92 GB до 9 MB

  • воскресенье, 2 ноября 2025 г. в 00:00:12
https://habr.com/ru/articles/962318/

Введение

Первый Docker-образ для моего Go-приложения весил 1.92 GB. Для микросервиса на 100 строк — абсурдно. Решил разобраться, куда именно уходит место и как добиться максимально лёгкого образа.

За несколько итераций оптимизации удалось уменьшить образ в 91 раз — до 21 MB production вариант. С дополнительным UPX-сжатием в 213 раз — до 9 MB.

В статье

  • Максимальная оптимизация Docker-образа для Go

  • Выбор базового образа и техник для каждого сценария

Создал простенький мониторинг микросервис

Функционал:

  • /health — Показывает работает ли приложение, сколько времени оно запущено и какая версия.

  • /ready — Отвечает на вопрос готово ли приложение принимать запросы.

  • /metrics — Показывает сколько памяти использует, сколько потоков работает, сколько ядер процессора доступно.

Сервер корректно завершается при отправке сигнала остановки: не принимает новые запросы и ждет 5 секунд пока закончатся текущие.

Стек: Go 1.24 + Gin, >100 строк кода.

Ссылка на репозиторий

Наивный подход (1.92 GB)

Начал с самого простого и очевидного на первый взгляд — официального образа golang:1.24, но не забыл про две важные практики:

  • Правильное копирование зависимостейgo.mod и go.sum копируем перед основным кодом. Docker кеширует этот слой, и при изменении исходников зависимости не будут скачиваться заново.

  • Файл .dockerignore — исключает из контекста сборки ненужные файлы и директории.

Пример .dockerignore:

logs/
*.log
.git
.gitignore
*.md
.vscode/
dist/
build/
bin/
*.exe
.env
*.local

Полный код образа:

FROM golang:1.24

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o /server

EXPOSE 8080

CMD ["/server"]
Время сборки
Время сборки
Итоговый размер образа
Итоговый размер образа

golang:1.24 базируется на Debian, использует glibc, включает множество системных утилит, так что занимает очень много места.

Переход на Alpine (998 MB)

Первая оптимизация — замена базового образа на golang:1.24-alpine3.20. Код остается прежним, меняется только базо��ый образ.

Важно: фиксируем версию Alpine для предсказуемости повторных сборок.

Полный код образа:

FROM golang:1.24-alpine3.20

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o /server

EXPOSE 8080

CMD ["/server"]
Время сборки
Время сборки
Итоговый размер образа
Итоговый размер образа

golang:1.24-alpine3.20 построен на Alpine Linux — минималистичном дистрибутиве, который использует musl libc вместо стандартной glibc и содержит только необходимый набор пакетов.

Multi-stage build (33 MB)

В предыдущем подходе финальный образ содержал весь Go SDK (компилятор, стандартная библиотека, утилиты сборки), хотя для запуска нужен только скомпилированный бинарник.

Разделим Dockerfile на две стадии:

  1. Стадия сборки (builder): Тяжёлый образ golang:1.24-alpine с Go SDK — компилирует приложение.

  2. Стадия запуска (runtime): Лёгкий образ alpine:3.20 — копирует только готовый бинарник.

В итоге в финальный образ попадет только то, что явно скопировано через COPY --from=builder. Весь Go SDK остаётся в стадии сборки.

Важно:

  • CGO_ENABLED=0 — делаем бинарник полностью статическим, не требующим динамических библиотек, но если ваш проект использует cgo (например, драйверы, требующие системных библиотек), сборка с CGO_ENABLED=0 упадёт. Тогда нужно включить CGO_ENABLED=1 и установить необходимые toolchain (gcc, musl-dev и т.п.) в builder стадии.

  • -ldflags="-s -w" — удаляем символы отладки и таблицы символов, сокращая размер на 25–30%.

  • RUN apk add --no-cache ca-certificates tzdata — устанавливаем два пакета — корневые сертификаты (ca-certificates) и базу часовых поясов (tzdata).

  • Для безопасности обязательно нужно создать непривилигированного пользователя: RUN addgroup -S appgroup && adduser -S appuser -G appgroup.

Критичные объекты, которые нужно скопировать в финальную стадию:

  • SSL-сертификаты

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

Без них: x509: certificate signed by unknown authority

  • Timezone данные

COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 

Без них: unknown time zone Europe/Moscow

  • Ну и про бинарник не забыть c указанием владельца и прав

COPY --from=builder --chown=appuser:appgroup --chmod=755 /server /server

Полный код образа:

FROM golang:1.24-alpine3.20 AS builder

RUN apk add --no-cache ca-certificates tzdata

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

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

FROM alpine:3.20

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder --chown=appuser:appgroup --chmod=755 /server /server

USER appuser

EXPOSE 8080

CMD ["/server"]
Время сборки
Время сборки
Итоговый размер образа
Итоговый размер образа

Отделив сборку от запуска мы добились уменьшения в 58 раз.

Пустой scratch (21 MB)

scratch — буквально пустой образ размером 0 байт. Внутри нет ОС, утилит, библиотек, файловой системы.

Go компилируется в самодостаточный бинарник:

  • Не требует runtime окружения.

  • Работает напрямую с ядром Linux без промежуточных слоёв.

Проблема: в отличие от прошлого образа мы не можем создать пользователя в финальной стадии.

Решение: cоздаем пользователя в стадии сборки и копируем сформированные файлы в финальную:

COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group

Полный код образа:

FROM golang:1.24-alpine3.20 AS builder

RUN addgroup -S appgroup && adduser -S appuser -G appgroup \
 && apk add --no-cache ca-certificates tzdata

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

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

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 /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
COPY --from=builder --chown=appuser:appgroup --chmod=755 /server /server

USER appuser

EXPOSE 8080

CMD ["/server"]
Время сборки
Время сборки
Итоговый размер образа
Итоговый размер образа

С помощью связи multi-stage и scratch мы добиваемся уменьшения размера в 91 раз.

Плюсы: минимальный размер, максимальная б��зопасность, мгновенный запуск.

Минус: нет утилит для дебага.

Когда использовать scratch, а когда alpine

Критерий

scratch

alpine

Размер

минимальный

компактный

Поддержка CGO

нет

есть

Shell/Debug

нет

есть

Безопасность

максимальная

высокая

Удобство отладки

низкое

хорошее

Scratch когда:

  • Чистый Go без CGO.

  • Нужна максимальная безопасность.

  • Критична скорость pull/deployment.

  • Нет зависимости от системных библиотек.

Alpine когда:

  • Нужен CGO.

  • Требуется shell для дебага.

  • Используете сторонние утилиты.

  • Нужен package manager для runtime-установки.

Компромисс между alpine и scratch: distroless (25 MB)

Distroless‑образ содержит только необходимые для запуска библиотеки: нет shell, пакетного менеджера и утилит, за счёт чего уменьшается поверхность атаки и снижается количество уязвимых компонентов. В отличие от alpine это не полноценный дистрибутив, а упакованный runtime, поэтому управлять им проще в production, если интерактивный дебаг не требуется. Для Go это удобный компромисс между полезностью и размером: меньше, чем alpine, но чуть тяжелее scratch.

Когда выбирать

  • Нужен минимальный и безопасный runtime без shell, но с системными библиотеками, необходимыми приложению.

  • Продакшн‑окружение, где важны малый размер и низкая поверхность атаки, а интерактивный дебаг не является обязательным сценарием.

  • Go‑сервисы без CGO, где тонкий рантайм предпочтительнее полноценного дистрибутива.

Пользователь и группа nonroot уже встроены. Сертификаты и данные часовых поясов также присутствуют, на builder стадии устанавливаем их для корректной сборки, копировать в runtime не требуется.

Полный код образа:

FROM golang:1.24-alpine3.20 AS builder

RUN apk add --no-cache ca-certificates tzdata

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

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

FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder --chown=nonroot:nonroot --chmod=755 /server /server

USER nonroot

EXPOSE 8080

CMD ["/server"]
Время сборки
Время сборки
Итоговый размер образа
Итоговый размер образа

Бонус: добавляем UPX-сжатие в образ c multi-stage + scratch (9 MB)

UPX (Ultimate Packer for eXecutables) — компрессор исполняемых файлов:

  • Сжимает бинарник алгоритмом LZMA (как в 7zip).

  • Добавляет встроенный декомпрессор в начало файла (~50KB).

  • Распаковывает себя в RAM при запуске.

Плюс: размер уменьшается в 2-3 раза.

Минус: замедление старта и рост потребления оперативной памяти.

Важно:

Cкачать upx:

RUN apk add --no-cache upx

Сжать собранный бинарник:

RUN upx --best --lzma /server

Полный код образа:

FROM golang:1.24-alpine3.20 AS builder

RUN addgroup -S appgroup && adduser -S appuser -G appgroup \
 && apk add --no-cache ca-certificates tzdata upx

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

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

RUN upx --best --lzma /server

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 /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
COPY --from=builder --chown=appuser:appgroup --chmod=755 /server /server

USER appuser

EXPOSE 8080

CMD ["/server"]
Время сборки
Время сборки
Итоговый размер образа
Итоговый размер образа

С помощью такого подхода образ уменьшается в 213 раз, но редко применяется в production из-за ряда недостатков:

  • При каждом старте CPU тратит время на распаковку.

  • В нагруженных микросервисах увеличивает холодный старт.

  • Некоторые антивирусы и системы безопасности помечают UPX-файлы как подозрительные.

Использовать можно: для AWS, для CLI-утилит, для дистрибуции инструментов без зависимости от ОС.

Вывод: UPX — нишевый инструмент, в production без крайней необходимости лучше не использовать.

Заключение

Оптимизация образов — это часть инженерной культуры: осознанный выбор базового образа, разделение этапов сборки и выполнения, чистый Dockerfile и строгий .dockerignore.

Лёгкие образы собираются быстрее, экономят трафик и дисковое пространство, а также уменьшают поверхность атаки при деплое.

Буду благодарен вашим комментариям, правкам и конструктивной критике.