自分なりのGoアプリケーションDockerfileのベストプラクティス

自分なりのGoアプリケーションDockerfileのベストプラクティス

February 27, 2020
Docker, Programming
docker, Dockerfile, golang
  • 最終的に作成される Docker Image が軽量になること
  • Docker Build にかかる時間を短縮する

を目標としている

フォルダ構成

$ tree .
.
├── Dockerfile
├── Makefile
├── pkg_a
│   ├── pkg_a.go
│   └── pkg_a_test.go
├── pkg_b
│   ├── pkg_b.go
│   └── pkg_b_test.go
├── go.mod
├── go.sum
├── main.go

標準的な Go アプリケーションの構成 ライブラリ管理は Go Modules を使います。

Dockerfile

Dockerfile は以下のようになっています。 工夫点はコメントで番号つけてます。詳細は後述します

# ①
FROM golang:1.13 as builder

# ②
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /go/<アプリのリポジトリ名>

# ③
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# ④
RUN make

# ⑤
FROM alpine
RUN apk add --no-cache ca-certificates

# ⑥
COPY --from=builder /go/<アプリのリポジトリ名>/app /app
CMD ["/app"]

説明

① multi-stage builds を使い Docker Image の軽量化を行う

Docker にはmulti-stage buildsという機能があり、これを使うと1つの Dockerfile 内で複数のFROMを指定し、ビルドのベースとなる Image を分けることができます。

これによりアプリケーションのビルド時のみ必要となるパッケージなどを、最終的な Image から省き、Image サイズを削減するというのが簡単に行えます。

Go の場合、Go ランタイムはビルド時のみ必要でバイナリにビルドして実行する場合は不要なので、最初の build 用の Image ベースは golang, 最終的な Image のベースは alpine としています。

Image のサイズとしてはgolang:1.13が 5.5GB、alpine:latestが 2.68MB なので、そのまま golang を最終的なイメージベースとするより 5GB 以上のサイズ削減になります。

② Go のビルド設定

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64

でビルドの設定を行なっています

特に重要なのがCGO_ENABLED=0で静的バイナリの生成を指定してます。 これ指定しないと、alpine で実行するときにエラーになります。

③ 依存ライブラリのダウンロードはなるべくキャッシュをきかせる

なるべく Docker build 時にキャッシュをきかせて、ビルド速度を上げるために、リポジトリ内の全てのファイルをコピーする前に、Go Modules 系のファイルだけコピーして依存ライブラリのダウンロードを行なっています。

Docker のビルドは1行を1レイヤーとして考え、そのレイヤーで変更がなければキャッシュを利用し、変更があれば、それ以降のレイヤー全てがキャッシュ使われなくなります。

そのため実際は以下でも動くのですが、アプリのソースコードを修正しただけでも毎回依存ライブラリのダウンロードが発生してしまいます。

- COPY go.mod go.sum ./
- RUN go mod download
- COPY . .
- RUN make
+ COPY . .
+ RUN make

そのため、事前にgo.mod, go.sumだけをコピーしています。 この場合、依存ライブラリを新たに追加しない限りは、毎回キャッシュが使われて Docker build にかかる時間を大幅に短縮できます。

④ Makefile 使う

RUN go build -o appとかでもいいんですが、Makefile を用意しておくと、Dockerfile がスッキリして見栄えがよくなるかと思います。

ちなみに Makefile は以下のようになっています。

all: test build

setup:
	go get
test:
	go test ./...
build:
	go build -o app

⑤ 最終的な Image のベースは alpine を利用する

multi-stage build については前述の通りです。 mulit-stage build で最終的な Image のベースとしては、サイズを意識するなら alpine 一択かなと思ってます。 (運用時にコンテナ内に exec で入ってデバッグ作業とかしたいなら別の Image とかの方が良いかもですが)

⑥ 別のステージで作成したアプリのバイナリファイルをコピーする

FROM golang:1.13 as builder
COPY --from=builder /go/<アプリのリポジトリ名>/app /app

FROM golang:1.13 as builderで最初の golang:1.13 を使った build の間にbuilderという alias をつけてます。

これを使ってCOPY --from=builder /go/<アプリのリポジトリ名>/app /appとすることで、golang:1.13 でビルドしたアプリのバイナリファイルを alpine の中に持ってくることができます。