Введение
Первый 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 на две стадии:
Стадия сборки (builder): Тяжёлый образ
golang:1.24-alpineс Go SDK — компилирует приложение.Стадия запуска (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.
Лёгкие образы собираются быстрее, экономят трафик и дисковое пространство, а также уменьшают поверхность атаки при деплое.
Буду благодарен вашим комментариям, правкам и конструктивной критике.
