はじめに
私は、コンテナイメージについてイメージサイズと同梱されるファイル数が小さければ小さいほど美しいと考えています。
ですが、この美学を阻むのが各種ライブラリです。
例えば、glibcといった基本的なライブラリ。世界中のタイムゾーンを扱うためのtzdata。
そして昨今の通信では欠かせないHTTPS通信を行うためのca-certificatesなどです。
本記事では、これらのパッケージをバイナリに埋め込んだものを作成し、scratchコンテナイメージにファイル1つをコピーするだけで動作するシングルバイナリの作り方をGo言語を使用して説明します。
記事要約
- Goを使うと
glibcに依存しないシングルバイナリが作れる - Goの
time/tzdataパッケージを使うとタイムゾーン情報をバイナリに埋め込める -
golang.org/x/crypto/x509roots/fallbackパッケージを使うとCA証明書をバイナリに埋め込める
本記事で目指すゴール
以下の項目を全て満たすシングルバイナリを作ることをゴールとします
- シングルバイナリで起動して
example-go-scratch-containerと表示できること - 現在時刻を取得して日本時間としてRFC3339形式で表示できること
-
https://example.comにアクセスしてHTTPステータスである200 OKが返ってくること
成功した場合には以下のような出力が出るものとします。
example-go-scratch-container
now: 2026-06-20T15:44:23+09:00
200 OK
使用した環境
- macOS Tahoe 26.5.1
- Go 1.26.3
- docker: 29.2.0
プログラムを作る
まずは以下のようなプログラムを作成します。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
fmt.Println("example-go-scratch-container")
jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
now := time.Now().In(jst)
fmt.Printf("now: %s\n", now.Format(time.RFC3339))
resp, err := http.Get("https://example.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status)
}
こちらをgolang:1.26.3-trixieイメージでビルドして実行してみましょう。
FROM golang:1.26.3-trixie
WORKDIR /build
COPY . .
RUN go build -trimpath -ldflags="-s -w" -o example-go-scratch-container .
ENTRYPOINT [ "/build/example-go-scratch-container" ]
$ docker build -t example-go-scratch-container:0.0.0 .
$ docker run -it --rm example-go-scratch-container:0.0.0
example-go-scratch-container
now: 2026-06-20T16:25:50+09:00
200 OK
ゴールの要件を満たすバイナリが作成できました。
それではこちらをDockerのマルチステージビルドを使用して、先ほどと同じイメージでビルドします。
ビルドされたバイナリをscratchイメージの中に同梱して実行してみましょう。
FROM golang:1.26.3-trixie AS build
WORKDIR /build
COPY . .
RUN go build -trimpath -ldflags="-s -w" -o example-go-scratch-container .
FROM scratch
COPY --from=build /build/example-go-scratch-container /example-go-scratch-container
ENTRYPOINT [ "/example-go-scratch-container" ]
$ docker build -t example-go-scratch-container:0.1.0 .
$ docker run -it --rm example-go-scratch-container:0.1.0
exec /example-go-scratch-container: no such file or directory
no such file or directoryと表示されますね。ファイルがあるのか確認してみましょう。
entrypointを書き換えて、存在しないファイルを指定して実行してみました。
$ docker run -it --rm --entrypoint="" example-go-scratch-container:0.1.0 /aaa
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "/aaa": stat /aaa: no such file or directory
Run 'docker run --help' for more information
本当にファイルがない場合にはstat /aaa: no such file or directoryと表示されるみたいです。
つまり、ビルドされたファイルはコンテナ内にありそうです。
では、ビルドされたexample-go-scratch-containerがどのようになっているのか確認してみましょう。
先ほど動作確認をしたイメージを使用して確認してみます。
$ docker run -it --rm --entrypoint="" example-go-scratch-container:0.0.0 sh
# ldd /build/example-go-scratch-container
linux-vdso.so.1 (0x0000f7e29b1c3000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000f7e29afc0000)
/lib/ld-linux-aarch64.so.1 (0x0000f7e29b186000)
libc.so.6を動的リンクして動作しているようですね。
scratchイメージはなにもないイメージのため、このように動的リンクしているバイナリは動作しません。
先ほどのno such file or directoryも、ライブラリが見当たらないことを指し示しているのかもしれません。
CGO_ENABLED=0を使って静的バイナリを作成する
GoにはCGO_ENABLEDという環境変数があります。
こちらをCGO_ENABLED=0と設定してビルドすると、使用するライブラリを内包した静的バイナリが作成できます。
こちらの環境変数を使用してイメージを作り直してみましょう。
FROM golang:1.26.3-trixie AS build
WORKDIR /build
COPY . .
-RUN go build -trimpath -ldflags="-s -w" -o example-go-scratch-container .
+RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o example-go-scratch-container .
FROM scratch
COPY --from=build /build/example-go-scratch-container /example-go-scratch-container
ENTRYPOINT [ "/example-go-scratch-container" ]
イメージをビルドしなおして実行してみます。
$ docker build -t example-go-scratch-container:0.2.0 .
$ docker run -it --rm example-go-scratch-container:0.2.0
example-go-scratch-container
panic: unknown time zone Asia/Tokyo
goroutine 1 [running]:
main.main()
example-go-scratch-container/main.go:13 +0x1e4
実行したところ、example-go-scratch-containerと表示されました。
しかし、その後の時刻表示とHTTPS通信の部分は実行できていません。
これらを修正していきましょう。
結果:
-
シングルバイナリで起動してexample-go-scratch-containerと表示できること -
現在時刻を取得して日本時間としてRFC3339形式で表示できること -
https://example.comにアクセスしてHTTPステータスである200 OKが返ってくること
time/tzdataでタイムゾーンをバイナリに組み込む
先ほどのイメージを実行した際のエラーを見てみましょう。
panic: unknown time zone Asia/Tokyo
goroutine 1 [running]:
main.main()
example-go-scratch-container/main.go:13 +0x1e4
unknown time zone Asia/Tokyoと表示されています。
実は、タイムゾーンを扱う際にはtzdataというパッケージが必要になります。
このパッケージには世界中のタイムゾーンに関するデータが入っており、基本的なマシンにはこのパッケージがインストールされています。
Goではこのパッケージを参照し、そこからタイムゾーンのデータを取得するようになっています。
scratchイメージにはタイムゾーンの情報はどこにもないため、エラーしたという状況です。
ところが、Go 1.15にてtime/tzdataというパッケージが追加されました。
このパッケージには名前の通り、tzdataパッケージに相当するデータが組み込まれています。
このパッケージをブランクインポートすることにより、tzdataパッケージ相当のデータをバイナリに組み込むことができます。
main.goのimportを以下のように編集します。
import (
"fmt"
"net/http"
"time"
+ _ "time/tzdata"
)
ではこちらのmain.goをビルドし、scratchイメージにてバイナリを実行してみましょう。
$ docker build -t example-go-scratch-container:0.3.0 .
docker run -it --rm example-go-scratch-container:0.3.0
example-go-scratch-container
now: 2026-06-20T17:22:32+09:00
panic: Get "https://example.com": tls: failed to verify certificate: x509: certificate signed by unknown authority
goroutine 1 [running]:
main.main()
example-go-scratch-container/main.go:21 +0x1c4
バイナリが実行され、日本時間の現在時刻も取得できました。
ですが、HTTPS通信のところでエラーしています。
こちらを修正していきましょう。
結果:
-
シングルバイナリで起動してexample-go-scratch-containerと表示できること -
: 現在時刻を取得して日本時間としてRFC3339形式で表示できること -
https://example.comにアクセスしてHTTPステータスである200 OKが返ってくること
golang.org/x/crypto/x509roots/fallbackでCA証明書をバイナリに組み込む
先ほどのエラーを確認しましょう。
panic: Get "https://example.com": tls: failed to verify certificate: x509: certificate signed by unknown authority
goroutine 1 [running]:
main.main()
example-go-scratch-container/main.go:21 +0x1c4
HTTPS通信を行う際、サーバーが提示してきた証明書の検証ができなくて失敗しています。
サーバーが提示してきた証明書を検証する際には、ca-certificatesというパッケージに同梱されているCA証明書を使って検証します。
こちらもtzdataと同様に、GoではOSに同梱されているCA証明書を探して見つかったものを使用します。
もちろんこちらもscratchイメージには存在しないため、検証用のCA証明書が見つからずにエラーしたという状況です。
Goプロジェクトのサブプロジェクトとしてgolang.org/x/crypto/x509roots/fallbackというパッケージがあります。
こちらもtime/tzdataと同様にCA証明書の内容を組み込むことができます。
main.goのimportを次のように編集します。
import (
"net/http"
"time"
_ "time/tzdata"
+
+ _ "golang.org/x/crypto/x509roots/fallback"
)
こちらは外部モジュールとなるのでgo mod tidyなんかを忘れずに。
ではこのmain.goをビルドして、scratchコンテナ上で動かしてみましょう。
$ docker build -t example-go-scratch-container:0.4.0 .
$ docker run -it --rm example-go-scratch-container:0.4.0
example-go-scratch-container
now: 2026-06-20T17:43:39+09:00
200 OK
バイナリが実行され、日本時間の現在時刻も取得でき、HTTPS通信もできるようになりました。
これにて、本記事のゴールを達成することができました。
結果:
-
シングルバイナリで起動してexample-go-scratch-containerと表示できること -
: 現在時刻を取得して日本時間としてRFC3339形式で表示できること -
https://example.comにアクセスしてHTTPステータスである200 OKが返ってくること
さいごに
本記事ではGo言語のCGO_ENABLED=0やtime/tzdata、golang.org/x/crypto/x509roots/fallbackを使うことによって、タイムゾーンやHTTPS通信が行えるシングルバイナリを作成していきました。
このシングルバイナリはscratchイメージに同梱しても動くため、とても美しいコンテナイメージが完成したと思います。
そしてGoのクロスコンパイルの容易性や、可搬性なんかが考えられている側面に触れられたと思います。
もちろん、この手法についてのデメリットもあります。
それは本来OSで共有すべきデータをバイナリに組み込んでいるため、バイナリ自体の容量が大きくなってしまいます。
せっかくなので今回ビルドしたイメージを用いて簡単に比較してみましょう。
| イメージ名 | 説明 | イメージサイズ |
|---|---|---|
| example-go-scratch-container:0.1.0 | scratchにコピー(動的リンク) | 7.81MB |
| example-go-scratch-container:0.2.0 |
CGO_ENABLED=0を設定 |
7.73MB |
| example-go-scratch-container:0.3.0 |
CGO_ENABLED=0, time/tzdataを追加 |
8.21MB |
| example-go-scratch-container:0.4.0 |
CGO_ENABLED=0, time/tzdata, golang.org/x/crypto/x509roots/fallbackを追加 |
8.45MB |
time/tzdataを追加すると480KBほど増加し、golang.org/x/crypto/x509roots/fallbackを追加すると240KBほど増加するみたいですね。
とはいえGoのバイナリはランタイム自体が大きいので、これらのパッケージ追加によって増える増分を考えると微々たるものとして考えてもいいかもしれません。
それでは皆さん、よい美しいコンテナイメージライフをお過ごしくださいませ。
今回使用したコード
付録
軽量で有名なalpineイメージと、Googleが提供している必要最小限のdistroless/staticに同梱してみたときのイメージサイズと同梱ファイル数を比較してみます。
distroless/staticにはca-certificatesとtzdataが同梱されています。
しかし、alpineイメージにはデフォルトでca-certificatesは入っていますが、tzdataに相当するものは入っていません。こちらはapkにて導入して、イメージを測定することにします。
| ベースイメージ | ビルド設定 | イメージサイズ | 同梱ファイル数 |
|---|---|---|---|
| gcr.io/distroless/static-debian13:latest |
CGO_ENABLED=0のみ |
14.1MB | 1378 |
| alpine:3.24.1 |
CGO_ENABLED=0のみ |
23MB | 1134 |
| alpine:3.24.1 | golang1.26.3-alpine3.23でビルド。CGO_ENABLED=1
|
23 MB | 1134 |
| (参考) scratch |
CGO_ENABLED=0, time/tzdata, golang.org/x/crypto/x509roots/fallbackを追加 |
8.45MB | 1 |
どれも一般的なコンテナイメージとしては小さめですが、やっぱりscratchでビルドしたものにはイメージサイズと同梱ファイル数どちらも最小となる結果になりました。
というか意外とdistroless/static-debian13ってファイル数が多いんですね。