dockerのscratchイメージでgolangのWebアプリを動かす

[Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) [(参考訳v18.09ベース)](https://qiita.com/zembutsu/items/a96b68277d699f79418d)を参考に、golangで書いたWebアプリを動かそうとしたらハマったので、その記録を残します。

## ざっくりいうと

1. golangで書いたwebアプリ(サンプル)を、[dockerのscratchイメージ](https://hub.docker.com/_/scratch)上で動かそうとしたら起動で失敗
2. 調べたらダイナミックリンクでビルドされており、scratchイメージではファイルが足りなかったのが原因
3. スタティックリンクでビルドし直したら解決した

## 環境

* go 1.13.5
* docker 19.03.5

## ハマるまでの流れ

["Use multi-stage builds"](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#use-multi-stage-builds)にマルチステージビルドを使うgolangのサンプルがあります。ビルドするときに使うイメージとリリースするイメージを分けることで、リリースイメージに余計なもの入れなくてすみ、イメージのサイズも小さくできます。

それはよさそうだと言うことで、手元にあったWebアプリをサンプルにして次のようなDockerfileを用意しました。

```Dockerfile
FROM golang:1.13.5-alpine AS build
WORKDIR /go/src/sample-go-server
COPY ./app /go/src/sample-go-server
RUN go build -o /bin/sample-go-server

FROM scratch
COPY --from=build /bin/sample-go-server /bin/sample-go-server
EXPOSE 8080
ENTRYPOINT ["/bin/sample-go-server"]
```
sample-go-serverは、`hello world`を返す簡単なものです。

```golang
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		_, _ = fmt.Fprintf(w, "hello world")
	})
	log.Fatal(http.ListenAndServe(":8080", nil))
}

```

このDockerfileをビルドします。

```shell
❯ docker build -t sample-go-server .
❯ docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
sample-go-server         latest              0acf7d1e1ed7        42 minutes ago      7.45MB
```

runで起動すると思いきや次のようなエラーで失敗します。

```shell
❯ docker run -d -p 8080:8080 --name sample sample-go-server
1e097919ec5ac31839228646e2b14bd0434f56d74787d73b6afa172154829250

❯ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

❯ docker logs sample
standard_init_linux.go:211: exec user process caused "no such file or directory"
```

`"no such file or directory"`と言われましても…。何が足りないかを言ってくれ〜😫

## 解決方法

ググったところ以下が見つかりました。ありがとうインターネッツ。

* [Docker + Goバイナリでstandard_init_linux.go:178: exec user process caused "no such file or directory"と出た時の対処 - Qiita](https://qiita.com/katoken-0215/items/f3a502fe0c2044709012)
* [golangで書いたアプリケーションのstatic link化 - okzkメモ](http://okzk.hatenablog.com/entry/2016/08/03/234738)

netパッケージを含む場合はダイナミックリンクでビルドされ、それが原因でno such fileになるらしいです。
なので、さっそくこれに該当しているかを確認してみます。
ビルドで使ったalpineイメージには`file`コマンドが入っていないため、まずそれを入れます。パッケージは [Alpine Linux packages](https://pkgs.alpinelinux.org/contents)で探せます。

```shell
/bin # apk update && apk add file
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
v3.11.2-25-g58afcd742e [http://dl-cdn.alpinelinux.org/alpine/v3.11/main]
v3.11.2-24-g7cfe3a1534 [http://dl-cdn.alpinelinux.org/alpine/v3.11/community]
OK: 11261 distinct packages available
(1/2) Installing libmagic (5.37-r1)
(2/2) Installing file (5.37-r1)
Executing busybox-1.31.1-r8.trigger
OK: 12 MiB in 17 packages
/bin # which file
/usr/bin/file
```

`file`コマンドで確認してみると、たしかに`dinamically linked`となっていました。

```shell
/bin # file sample-go-server 
sample-go-server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, Go BuildID=5E6Qy3Li7DELFoSZUyv5/TdC1EDiTsXrg0i0ta2Xx/49v4RWXcyEm12mEJPV8f/wYNglNx2pwV9Z_45IRLn, not stripped
```

参考サイトにあった`CGO_ENABLED=0`でビルドをし直してみます。

```Dockerfile
FROM golang:1.13.5-alpine AS build
WORKDIR /go/src/sample-go-server
COPY ./app /go/src/sample-go-server
RUN CGO_ENABLED=0 go build -o /bin/sample-go-server

FROM scratch
COPY --from=build /bin/sample-go-server /bin/sample-go-server
EXPOSE 8080
ENTRYPOINT ["/bin/sample-go-server"]
```

たしかに`statically linked`に変わりました。

```
/bin # file sample-go-server 
sample-go-server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=cD81ASWTt8bngTyIxfpe/TdC1EDiTsXrg0i0ta2Xx/49v4RWXcyEm12mEJPV8f/rmgAW8vnYv0XAugzIkMj, not stripped
```

docker runでも無事起動できました。

```
❯ docker run -d -p 8080:8080 --name sample sample-go-server
c27e29aaa697ac7131995472377bb2ff58e6a6ebe6a8174fb8a9de64efc767d5

❯ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
c27e29aaa697        sample-go-server    "/bin/sample-go-serv…"   2 seconds ago       Up 1 second         0.0.0.0:8080->8080/tcp   sample

❯ curl http://localhost:8080
hello world
```

## 参考

* [Best practices for writing Dockerfiles | Docker Documentation](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
* [Dockerfileを書くためのベストプラクティス【参考訳】v18.09 - Qiita](https://qiita.com/zembutsu/items/a96b68277d699f79418d)
* [Docker + Goバイナリでstandard_init_linux.go:178: exec user process caused "no such file or directory"と出た時の対処 - Qiita](https://qiita.com/katoken-0215/items/f3a502fe0c2044709012)
* [golangで書いたアプリケーションのstatic link化 - okzkメモ](http://okzk.hatenablog.com/entry/2016/08/03/234738)