Best practices for writing Dockerfiles (参考訳v18.09ベース)を参考に、golangで書いたWebアプリを動かそうとしたらハマったので、その記録を残します。
ざっくりいうと
- golangで書いたwebアプリ(サンプル)を、dockerのscratchイメージ上で動かそうとしたら起動で失敗
- 調べたらダイナミックリンクでビルドされており、scratchイメージではファイルが足りなかったのが原因
- スタティックリンクでビルドし直したら解決した
環境
- go 1.13.5
- docker 19.03.5
ハマるまでの流れ
"Use multi-stage builds"にマルチステージビルドを使うgolangのサンプルがあります。ビルドするときに使うイメージとリリースするイメージを分けることで、リリースイメージに余計なもの入れなくてすみ、イメージのサイズも小さくできます。
それはよさそうだと言うことで、手元にあったWebアプリをサンプルにして次のような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
を返す簡単なものです。
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をビルドします。
❯ 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で起動すると思いきや次のようなエラーで失敗します。
❯ 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
- golangで書いたアプリケーションのstatic link化 - okzkメモ
netパッケージを含む場合はダイナミックリンクでビルドされ、それが原因でno such fileになるらしいです。
なので、さっそくこれに該当しているかを確認してみます。
ビルドで使ったalpineイメージにはfile
コマンドが入っていないため、まずそれを入れます。パッケージは Alpine Linux packagesで探せます。
/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
となっていました。
/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
でビルドをし直してみます。
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