2026年8月29日
2026年8月29日
DockerでGoアプリをビルド・デプロイする方法|マルチステージビルドで小さなイメージを作る
Goはビルドすると1つの実行ファイルになるので、Dockerとの相性がよい言語です。ビルドに必要なGoのツールチェーンと、実行に必要なファイルを分ける「マルチステージビルド」を使うと、数十MB以下の小さなイメージを作れます。
この記事では、簡単なWebアプリを例に、Dockerfileの書き方からサーバーへのデプロイまでを説明します。バージョンは2026年10月時点のもの(Go 1.27、Docker Engine 29)で、手順は実際にビルドと起動を確かめています。
例にするアプリ
8080番で待ち受け、/ と /healthz に応答するだけのWebアプリです。ディレクトリの構成は次のとおりです。
myapp/
├── go.mod
├── main.go
├── Dockerfile
├── .dockerignore
└── compose.yaml
main.go の内容です。標準ライブラリだけを使っています。
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Go in Docker")
})
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
})
log.Printf("listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, mux))
}
go.mod が無ければ、go mod init example.com/myapp で作ります。外部のモジュールを使う場合は go mod tidy で go.sum も作っておきます。
Dockerfileを書く
# syntax=docker/dockerfile:1
# ---- ビルド用のステージ ----
FROM golang:1.27 AS build
WORKDIR /src
# 依存モジュールだけ先にダウンロードして、レイヤーのキャッシュを効かせる
COPY go.mod go.sum* ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app .
# ---- 実行用のステージ ----
FROM gcr.io/distroless/static-debian13:nonroot
COPY --from=build /out/app /app
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app"]
ポイントは次のとおりです。
- ビルドは公式の
golangイメージで行う。 OSのパッケージ(apt-get install golang)で入るGoは古いことが多いので、Docker Hubの公式イメージでバージョンを指定する go.modとgo.sumを先にコピーする。 ソースコードだけを変えたときはgo mod downloadの層がキャッシュから使われ、ビルドが速くなる。go.sum*の*は、go.sumが無いプロジェクトでもエラーにしないための書き方RUN --mount=type=cacheは、モジュールとビルド結果のキャッシュをビルドの間で使い回すBuildKitの機能。Docker Engine 23以降のdocker buildは、既定でBuildKitを使うCGO_ENABLED=0で、Cのライブラリに依存しない静的なバイナリを作る。これで、libcの無い最小のイメージでも動く-trimpath -ldflags="-s -w"は、ビルドした環境のパスとデバッグ情報を除き、ファイルを小さくする- 実行用には distroless の
staticイメージを使う。 シェルやパッケージマネージャーが入っていないので、攻撃に使える道具が少ない。:nonrootタグはroot以外のユーザー(UID 65532)で動く ENTRYPOINTは配列の形で書く。 シェルを介さずに起動するので、docker stopのシグナルがアプリに直接届く
CGOが必要なモジュール(SQLiteのドライバーの一部など)を使う場合は CGO_ENABLED=0 にできません。その場合は、実行用のイメージを gcr.io/distroless/base-debian13 など、libcが入ったものにします。
.dockerignore を置く
ビルドに関係ないファイルをDockerに送らないように、.dockerignore を置きます。
.git
*.md
compose.yaml
Dockerfile
.env
.env などの秘密情報を含むファイルをイメージに入れないためにも必要です。
ビルドして手元で動かす
docker build -t my-go-app:1.0.0 .
docker run -d --name my-go-app -p 8080:8080 my-go-app:1.0.0
curl http://localhost:8080/
curl http://localhost:8080/healthz
Hello from Go in Docker と ok が返れば動いています。ログは docker logs my-go-app、停止と削除は docker rm -f my-go-app で行います。
できたイメージの大きさは、次のコマンドで確認できます。この例では15MB程度でした。
docker images my-go-app
distrolessのイメージにはシェルが無いので、docker exec -it my-go-app sh は使えません。中を調べたいときは、ビルド用のステージだけを docker build --target build で作って調べるか、デバッグ用の :debug-nonroot タグ(BusyBoxのシェル入り)に一時的に変えます。
Docker Composeで起動する
サーバーでは、起動の設定を compose.yaml に書いておくと、更新や再起動を同じコマンドで行えます。
services:
app:
image: registry.example.com/myteam/my-go-app:1.0.0
build: .
ports:
- "127.0.0.1:8080:8080"
environment:
PORT: "8080"
restart: unless-stopped
image:とbuild:を両方書くと、docker compose buildでビルドしたイメージにこの名前が付くports:を127.0.0.1:で始めると、サーバーの外からは直接つながらず、同じサーバーのnginxなどのリバースプロキシからだけ接続できる。HTTPSはリバースプロキシで終端するrestart: unless-stoppedで、サーバーの再起動後やアプリの異常終了時に自動で起動する- ファイルの先頭に
version:を書く必要はない。今のDocker Composeでは使われず、書くと警告が出る
docker compose up -d --build
docker compose ps
docker compose logs -f app
サーバーにデプロイする
コンテナレジストリを使う方法
Docker HubやGitHub Container Registry(ghcr.io)などのレジストリにイメージを置き、サーバーで取得します。
# 開発機でビルドして送る
docker login registry.example.com
docker build -t registry.example.com/myteam/my-go-app:1.0.0 .
docker push registry.example.com/myteam/my-go-app:1.0.0
# サーバーで取得して起動し直す(compose.yaml がある場所で実行)
docker compose pull
docker compose up -d
タグは latest ではなく、バージョンやGitのコミットのハッシュを付けると、どのイメージが動いているかがわかり、前のバージョンに戻すのも簡単です。
レジストリを使わない方法
小さな構成なら、イメージをファイルにしてSSHで送ることもできます。
docker save my-go-app:1.0.0 | gzip | ssh user@server.example.com 'gunzip | docker load'
サーバーでは、compose.yaml の image: を my-go-app:1.0.0 にして docker compose up -d を実行します。
開発機とサーバーのCPUが違う場合
Apple シリコンのMac(arm64)でビルドしたイメージは、x86_64(amd64)のVPSでは動きません(exec format error になる)。サーバーに合わせて、プラットフォームを指定してビルドします。
docker buildx build --platform linux/amd64 -t registry.example.com/myteam/my-go-app:1.0.0 --push .
よくあるエラー
| 症状 | 原因と対処 |
|---|---|
exec /app: no such file or directory |
CGOを有効にしたままビルドし、libcの無いイメージで動かしている。CGO_ENABLED=0 を付けるか、libcのある実行用イメージにする |
exec format error |
ビルドしたCPUの種類とサーバーが違う。--platform を指定してビルドする |
| ブラウザーから接続できない | アプリが 127.0.0.1 で待ち受けている。コンテナの中では :8080(すべてのアドレス)で待ち受ける |
go mod download で失敗する |
プライベートなモジュールに認証が必要。BuildKitの --mount=type=secret で認証情報を渡す(イメージに残さない) |
| 1024番未満のポートで起動できない | nonroot ユーザーは1024番未満で待ち受けられない。アプリは8080番などで待ち受け、-p 80:8080 で公開する |
まとめ
- マルチステージビルドで、ビルドは
golangイメージ、実行は distroless のstaticイメージに分ける CGO_ENABLED=0で静的なバイナリを作り、root以外のユーザーで動かすgo.modとgo.sumを先にコピーし、キャッシュのマウントでビルドを速くする- サーバーでは
compose.yamlで起動し、レジストリ経由かdocker saveとdocker loadでイメージを届ける - 開発機とサーバーのCPUが違う場合は
--platformを指定する
関連記事: