本文へスキップ
記事一覧

Docker で Go アプリケーションをビルドする:ポータブルで密閉されたツールチェーン

Ghazi Triki 著 · 7 分で読めます

Docker で Go アプリケーションをビルドする
この記事の内容

ホストに Go をインストールするのはもうやめましょう。GOPATH の汚染も、「自分の環境では動く」問題も、バージョンのずれもありません。BigBlueButton の Learning Analytics レポートの Java エージェントの書き換えと、セキュアな Piler Go Agent の構築に実際に使ったパターンです。Docker だけで静的バイナリを出荷する方法をご紹介します。

Ghazi Triki(RIADVICE)

「Go をインストールするだけ」の問題点

BigBlueButton の Learning Analytics レポート用に Java ベースのエージェントを管理していた頃、私たちは典型的な「依存関係地獄」に直面していました。Go への移行はシンプルさを約束してくれましたが、それは同じ過ちを繰り返さなければの話です。私たちには、生成するバイナリと同じくらい密閉されたビルドプロセスが必要でした。

どのチームも、いずれ次のような壁に突き当たります。

  • バージョンのずれ。 Ahmed は 1.24、Omar は 1.26、本番サーバーは 1.25 を使っています。コンパイラのちょっとした変更でビルドが壊れますが、壊れるのは Ahmed の環境だけです。
  • ホストの汚染。 GOPATH、GOMODCACHE、グローバルにインストールしたツール(staticcheck、gofumpt)が、ワークステーションごとに異なります。
  • オンボーディングの手間。 新しく加わったエンジニアが、初日を環境変数や cgo 用の C ツールチェーンとの格闘に費やしてしまいます。

RIADVICE では、規律と秩序を大切にしています。私たちが求めたのは、それを反映した、クリーンで予測可能、かつ共有できるビルドプロセスでした。

定番の答えは「Docker を使う」ですが、ほとんどのチュートリアルはコンテナイメージのビルド方法しか説明していません。Go コンパイラをローカルに一切インストールせずに、ホスト上にプレーンな静的リンクのバイナリを得る方法までは示していないのです。systemd サービスやエアギャップ環境へのデプロイで実際に必要になるのは、まさにその成果物です。

Shell
docker run --rm -v "$(pwd):/app" -w /app golang:1.26.5-trixie \
    go build -o dist/myapp ./cmd/myapp

公式の golang イメージがコンパイラです。ソースは /app にバインドマウントされます。バイナリはホスト上の dist/myapp に書き出されます。コマンドが終了するとコンテナは破棄され(--rm)、ホストは実行前とまったく同じクリーンな状態のまま残ります。

実際の経験:BigBlueButton から Piler まで

これは単なる理論ではありません。私たちはこのパターンを二つの重要なプロジェクトに適用しました。

  1. BigBlueButton Learning Analytics Reports:重い Java エージェントを軽量な Go バイナリに書き換えました。Docker ベースのビルドにより、BBB サーバーの Debian 環境を正確にターゲットにでき、その環境を手元のノートパソコンに用意する必要がありませんでした。
  2. Piler Go Agent:メールアーカイブソリューション向けに、セキュリティを強化したエージェントを構築しました。ビルドプロセスもコードと同じくらいセキュアである必要がありました。コンパイラを特定の SHA に固定することで、バイナリのすべてのビットの出所を明確にしました。

Go をインストールするより優れている理由

観点ホストでの go installdocker run golang:...
Go のバージョン最後にインストールしたもの次第固定され、全員で同一
GOPATH/キャッシュ~/go を汚染する密閉されたコンテナレイヤー
cgo ツールチェーンホスト側のセットアップが必要公式イメージに同梱
CI との一致「自分の環境では動く」同一のイメージ、同一のバイナリ
Makefile
BINARY := mini-api
DIST := dist

# Version metadata derived from git
VERSION    := $(shell tmp=$$(git describe --tags --always --dirty 2>/dev/null) && echo "$${tmp#v}" || echo dev)
COMMIT     := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
BRANCH     := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X example.com/mini-api/internal/version.Version=$(VERSION) \
           -X example.com/mini-api/internal/version.Commit=$(COMMIT) \
           -X example.com/mini-api/internal/version.Branch=$(BRANCH) \
           -X example.com/mini-api/internal/version.BuildDate=$(BUILD_DATE)

# Pinned toolchain image
GO_IMAGE := golang:1.26.5-trixie

# Run any command inside the Go toolchain container
define run_in_docker
    docker run --rm -v "$(PWD):/app" -w /app $(GO_IMAGE)
endef

.PHONY: build test lint

build:
    @mkdir -p $(DIST)
    $(call run_in_docker) bash -c 'CGO_ENABLED=0 go build -buildvcs=false -ldflags="$(LDFLAGS)" -o $(DIST)/$(BINARY) ./cmd/$(BINARY)'
    @echo "Built $(DIST)/$(BINARY) (version $(VERSION))"

test:
    $(call run_in_docker) go test -race ./...

lint:
    $(call run_in_docker) go vet ./...

Makefile:規律の源

ビルド手順をコードとして明文化するために、Makefile を使っています。GO_IMAGE が特定のバージョンに固定されている点に注目してください。latest は決して使いません。

重要な技術的ポイント

1. 静的バイナリのための CGO_ENABLED=0

Piler エージェントでは、最新の Debian からレガシーなサーバーまで、どこでも動くバイナリが必要でした。CGO を無効にすることで、バイナリに必要なものがすべて含まれ、ホストのライブラリへの動的な依存がなくなります。

2. 来歴を示すための -ldflags -X

バイナリは、自らがどこから来たのかを正確に示せるべきです。私たちはリンク時に git のコミットとビルド日時を埋め込んでいます。これは、数十にのぼるさまざまなクラスターで BigBlueButton エージェントをデバッグするうえで不可欠でした。

3. コンパイライメージの固定

私たちの文化では、一貫性を重視しています。latest を使わずに golang:1.26.5-trixie に固定しておけば、Ahmed が今日ビルドしたバイナリと、Omar が 6 か月後にビルドしたバイナリは、ビット単位でまったく同一になります。

まとめ

コンパイラは道具であり、ほかの道具と同じく、しかるべき場所に置いておくべきものです。Go ツールチェーンを Docker の中に移したことで、「自分の環境では動く」という種類のバグを丸ごと排除し、RIADVICE に加わる新しいエンジニア全員のオンボーディングを簡素化できました。

Java エージェントを書き換える場合でも、Piler 向けにセキュアな API を構築する場合でも、このパターンは本番システムに求められる規律と信頼性をもたらします。


Ghazi Triki は RIADVICE の創業者です。私たちは BigBlueButton、Piler などに向けて、高性能でセキュアなインテグレーションを構築しています。ぜひ一緒に素晴らしいものを作りましょう。

🔗 GitHub で golang-docker-boilerplate を入手する

この記事を共有