Предыстория
Как-то раз, в результате многих сверхурочных часов кропотливой работы, мне удалось разработать простейшее Hello world приложение на Go. И хотя оно всего-навсего производило вывод на экран, а затем заканчивало свою работу, мой начальник был настолько впечатлен, что попросил меня опубликовать мое детище, в написании которого я реализовал весь свой потенциал.
Схема проекта была следующей.
.
├── go.mod
└── hello.go
Код hello.go
выглядел вот так:
package mainfunc main() {
println("hello world!")
}
И еще мой начальник попросил использовать docker, чтобы мы выглядели посовременнее.
Первая попытка
Расспросив своих знакомых с целью получить полезные советы, я выяснил, что было бы неплохо скомпилировать весь процесс в docker, и немного поработав, я получил следующий Dockerfile
:
FROM golang:alpine
WORKDIR /build
COPY hello.go .
RUN go build -o hello hello.go
CMD [". /hello"]
И чтобы собрать образ:
$ docker build -t hello:v1 .
Готово. Давайте посмотрим, что у нас получилось:
$ docker run -it --rm hello:v1 ls -l /build
total 1260
-rwxr-xr-x 1 root root 1281547 Mar 6 15:54 hello
-rw-r--r-- 1 root root 55 Mar 6 14:59 hello.go
Весь мой код упакован в образ. Но я не могу позволить себе писать плохой код, ведь если его увидит операционистка, то она будет надо мной потешаться
Посмотрим на размер зеркала. Говорят, чем больше образ, тем медленнее pull.
$ docker docker images | grep hello
hello v1 2783ee221014 44 minutes ago 314MB
Ого, 314 МБ, неужели docker build превратила Go в программу на Java?
Давайте разберемся, почему он такой большой:
Смотрите, мы уже получаем 300+МБ еще до того, как мы запустим первую инструкцию (WORKDIR). Это какая-то жесть!
В любом случае, давайте запустим его и посмотрим, как он работает:
$ docker run --it --rm hello:v1
hello world!
Ну тут проблем нет, по крайней мере, он работает!
Вторая попытка
После долгих исследований и нескольких советов от знакомых мы обнаружили, что используемый нами базовый образ был слишком большим.
$ docker images | grep golang
golang alpine d026981a7165 2 days ago 313MB
Один мой знакомый сказал мне, что я могу сначала скомпилировать код, а затем скопировать его без огромного базового образа, но это легче сказать, чем сделать. Приложив к этому некоторые усилия, я получил следующий Dockerfile
:
FROM alpine
WORKDIR /build
COPY hello .
CMD [". /hello"]
Попробуем его запустить:
$ docker build -t hello:v2 .
...
=> ERROR [3/3] COPY hello . 0.0s
------
> [3/3] COPY hello . :
------
failed to compute cache key: "/hello" not found: not found
Так, hello
не найден — значит, я забыл сначала скомпилировать hello.go. Попробуем снова~
$ go build -o hello hello.go
Затем запустим docker build -t hello:v2 ..
Тут уже все без проблем. Сделаем глубокий вдох и запустим:
$ docker run -it --rm hello:v2
standard_init_linux.go:228: exec user process caused: exec format error
Ошибка! Что ж, формат не тот поэтому, что наша машина для разработки не на linux. Еще разок~
$ GOOS=linux go build -o hello hello.go
Перезапустим docker build. Наконец все готово, и мы можем запустить:
$ docker run --it --rm hello:v2
hello world!
Никаких проблем. Давайте посмотрим содержимое и размер:
$ docker run --it --rm hello:v2 ls -l /build
total 1252
-rwxr-xr-x 1 root root 1281587 Mar 6 16:18 hello
Внутри только hello
. Больше не нужно переживать о людях, насмехающихся над моим кодом ~
$ docker images | grep hello
hello v2 0dd53f016c93 53 seconds ago 6.61MB
hello v1 ac0e37173b85 25 minutes ago 314MB
Вау, 6.61МБ, прекрасно!
Смотрите, теперь у нас только 5.3МБ перед первой командой (WORKDIR), я уже счастлив!
Третья попытка
В ответ на мое невинное хвастовство кое-кто все-таки опустил меня на землю, сказав, что сейчас популярно создавать сборки в несколько этапов. Так что же не так со вторым способом? Поразмыслив над этим, я понял, что нам стоит реализовать возможность собирать образ docker прямо из кода Go, что потребует от нас три следующие шага:
Скомпилировать код Go нативно (если он включает кроссплатформенную cgo компиляцию, это будет более проблематично).
2. Создать образ docker с скомпилированным исполняемым файлом.
3. Написать shell-скрипт или makefile, чтобы запускать все это с помощью одной команды.
Многоэтапная сборка помещает все это в Dockerfile — никаких утечек исходного кода, никаких скриптов для компиляции на разных платформах и образ минимального размера.
Наконец-то я написал следующий Dockerfile, ни одной строкой больше, ни одной строкой меньше:
FROM golang:alpine AS builder
WORKDIR /build
ADD go.mod .
COPY . .
RUN go build -o hello hello.go
FROM alpine
WORKDIR /build
COPY --from=builder /build/hello /build/hello
CMD [". /hello"]
Первый FROM
начинается со сборки builder-образа для компиляции исполняемого файла hello
, а второй From
начинается с копирования исполняемого файла hello
из первого образа и использования наименьшего возможного базового образа alpine
, чтобы окончательный образ было как можно меньше. Причина, по которой мы не используем scratch
меньшего размера, заключается в том, что scratch
на самом деле ничего в себя не включает, поэтому у нас даже нет возможности просмотреть его в случае возникновения проблемы, а alpine
весит всего 5МБ, так что это не окажет большого влияния на наш сервис. Я расскажу о создании образов на основе scratch
чуть позже в этой статье.
Давайте запустим его и убедимся, что все работает:
$ docker run --it --rm hello:v3
hello world!
Никаких проблем, как и ожидалось! Посмотрим на размер:
$ docker images | grep hello
hello v3 f51e1116be11 8 hours ago 6.61MB
hello v2 0dd53f016c93 8 hours ago 6.61MB
hello v1 ac0e37173b85 8 hours ago 314MB
Размер образа точно такой же, как и во втором методе. И посмотрите на содержимое образа:
$ docker run -it --rm hello:v3 ls -l /build
total 1252
-rwxr-xr-x 1 root root 1281547 Mar 6 16:32 hello
Оно содержит только исполняемый файл hello
. Замечательно!
Это по сути то же самое, что и второй окончательный образ, но мы упростили процесс до использования только Dockerfile
и запуска всего одной команды, поэтому мне не нужно задействовать всю эту муть с shell
и makefile
.
Доводим до совершенства
Теперь мои товарищи по команде считают эту работу идеальной и хвалят меня! Однако каждый раз, когда я пишу такой Dockerfile
, меня все равно это напрягает, поэтому я написал специальный инструмент. Позвольте мне вам его продемонстрировать~
# Сначала установите его
$ go install github.com/zeromicro/go-zero/tools/goctl@latest
# А теперь вы можете создать файл Docker с помощью всего одной команды
$ goctl docker -go hello.go
Готово! Посмотрим на сгенерированный Dockerfile
.
FROM golang:alpine AS builder
LABEL stage=gobuilder
ENV CGO_ENABLED 0
ENV GOOS linux
RUN apk update --no-cache && apk add --no-cache tzdata
WORKDIR /build
ADD go.mod .
ADD go.sum .
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o /app/hello . /hello.go
FROM alpine
RUN apk update --no-cache && apk add --no-cache ca-certificates
COPY --from=builder /usr/share/zoneinfo/America/New_York /usr/share/zoneinfo/America/New_York
ENV TZ America/New_York
WORKDIR /app
COPY --from=builder /app/hello /app/hello
CMD [". /hello"]
Вот несколько моментов, которые нужно здесь не мешало бы объяснить:
cgo
отключен по умолчанию.
Удалено сообщение отладки
-ldflags="-s -w"
для уменьшения размера образа.
Установлено ca-certificates, чтобы не было проблем с использованием TLS сертификатов.
tzdata
устанавливается в builder-образ билдера, а в окончательный образ копируется только необходимый часовой пояс.
Автоматически устанавливает местный часовой пояс, чтобы в контейнере мы видели New York.
Давайте посмотрим на размер образа, созданного с помощью этого автоматически сгенерированного Dockerfile
.
$ docker images | grep hello
hello v4 94ba3ece3071 4 hours ago 6.66MB
hello v3 f51e1116be11 8 hours ago 6.61MB
hello v2 0dd53f016c93 8 hours ago 6.61MB
hello v1 ac0e37173b85 9 hours ago 314MB
Он немного больше, потому что мы скопировали ca-certificates и tzdata
. Проверим это:
Давайте посмотрим, что у нас в финальном образе:
$ docker run -it --rm hello:v4 ls -l /app
total 832
-rwxr-xr-x 1 root root 851968 Mar 7 08:36 hello
Все также только hello
, а размер файла был уменьшен с 1281КБ до 851КБ. Запустим его и посмотрим исполняемый shell-файл:
$ docker run --it --rm hello:v4
hello world!
И вы можете указать базовый образ scratch при создании Dockerfile, чтобы образ был меньше, но вы не можете войти напрямую через sh
.
$ goctl docker -base scratch -go hello.go
Размер стал в несколько раз меньше:
$ docker images | grep hello
hello v5 d084eed88d88 4 seconds ago 1.07MB
hello v4 94ba3ece3071 15 hours ago 6.66MB
hello v3 f51e1116be11 4 days ago 6.61MB
hello v2 0dd53f016c93 4 days ago 6.61MB
hello v1 ac0e37173b85 4 days ago 314MB
И посмотри, что в зеркале:
Команда docker кросс-билдинг образов. Если вы используете чипы Apple Silicon, вы можете передать --platform linux/amd64
для создания образов докеров под linux/amd64.
Вы также можете использовать upx
для уменьшения размера бинарника. Установите upx
на этапе сборки и запустите upx /app/hello
после сборки.
$ docker images | grep hello
hello v7 491d2cf669c7 16 minutes ago 548kB # amd64 after upx
hello v6 0a80622b3310 7 days ago 972kB # amd64
hello v5 d084eed88d88 13 days ago 1.07MB # arm64
Ладно, ладно, на сегодня хватит Dockerfile
, пора дальше осваивать новые навыки~
Сам проект можно найти здесь: ссылка на Github.
Приглашаем всех желающих на открытый урок «Структуры». На занятии рассмотрим такие понятия, как: процесс определения структур, инкапсуляция полей структуры, определение методов структуры. Побеседуем про вложенные и анонимные структуры. Также постараемся успеть поговорить и про структурные тэги и их использование в контексте JSON, XML и СУБД. После занятия вы сможете создавать пользовательские типы данных и комбинировать их между собой. Регистрация на занятие по ссылке.