При создании контейнеров есть много нюансов, и один из них, это размер образа на выходе. Чем он меньше - тем лучше, тем быстрее мы можем его развернуть. И разница в размере может достигать 70-ти кратного (а в некоторых случаях и более), и в данном примере я вам покажу как его уменьшить.

Когда мы создаём docker образ первым делом многие начинают использовать стандартные официальные образы:

FROM golang
FROM nginx
FROM openjdk

Как вы заметили есть официальный Docker образ для Go.

$ docker image list
golang    latest       1c1309ff8e0d        10 days ago         779MB

Но, ничоси, 779 MB для пустого образа, да ещё и без нашего приложения... Это жесть, и только псих будет его использовать! 😾

Существует так же и легковесный образ на основе ОС Linux Alpine Docker образ для Go.

Подробнее можно прочитать про ОС здесь - Alpine linux.

FROM golang:alpine

Данный образ гораздо меньше, 269 MB, но всё ещё великоват для продакшен образа без чего либо внутри:

$ docker image list
golang     alpine      bbab7aea1231        7 weeks ago         269MB

Использование Multi-stage сборок

В докере есть multi-stage сборки, в результате мы можем собирать наше приложение в докер образе с alpine и дальше собранное приложение (бинарники и конфиги) пробрасывать в scratch образ.

Multi-stage сборки новая фишка докера, и требуется версия Docker 17.05 или выше на демоне и клиенте. (docs.docker.com)

OK, пришло время собрать наш минималистичный обаз с использованием multi-stage сборок.

Перед этим рассмотрим docker scratch образ, который весит всего 2MB. Идеально для нашего бинарного golang приложения.

# STEP 1 build executable binary
FROM golang:alpine as builder
COPY . $GOPATH/src/mypackage/myapp/
WORKDIR $GOPATH/src/mypackage/myapp/
#get dependancies
RUN go get -d -v
#build the binary
RUN go build -o /go/bin/hello

# STEP 2 build a small image
# start from scratch
FROM scratch
# Copy our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
ENTRYPOINT ["/go/bin/hello"]

Вот это уже сильно круче, всего лишь 21.2 MB вместе со всем что нужно для запуска нашего go приложения:

$ docker image list
hello    latest        bbab7aea1234       3 hours ago         21.2MB

Но и это не всё! Мы можем его ещё оптимизировать убрав информацию для отладки и скомпилировав только для Linux Target'а и отключив кросс-компилляцию, вот как это делается:

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags=”-w -s” -o /go/bin/backoffice

Теперь наш образ весит ещё меньше, а именно - 13.6 MB, и теперь он готов к продакшену. (Если есть идеи как ещё можно уменьшить размер пишите, у меня меньше не получилось)

$ docker image list
hello    latest        bbab7aea1234       2hours ago         13.6MB

Давайте соберём более защищёный и безопасный docker образ

Для начала несколько замечаний:

  • Запускайте только один процесс в контейнере.
  • НИКОГДА не запускайте процессы от root пользователя внутри контейнера.
  • Никогда не храните ваши данные внутри контейнера, храните их в сберегательной кассе, ну или в volume :)
  • Никогда не храните учетные данные в контейнере, делайте это в volume
  • Поддерживайте ваши образы в актуальном состоянии
  • Проверяйте каждый раз сторонние контейнеры, которые вы используете
  • Используйте тулзы для проверки на безопасность, например docker-security-scanning
  • Да прибудет с вами сила 🙏

Сперва нам нужно создать нового пользователя appuser в builder образе и скопировать файл /etc/passwd из builder в scratch образ. Теперь мы можем использовать пользователя appuser для запуска нашего приложения.

# STEP 1 build executable binary
FROM golang:alpine as builder
# Create appuser
RUN adduser -D -g '' appuser
COPY . $GOPATH/src/mypackage/myapp/
WORKDIR $GOPATH/src/mypackage/myapp/
#get dependancies
RUN go get -d -v
#build the binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags=”-w -s” -o /go/bin/backoffice

# STEP 2 build a small image
# start from scratch
FROM scratch
COPY --from=builder /etc/passwd /etc/passwd
# Copy our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
USER appuser
ENTRYPOINT ["/go/bin/hello"]

Всегда экспортируйте порты > 1024 если есть возможность

OK, теперь у нас уже немного более безопасный образ. Но если мы открываем в докере порт < 1024, нам нужны дополнительные привилегии для этого, что менее безопасно.

Ну раз такое дело, то давайте пропишем в EXPOSE порт > 1024

COPY --from=builder /etc/passwd /etc/passwd
# Copy our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
USER appuser
EXPOSE 9292
ENTRYPOINT ["/go/bin/hello"]

Добавляем SSL CA сертификаты

Замечательно, у нас почти полностью защищён образ, но можно сделать его ещё более защищённым, для этого воспользуемся SSL сертификатами ?

По умолчанию scratch образ не поддерживает работу SSL CA certificates. Но с multi-step сборкой мы можем решить и эту проблему:

# STEP 1 build executable binary
FROM golang:alpine as builder
# Install SSL ca certificates
RUN apk update && apk add git && apk add ca-certificates
# Create appuser
RUN adduser -D -g '' appuser
COPY . $GOPATH/src/mypackage/myapp/
WORKDIR $GOPATH/src/mypackage/myapp/
#get dependancies
RUN go get -d -v
#build the binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags=”-w -s” -o /go/bin/backoffice

# STEP 2 build a small image
# start from scratch
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
# Copy our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
USER appuser
ENTRYPOINT ["/go/bin/hello"]

Комментарии