docker 17.05 の multi-stage build で image サイズを30分の1にしてみる

# きっかけ

[Kubernetes best practices: How and why to build small container images](https://cloudplatform.googleblog.com/2018/04/Kubernetes-best-practices-how-and-why-to-build-small-container-images.html) に docker image size を小さくする tips について書かれていたので、実際に試してみました。

**Kubernetes ではパフォーマンス上 image size は小さい方が良い** というのはあると思いますが、他のユースケースでも image size が小さい方が嬉しいことは多いと思います。

すぐに忘れちゃうので、基本的には自分の備忘録のために残しておきます。

# その前に multi-stage build ってなに？

[Docker Documentation](https://docs.docker.com/develop/develop-images/multistage-build) に詳細が記載されています。

Docker 17.05 以降のバージョンから使えるようになった機能です。
1つの Dockerfile 内で複数のベースイメージを使うことができ、最終的に一つの image をつくることができます。
**メンテナンス性を保ったまま、Dockerfile の最適化**などを行うことができます。

端的にいうと **Builder-Pattern を使わずに Docker Image Size を小さくする** ことができます。

今回は便宜上、単一のベースイメージから作成する Dockerfile を single-stage と書くことにします。

# 今回の構成
Docker 17.09.0-ce (Docker for Mac)

```bash
.
├── multi-stage
│   ├── Dockerfile
│   └── main.go
└── single-stage
    ├── Dockerfile
    └── main.go
```
https://github.com/ight-reco/go-docker-multistage に今回の構成を置いています。

# 実際にやってみる

## single-stage
single-stage の docker image から作ってみます。

```dockerfile:single-stage/Dockerfile
# golang app build & 実行用
FROM golang:alpine
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp

EXPOSE 8080
ENTRYPOINT ./goapp
```
```go:single-stage/main.go
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);
}
```
```bash
$ 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`ぐらいでした。
念のため実行してみます。

```bash
$ docker run -it -p 8080:8080 builder-pattern
Running http://localhost:8080
```
```bash
$ curl localhost:8080
Hello, World
```
無事実行できているようです！

## multi-stage

同様に multi-stage image を作っていきます。
multi-stage なので `FROM` が2回出現します。
`main.go` は single と同じなので割愛します。

```dockerfile:multi-stage/Dockerfile
# 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
```

```bash
$ 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](https://docs.docker.com/develop/develop-images/multistage-build) と呼ばれるパターンでビルドと実行を分ければ multi-stage build 機能を使わずに実現可能ですが、やはり複雑性が増してしまうので multi-stage build を採用するで良いと思います！

今回はすごい極端な例ですので、実際にはもっと image size が大きくなるとは思います。