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 を指定する

関連記事:

お気軽にご相談ください

お見積りへ お問い合わせへ
▲