こんばんは、ねじねじおです。
Go言語で書いたアプリの Docker イメージを軽量化するには、マルチステージビルドを使って scratch をベースに構築するのがよいと聞いて、やってみました。
準備
まず、サンプルとして小さな Web API を echo で作ります。
$ mkdir app
$ cd app
$ go mod init example.com/example
$ go get github.com/labstack/echo/v4
下記のディレクトリ構成で server.go と Dockerfie を追加します。
-example
|-app
│-Dockerfile
│-go.mod
│-go.sum
|-server.go
package main
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
// Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.GET("/", func (c echo.Context) error {
return c.JSON(http.StatusOK, []string{
"Hello, World!",
time.Now().Format(time.RFC3339),
})
})
// Start server
e.Logger.Fatal(e.Start(":1323"))
}
FROM golang:1.14.7
WORKDIR /go/src/app
COPY ./ ./
RUN go mod download
RUN go build -o ./server ./server.go
ビルドして実行してみます。
$ docker build ./ -t example
$ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server
動作確認。
$ curl http://localhost:1323/
["Hello, World!","2020-08-10T19:51:15+09:00"]
成功!!
では、イメージサイズを確認してみます。
$ docker images example
REPOSITORY TAG IMAGE ID CREATED SIZE
example latest 5ef805ca57ec 2 minutes ago 908MB
908MB って、でかいのかな?
基準がわからないので、レイヤーを確認します。
$ docker history example
IMAGE CREATED CREATED BY SIZE COMMENT
5ef805ca57ec 2 minutes ago /bin/sh -c go build -o ./server ./server.go 20.5MB
13249ee42cff 2 minutes ago /bin/sh -c go mod download 77.3MB
a3e0d3e82f45 3 minutes ago /bin/sh -c #(nop) COPY dir:cccd1ec30e0093efe… 4.96kB
aa1a8385bb42 44 hours ago /bin/sh -c #(nop) WORKDIR /go/src/app 0B
baaca3151cdb 3 days ago /bin/sh -c #(nop) WORKDIR /go 0B
... 省略 ...
マルチステージビルドを使うと、go mod download と go build の領域を節約できそうです。
マルチステージビルドを使う
Dockerfileを変更します。
builderステージでコンパイル、できたバイナリファイルをコピーしてproductionステージを作ります。
FROM golang:1.14.7 AS base
WORKDIR /go/src/app
FROM base AS builder
COPY ./ ./
RUN go mod download
RUN go build -o ./server ./server.go
FROM base AS production
COPY --from=builder /go/src/app/server ./
ビルドします。
$ docker build ./ -t example
イメージのサイズとレイヤーを確認。
$ docker images example
REPOSITORY TAG IMAGE ID CREATED SIZE
example latest 62b3a8a02af3 3 minutes ago 822MB
$ docker history example
IMAGE CREATED CREATED BY SIZE COMMENT
62b3a8a02af3 3 minutes ago /bin/sh -c #(nop) COPY file:3d74d9674cd3943c… 12.2MB
aa1a8385bb42 44 hours ago /bin/sh -c #(nop) WORKDIR /go/src/app 0B
baaca3151cdb 3 days ago /bin/sh -c #(nop) WORKDIR /go 0B
... 省略 ...
イメージサイズは、822MB。少し小さくなりましたね。
動作確認です。
$ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server
$ curl http://localhost:1323/
["Hello, World!","2020-08-10T19:58:28+09:00"]
成功!
しかし、先ほどの docker history の出力をみると、バイナリをCOPYしてくるレイヤーは、たかだか 12.2MB 。
対して、イメージ全体のサイズは、822MB。
ベースのイメージが大きいのですね。
scratch をベースに最終イメージを構築する
Goの実行環境にはGoは不要なので、ミニマムに scratch をベースに最終イメージを構築してみます。
失敗 その1
FROM golang:1.14.7 AS base
WORKDIR /go/src/app
FROM base AS builder
COPY ./ ./
RUN go mod download
RUN go build -o ./server ./server.go
FROM scratch AS production
WORKDIR /go/bin
COPY --from=builder /go/src/app/server ./
ビルドします。
$ docker build ./ -t example
イメージのサイズとレイヤーを確認。
$ docker images example
REPOSITORY TAG IMAGE ID CREATED SIZE
example latest e1ba3f7f81f0 About a minute ago 12.2MB
$ docker history example
IMAGE CREATED CREATED BY SIZE COMMENT
e1ba3f7f81f0 2 minutes ago /bin/sh -c #(nop) COPY file:3d74d9674cd3943c… 12.2MB
06d1e3bea55e 12 minutes ago /bin/sh -c #(nop) WORKDIR /go/bin 0B
イメージサイズは、 12.2MB 。
劇的に小さくなりました!!
動作確認です。
$ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server
standard_init_linux.go:211: exec user process caused "no such file or directory"
エラーが発生して、起動できない。
いろいろ調べて見ると、builder と production で実行環境が異なるので、コンパイル時に CGO を無効にする必要がありました。
失敗 その2
コンパイル時に CGO を無効にするように変更して再チャレンジです。
FROM golang:1.14.7 AS base
WORKDIR /go/src/app
FROM base AS builder
COPY ./ ./
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./server ./server.go
FROM scratch AS production
WORKDIR /go/bin
COPY --from=builder /go/src/app/server ./
ビルドして実行します。
$ docker build ./ -t example
$ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server
起動しました!
動作確認です。
$ curl http://localhost:1323/
["Hello, World!","2020-08-10T11:53:39Z"]
成功!
いや、否。
タイムゾーンが無視されている。。
タイムゾーンは、 アジアの都市、東京 です。"+09:00" です。
そして成功へ
こちらのサイトに答えが書いてありました。
Using local time in a Golang Docker container built from Scratch
今回は、builder ステージがから /usr/share/zoneinfo をコピーする方法を採用しました。
FROM golang:1.14.7 AS base
WORKDIR /go/src/app
FROM base AS builder
COPY ./ ./
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./server ./server.go
FROM scratch AS production
WORKDIR /go/bin
COPY --from=builder /go/src/app/server ./
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
ビルドして実行します。
$ docker build ./ -t example
$ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server
動作確認です。
$ curl http://localhost:1323/
["Hello, World!","2020-08-10T21:11:29+09:00"]
タイムゾーンが反映されています!
イメージのサイズを確認します。
$ docker images example
REPOSITORY TAG IMAGE ID CREATED SIZE
example latest e673e01668c6 14 minutes ago 13.3MB
$ docker history example
IMAGE CREATED CREATED BY SIZE COMMENT
e673e01668c6 14 minutes ago /bin/sh -c #(nop) COPY dir:b39c255c3688d7205… 1.21MB
3865c579a2d0 25 minutes ago /bin/sh -c #(nop) COPY file:bafd1cb7f8670973… 12.1MB
06d1e3bea55e 34 minutes ago /bin/sh -c #(nop) WORKDIR /go/bin 0B
13.3 MB。
OK。
ねじねじおでした。
- 2020年8月15日 追記
- Go1.15でタイムゾーンデータベースをバイナリに組み込めるようになったので、こちら に方法を書きました。