Как я уменьшил Docker-образ Go-приложения с 1.92 GB до 9 MB
- воскресенье, 2 ноября 2025 г. в 00:00:12
Первый 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 строк кода.
Начал с самого простого и очевидного на первый взгляд — официального образа 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, включает множество системных утилит, так что занимает очень много места.
Первая оптимизация — замена базового образа на 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 и содержит только необходимый набор пакетов.
В предыдущем подходе финальный образ содержал весь 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 — буквально пустой образ размером 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 |
|---|---|---|
Размер | минимальный | компактный |
Поддержка CGO | нет | есть |
Shell/Debug | нет | есть |
Безопасность | максимальная | высокая |
Удобство отладки | низкое | хорошее |
Scratch когда:
Чистый Go без CGO.
Нужна максимальная безопасность.
Критична скорость pull/deployment.
Нет зависимости от системных библиотек.
Alpine когда:
Нужен CGO.
Требуется shell для дебага.
Используете сторонние утилиты.
Нужен package manager для runtime-установки.
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 (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.
Лёгкие образы собираются быстрее, экономят трафик и дисковое пространство, а также уменьшают поверхность атаки при деплое.
Буду благодарен вашим комментариям, правкам и конструктивной критике.