きっかけ
Kubernetes best practices: How and why to build small container images に docker image size を小さくする tips について書かれていたので、実際に試してみました。
Kubernetes ではパフォーマンス上 image size は小さい方が良い というのはあると思いますが、他のユースケースでも image size が小さい方が嬉しいことは多いと思います。
すぐに忘れちゃうので、基本的には自分の備忘録のために残しておきます。
その前に multi-stage build ってなに?
Docker Documentation に詳細が記載されています。
Docker 17.05 以降のバージョンから使えるようになった機能です。
1つの Dockerfile 内で複数のベースイメージを使うことができ、最終的に一つの image をつくることができます。
メンテナンス性を保ったまま、Dockerfile の最適化などを行うことができます。
端的にいうと Builder-Pattern を使わずに Docker Image Size を小さくする ことができます。
今回は便宜上、単一のベースイメージから作成する Dockerfile を single-stage と書くことにします。
今回の構成
Docker 17.09.0-ce (Docker for Mac)
.
├── multi-stage
│ ├── Dockerfile
│ └── main.go
└── single-stage
├── Dockerfile
└── main.go
https://github.com/ight-reco/go-docker-multistage に今回の構成を置いています。
実際にやってみる
single-stage
single-stage の docker image から作ってみます。
# golang app build & 実行用
FROM golang:alpine
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
EXPOSE 8080
ENTRYPOINT ./goapp
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World")
});
fmt.Println("Running http://localhost:8080");
http.ListenAndServe(":8080", nil);
}
$ docker build -t single-stage ./single-stage
...
Successfully tagged single-stage:latest
$ docker images --format "table {{.Repository}}\t{{.Size}}"
REPOSITORY SIZE
single-stage 382MB
...
build できました。golang:alpine
を使っていても 382MB
ぐらいでした。
念のため実行してみます。
$ docker run -it -p 8080:8080 builder-pattern
Running http://localhost:8080
$ curl localhost:8080
Hello, World
無事実行できているようです!
multi-stage
同様に multi-stage image を作っていきます。
multi-stage なので FROM
が2回出現します。
main.go
は single と同じなので割愛します。
# golang app build 用
FROM golang:alpine AS build-env
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
# golang app 実行用 (ここのベース image は最小限に)
FROM alpine
WORKDIR /app
# build した goapp を実行用に COPY
COPY --from=build-env /app/goapp /app
EXPOSE 8080
ENTRYPOINT ./goapp
$ docker build -t multi-stage ./multi-stage
...
Successfully tagged multi-stage:latest
$ docker images --format "table {{.Repository}}\t{{.Size}}"
REPOSITORY SIZE
multi-stage 10.8MB
single-stage 382MB
...
10.8MB
!!!! 大幅に小さくなりました!
なんでそうなるの?
golang の場合、build するとワンバイナリになるので、基本的にはビルドされたバイナリさえあれば動作します (HTTPS 通信など一部のケースではその場限りではない)。
そのため、ビルド時だけに必要なファイルは実行用イメージには含めずに作成することで image size を抑えることができるようです。
まとめ
Builder-Pattern と呼ばれるパターンでビルドと実行を分ければ multi-stage build 機能を使わずに実現可能ですが、やはり複雑性が増してしまうので multi-stage build を採用するで良いと思います!
今回はすごい極端な例ですので、実際にはもっと image size が大きくなるとは思います。