Docker を使った Go のデプロイ
Goの静的コンパイル機能により、コンテナ化されたデプロイに最適な言語となっています。1つのバイナリファイルとスクラッチイメージを組み合わせるだけで、12MBの本番用イメージが完成します。
Go APIを本番環境にデプロイする際、適切に設計されたDockerfileを使用すれば、ベースイメージのサイズを1.2GBから12MBまで最適化することができます。
1. 学習内容
- 多段階ビルド
- Scratch、Alpine、またはDistrolessのイメージを選択する
CGO_ENABLED=0静的コンパイル- クロスプラットフォームのクロスコンパイル
- Dockerのヘルスチェック
- Docker Compose によるマルチサービスのオーケストレーション
- K8s デプロイメントの YAML 例
2. バックエンドエンジニアの実話
(1) 課題:1.2 GBのDockerイメージをデプロイするのに、毎回5分かかる
ボブのeコマースAPIは、まもなく公開される予定です:
「最も手軽な方法を採用しました。ベースイメージとして
FROM golang:1.22を使用し、そこにソースコードをCOPYして、コンテナ内でコンパイルしました。イメージのサイズは1.2 GBで、デプロイのたびにプルするのに5分かかります。CI/CDパイプラインは、コミットからデプロイまで15分かかります。上司は『デプロイが遅すぎる。ロールバックするのに10分もかかる』と言いました。」
# 悪いやり方:コンテナ内でコンパイル、全ビルドツールを保持
FROM golang:1.22 # 800MB + コンパイルツール
WORKDIR /app
COPY . .
RUN go build -o server .
EXPOSE 8080
CMD ["./server"] # イメージ 1.2GB!コンパイラ、依存関係、ツールチェーンを含む
(2) Goの解決策:多段階ビルド
# 良いやり方:マルチステージビルド
# Stage 1: コンパイル(完全な Go イメージを使用)
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go ビルド -o サーバー .
# Stage 2: 実行(最小イメージを使用)
FROM scratch
COPY --from=builder /app/サーバー /サーバー
EXPOSE 8080
CMD ["/サーバー"] # ミラー 12MB!バイナリファイルのみ
(3) 結果:最適化前と最適化後
| メートル法 | 単段式 (Go: 1.22) | 多段式 (Scratch) | 改善度 |
|---|---|---|---|
| 画像サイズ | 1.2 GB | 12 MB | 100倍 |
| デプロイメント・プル | 5分 | 5秒 | 60回 |
| セキュリティリスク | コンパイラやツールチェーンを含む | バイナリのみ | 攻撃対象領域が最小限 |
| キャッシュの構築 | ❌ 毎回完全なコンパイル | ✅ 階層型依存関係キャッシュ | — |
3. Dockerfile のベストプラクティス
▶ サンプル:GoのマルチステージDockerfile
# ===== Stage 1: Build =====
FROM golang:1.22-alpine AS builder
# 作業ディレクトリの設定
WORKDIR /app
# まず依存ファイルをコピー(Docker キャッシュを活用)
COPY go.mod go.sum ./
RUN go mod download
# ソースコードをコピーする
COPY . .
# 静的コンパイル
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .
# ===== Stage 2: Run =====
FROM scratch
# builder ステージからバイナリをコピー
COPY --from=builder /app/server /server
# タイムゾーンファイル(必要な場合)
# COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# SSL 証明書(必要な場合)
# COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/server", "-health"]
CMD ["/server"]
▶ サンプル:アルパイン版
# ===== Stage 1: Build =====
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .
# ===== Stage 2: Run (Alpine) =====
FROM alpine:3.19
# ランタイム依存関係のインストール(必要な場合)
# RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
(1) ベースイメージの選択
| 画像 | サイズ | セキュリティ | 利用例 |
|---|---|---|---|
scratch |
0 MB | ✅ 攻撃対象領域が最小限 | 外部依存関係のない純粋なGoで静的コンパイル済み |
alpine |
5 MB | ⚠️ musl libc | シェル、curl、証明書などが必要です |
distroless |
20 MB | ✅ 最小構成 + ツール | SSL証明書、タイムゾーンデータが必要 |
golang:alpine |
350 MB | ❌ 開発用 | ビルドフェーズ専用 |
golang:1.22 |
800 MB | ❌ | 本番環境では絶対に使用しないでください |
-ldflags="-s -w" を使用するとバイナリのサイズを縮小できます。-s はシンボルテーブルを削除し、-w は DWARF デバッグ情報を削除します。これにより、動作に影響を与えることなく、バイナリのサイズをさらに 30~40% 縮小できます。
4. クロスコンパイル
▶ サンプル:クロスコンパイルスクリプト
# Makefile
APP=server
.PHONY: build-all
# 現在のプラットフォーム向けビルド
build:
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP) .
# 複数プラットフォーム向けクロスコンパイル
build-all:
# Linux amd64(最もよく使われる)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/$(APP)-linux-amd64 .
# Linux arm64(AWS Graviton / Apple M1)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/$(APP)-linux-arm64 .
# macOS
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o bin/$(APP)-darwin-amd64 .
# Windows
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o bin/$(APP)-windows-amd64.exe
# Docker ビルド
docker-build:
docker build -t myapp:latest .
(2) CGO_ENABLEDの影響
| CGO_ENABLED | メリット | デメリット |
|---|---|---|
=0 |
静的コンパイル済み、あらゆるプラットフォームで動作、最小限のイメージ | Cライブラリ(SQLite用のCドライバなど)は使用不可 |
=1 (デフォルト) |
Cライブラリが利用可能 | Cランタイムが必要;イメージサイズが増加 |
mattn/go-sqlite3(C ドライバー)を使用している場合、CGO_ENABLED=0 を設定するとコンパイルに失敗します。解決策:純粋なGo製SQLiteドライバ(modernc.org/sqliteなど)を使用するか、alpineイメージでCGO_ENABLED=1を設定してください(gccとmusl-devのインストールが必要です)。
5. 複数のサービス向けの Docker Compose
▶ サンプル:EコマースAPI + データベース + Redis
# docker-compose.yml
version: '3.8'
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- DB_HOST=db
- DB_PORT=3306
- DB_USER=app
- DB_PASSWORD=secret
- DB_NAME=shop
- REDIS_ADDR=redis:6379
- GIN_MODE=release
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
healthcheck:
test: ["CMD", "/server", "-health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: shop
MYSQL_USER: app
MYSQL_PASSWORD: secret
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
volumes:
db_data:
6. K8sのデプロイ
▶ サンプル:K8sの最小限のデプロイ
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-api
labels:
app: go-api
spec:
replicas: 3
selector:
matchLabels:
app: go-api
template:
metadata:
labels:
app: go-api
spec:
containers:
- name: api
image: myregistry/go-api:latest
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: "mysql-service"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: go-api-service
spec:
selector:
app: go-api
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
flowchart TD
subgraph Build [構築段階]
SRC[ソースコード] --> DEP[go mod download]
DEP --> BUILD[go build]
BUILD --> BIN[バイナリファイル 15MB]
end
subgraph Container [コンテナフェーズ]
BIN --> CP[COPY ~ scratch]
CP --> IMG[ミラー 12MB]
end
subgraph Deploy [導入段階]
IMG --> PUSH[プッシュ先 Registry]
PUSH --> K8S[K8s Deployment]
K8S --> POD[Pod 3 コピー]
end
Build --> Container --> Deploy
7. 完全な例:EコマースAPIのコンテナ化プロセス全体
// cmd/server/main.go(ヘルスチェック機能付き完全 API)
package main
import (
"context"
"encoding/json"
"flag"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
healthFlag := flag.Bool("health", false, "run health check")
flag.Parse()
if *healthFlag {
// Health check モード:サービスが利用可能か確認
resp, err := http.Get("http://localhost:8080/health")
if err != nil {
os.Exit(1)
}
resp.Body.Close()
os.Exit(0)
}
mux := http.NewServeMux()
// ヘルスチェックエンドポイント
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
mux.HandleFunc("GET /ready", func(w http.ResponseWriter, r *http.Request) {
// データベースなどの依存関係が準備完了か確認
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"ready": "true"})
})
// サービスエンドポイント
mux.HandleFunc("GET /api/products", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"products": []string{"laptop", "mouse", "keyboard"},
})
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// グレースフルシャットダウン
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("閉じようとしています...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
log.Println("サービスの開始は :8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
(7) 付属の Dockerfile
# Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
# 依存関係をキャッシュ
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 静的コンパイル + デバッグ情報を削除
RUN CGO_ENABLED=0 GOOS=linux go ビルド -ldflags="-s -w" -o /app/サーバー ./cmd/サーバー
# === 実行ステージ ===
FROM scratch
# バイナリをコピー
COPY --from=builder /app/サーバー /サーバー
# SSL 証明書(HTTPS 外部 API アクセス用)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/サーバー", "-health"]
CMD ["/サーバー"]
(8) .dockerignore ファイル
# .dockerignore
.git
.gitignore
*.md
bin/
tmp/
Dockerfile
.dockerignore
.dockerignore はビルド速度に大きな影響を与えます。不要なファイルを除外することで、Docker デーモンに送信されるコンテキストのサイズを縮小します。.git ディレクトリ(数百MBに達することもあります)を追加することで、ビルドを大幅に高速化できます。
❓ よくある質問
alpine/distrolessに切り替える必要があります。CGO_ENABLED=0 を設定すると、Go は外部ライブラリに依存しない完全な静的バイナリを生成します。これにより、そのバイナリは scratch を含むあらゆる Linux システム上で実行可能になります。GOOS=linux GOARCH=amd64 go ビルド では、macOS上でLinux用バイナリをコンパイルします。Goはほぼすべてのプラットフォームの組み合わせに対応しています。クロスコンパイルは、CGO_ENABLED=0 の場合が最も簡単です。node:22ベース) ≈ 400 MB。Javaアプリケーション(amazoncorretto:21ベース)は約500 MBです。Goのイメージはサイズ面で大きな優位性があります。📖 まとめ
- マルチステージビルド:「ビルダー」ステージでのコンパイル、「ラン」ステージでの最小イメージの生成
- ベースイメージ:scratch (minimal) > distroless > alpine > golang:alpine
CGO_ENABLED=0: 完全に静的コンパイルされ、どのプラットフォームでも動作する-ldflags="-s -w": シンボルテーブルとデバッグ情報を削除する- クロスコンパイル:
GOOS=linux GOARCH=arm64 go ビルド HEALTHCHECK: Dockerのネイティブヘルスチェックdocker-compose: マルチサービス・オーケストレーション(API + DB + Redis)- K8sのデプロイ:最小限のYAML例(ライヴネス・プローブとレディネス・プローブ)
.dockerignore: 不要なファイルを除外してビルドを高速化する
📝 練習問題
-
基本演習(難易度 ⭐):単純な Go HTTP サービス用の Dockerfile(マルチステージビルド)を作成してください。イメージをビルドし、
docker run -p 8080:8080を使用してアクセスできることを確認してください。docker imagesを使用して、イメージのサイズを確認してください。 -
上級問題(難易度 ⭐⭐):データベースを伴う Go アプリケーションのフルスタックコンテナ化を実装してください。要件:(1) 3つのサービス:Go API、MySQL、Redis;(2) docker-compose.ymlによるオーケストレーション;(3) APIを起動する前に、データベースとRedisが利用可能であることを確認するためのヘルスチェックを実装すること;(4) Goコード内で
os.Getenvを使用してデータベースの接続情報を読み込むこと;(5).dockerignoreを使用して不要なファイルを除外すること。 -
課題(難易度:⭐⭐⭐):完全な CI/CD パイプライン(概念的な実装。実際の CI は不要)を実装してください。要件: (1) Makefileは、
make ビルド(linux/amd64 + linux/arm64のクロスコンパイル)、make docker-ビルド、およびmake docker-プッシュに対応していること; (2) マルチステージの Dockerfile によるビルド;(3) 各ステージにおけるビルドキャッシュの最適化;(4) K8sdeployment.yaml+service.yaml;(5) ライブネスプローブおよびレディネスプローブを含むこと;(6) リソース制限 (requestsおよびlimits)。



