最小のNode.jsのDockerイメージを目指すスレ」、「JavaでもDockerでマルチステージビルド」というエントリーでは、Node.jsとJavaを使ったアプリケーションのイメージをなるべく小さくするトライアルをしました。

今度はGoでやってみます。ただし、Pure Goで最小というのはすでに方法があって、scratchという何も含まれないイメージを元に、静的リンクしたバイナリを配置するという方法です。

Goを使う場合に、一部cgoで使われたパッケージを利用したいこともあるでしょうし、雑にコマンドラインを利用することもあるだろう、ということで、今回も、できることを減らさずに(やりたいことにしたがって細かく作戦を微調整する必要がない)、なるべく小さく、という方針でいきたいと思います。

STEP1: マルチステージビルド

ということで、Node.jsで強力だった、マルチステージビルドを使ったネイティブコンパイラ環境をいきなり投入してみましょう。

main/main.go
package main

import (
    "fmt"
)

/*
#include <stdio.h>

void print_from_c() {
    printf("Hello from cgo\n");
}
*/
import "C"

func main() {
    fmt.Println("Hello World")
    C.print_from_c();
}

Goとcgoのコードの両方でHelloしています。

Dockerfileは前回のnode.js版とだいたい似ています。BuilderのイメージではCコンパイラとかも入れてGoのコードをビルドし、Runnerの方に最終成果物のみを入れて実行します。

Dockerfile
FROM alpine:latest as builder

RUN apk add --no-cache go \
        git \
        binutils-gold \
        curl \
        g++ \
        gcc \
        gnupg \
        libgcc \
        linux-headers \
        make \
        python
ADD main main
WORKDIR main
RUN go build

FROM alpine:latest as runner
RUN apk add --no-cache libc6-compat
COPY --from=builder ./main/main ./
CMD ./main

これでビルドすると、6.53MBです。Node.jsと比べると1桁、Javaと比べると2桁小さいですね。

dlopenを使う時はNode.jsのエントリーを参照してください。

STEP2: 小さくするコマンドラインオプション

Go歴がそこそこある人なら常識の

go build -ldflags "-s -w"

を試してみます。Dockerfileでビルドしているところにオプションを追加するだけです。これで少し小さいバイナリファイルが作成できます。

これで5.87MBになりました。1割近く小さくなりました。

STEP3: UPX

upx: Ultimate Packer for eXecutablesという、実行ファイルを小さくするコマンドがあります。Goはシングルバイナリで、ランタイムライブラリを内臓しているが、そこそこバイナリが大きくなってしまう(C言語と比べて)という欠点があります。upxはそのバイナリを小さくしてしまうというコマンドです。

Goの場合はupxを直接使うのではなく、goupxコマンド経由で使います。このコマンドはgo getで取得してくる必要があります。そのためにGOBIN環境変数などを設定します。

Dockerfile
FROM alpine:latest as builder

RUN apk add --no-cache go \
        git \
        binutils-gold \
        curl \
        g++ \
        gcc \
        gnupg \
        libgcc \
        linux-headers \
        make \
        python \
        upx
RUN mkdir /gobin
ENV GOBIN /gobin
ENV PATH $PATH:/gobin
ADD main main
WORKDIR main
RUN go build -ldflags "-s -w"
RUN go get github.com/pwaller/goupx
RUN goupx main

これで4.72MBです。Goのアプリケーションのサイズが1.5MBぐらいから500KBぐらいになり、25%ぐらい削減になりました。ちなみに、Alpine Linux本体のサイズが4.12MBですので、削減効果はかなり大きいということがわかります。

まとめ

マルチステージビルドを使えば、ビルドの方はイメージサイズを気にせずに、実行ファイルのサイズだけを気にしてDockerfileが書けます。STEP3のDockerfileをみてもらうと、あることに気づくと思います。

いままで、ファイルサイズが小さいイメージを作ろうとしたら、RUNコマンドのあとは && \ を付与して、1つのコマンドとして実行されるように一筆描きが強要されてしまいました。これは確かにサイズ縮小には聞きますが、途中でキャッシュがされないので、途中で失敗すると、時間のロスが大きいです。また、どの部分でエラーが起きたのかをエラーメッセージからエスパーしないといけません。

マルチステージになったら、コマンドを細かく1行ごとにRUNできます。これは途中でDockerイメージのビルドが失敗してなかをみたいときにとても助かります。また、こまめにキャッシュされるようになるため、途中でエラーが発生してなんども繰り返し実行しなければならない、というときにもビルド時間を極めて小さくすることができます。性能のために書きやすさやメンテナンス性の悪さを我慢する必要はなくなりました。apk addとかも、1パッケージごとに書いてもいいぐらいだと思います(特にサイズの大きなgccとかは)。

Scratchイメージを元にする方法と比べると、多少サイズは大きいですが、cgoが有効だし、Alpineなので、必要なツールやライブラリを別途ダウンロードして利用できますし、ネイティブコードが利用な高度なパッケージも自由に使えます。かなりGoライフが快適になるんじゃないでしょうか?