Загрузка...

How to reduce the size of a Docker image in Go by 150 times?

Thread in Go created by DearFriend Feb 9, 2026. (bumped Feb 14, 2026) 210 views

  1. DearFriend
    DearFriend Topic starter Feb 9, 2026 Hello 97 Jun 1, 2020
    Всем привет, в этой статье я хочу рассказать о способах сократить размер Docker-образов приложений написанных на компилируемых языках, поделившись своим недавним кейсом на Go.

    Исходные данные: есть тг-бот для парсинга данных с крипто-агрегаторов, буквально на 200-300 строчек, написанный на Go. Из внешних зависимостей только HTTP-клиент и фреймворк для написания тг-ботов.

    Этап 1. Подход в лоб
    Напишем для приложения стандартный Dockerfile и .dockerignore, а затем посмотрим размер образа
    Code
    FROM golang:1.25

    WORKDIR /app

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

    COPY . .
    RUN go build -o /bot

    CMD ["/bot"]
    Code
    .git/
    .gitignore
    *.md
    .vscode/
    .env
    .env.example
    LICENSE
    [IMG]
    Размер - целых 578MB для такого легкого приложения. Все дело в том, что образ самого Golang базируется на тяжелом Debian, который занимает много места.

    Этап 2. Смена базового образа
    На официальных страницах популярных образов в докер хабе часто имеется секция Image Variants - в ней можно увидеть стандартный образ, который чаще всего базируется на Debian (в свою очередь использующий glibc) и альтернативу на Alpine - легковесном дистрибутиве, использующем musl. Его мы и будем использовать.

    Ниже в спойлере представлена таблица сравнения размера этих библиотек. Стоит взглянуть только на первые две строки, которые обозначают размер всех статических и динамических библиотек и станет понятно, почему Alpine легче Debian.​
    [IMG]

    Теперь меняем первую строку в Dockerfile на
    ⁡FROM golang:1.25-alpine3.22
    ⁡ и уже можем наблюдать существенные изменения (578MB -> 337MB)​
    [IMG]

    Этап 3. Multi-stage сборка
    Вкратце ее смысл в том, что приложение собирается/компилируется/устанавливает зависимости в одном образе (так называемом builder-стейдже), где есть весь инструментарий, а затем копируется в образ, где нет ничего лишнего. Это маст-хев именно для компилируемых приложений.

    Также существуют некоторые хитрости при работе с Go, которые позволяют еще больше сократить размер образа.

    Во-первых, при сборке бинарника в builder-стейдже можно указать переменную
    ⁡CGO_ENABLED=0
    ⁡ , если не используются разделяемые библиотеки (по-простому это код, который лежит отдельно от программы в .dll-, .so- или .dylib-файлах) - это позволит не добавлять в бинарник лишние C-шные зависимости.

    Во-вторых флаги
    ⁡-ldflags="-s -w"
    ⁡ , которые уберут символьные таблицы и debug-информацию, что конечно сократит информативность стектрейсов, но зато бинарник опять таки станет меньше.

    Напоследок, если базовый образ builder-стейджа это Golang, базируемый на Alpine, то финальный базовый образ - это чистый Alpine. Именно это я и имел ввиду, когда писал о том, что приложение копируется в образ, где нет ничего лишнего.

    Применим же multi-stage сборку на практике и исправим наш Dockerfile
    Code
    FROM golang:1.25-alpine3.22 AS builder

    WORKDIR /app

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

    COPY . .
    RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bot

    FROM alpine

    COPY --from=builder /app/bot /bot

    CMD ["/bot"]
    Результат на лицо (337MB -> 9.02MB)
    [IMG]

    Этап 4. Дополнительные решения
    Multi-stage сборка это уже достаточный продакшен-вариант для большинства приложений. Но что если нужна максимальная экономия места?
    Первый способ - использовать Scratch, вместо Alpine как финальный базовый образ. Это буквально пустой образ, используемый для построения минимальных образов.

    На официальной странице Scratch в докер хабе минимальные образы описывают так:​
    Второй способ - использовать UPX (сокр. от Ultimate Packer for eXecutables, а не сраный казик :roflanebalo:)
    Использование Scratch может создать некоторые проблемы при запуске нагруженных приложений с большИм количеством зависимостей, так как они могут быть завязаны на системных библиотеках. Также на нем сложнее проводить отладку и хотфиксы, поэтому этот вариант очень редко используют в проде.
    Использование UPX, насколько я знаю, из проблем приносит только небольшое замедление старта приложения и рост потребления оперативной памяти.​

    Я использую оба способа, так как для моего тг-бота с этим нет никаких проблем и единственное, что нам предстоит скопировать в финальный образ помимо бинарника - это CA-сертификаты, так как без них невозможно установить TLS-соединение с Telegram API.

    Напоследок меняем Dockerfile
    Code
    FROM golang:1.25-alpine3.22 AS builder

    WORKDIR /app

    COPY go.mod go.sum ./
    RUN go mod download \
    && apk add --no-cache upx

    COPY . .

    RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bot

    RUN upx --best --lzma /app/bot

    FROM scratch

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

    CMD ["/bot"]
    Конечный результат (9.02MB -> 3.87MB)
    [IMG]

    Надеюсь это поможет вам в будущих проектах, удачи!
    Мой ТГК, где я делюсь своим опытом, готовыми проектами и мыслями по коду, крипте и не только - https://t.me/touchingcode
     
  2. oiiaioiiao
    статья уровня хабр, респект, продублируй на хабр рил, там такие статьи любят
    The post was merged to previous Feb 10, 2026
    про скратч не знал, интересно, кст ещё есть distroless debian, там минимальный набор для запуска dynamically linked binaries, типо glibc и сертификаты, т.к. не каждый билд полностью статичен, для этого нужно изъебнуться

    ещё по поводу alpine, некоторые приложения на нем 5% медленнее работают, т.к. musl медленее чем glibc, но не во всех случаях, вроде как
     
    1. DearFriend Topic starter
      avataroiiaioiiao, спасибо за совет, продублирую на хабр) Про distroless слышал, его описывают как что-то среднее между alpine и scratch как раз
Loading...