Docker を使った Go のデプロイ

Goの静的コンパイル機能により、コンテナ化されたデプロイに最適な言語となっています。1つのバイナリファイルとスクラッチイメージを組み合わせるだけで、12MBの本番用イメージが完成します。

Go APIを本番環境にデプロイする際、適切に設計されたDockerfileを使用すれば、ベースイメージのサイズを1.2GBから12MBまで最適化することができます。

1. 学習内容


2. バックエンドエンジニアの実話

(1) 課題:1.2 GBのDockerイメージをデプロイするのに、毎回5分かかる

ボブのeコマースAPIは、まもなく公開される予定です:

「最も手軽な方法を採用しました。ベースイメージとしてFROM golang:1.22を使用し、そこにソースコードをCOPYして、コンテナ内でコンパイルしました。イメージのサイズは1.2 GBで、デプロイのたびにプルするのに5分かかります。CI/CDパイプラインは、コミットからデプロイまで15分かかります。上司は『デプロイが遅すぎる。ロールバックするのに10分もかかる』と言いました。」

DOCKERFILE
# 悪いやり方:コンテナ内でコンパイル、全ビルドツールを保持
FROM golang:1.22          # 800MB + コンパイルツール
WORKDIR /app
COPY . .
RUN go build -o server .
EXPOSE 8080
CMD ["./server"]          # イメージ 1.2GB!コンパイラ、依存関係、ツールチェーンを含む

(2) Goの解決策:多段階ビルド

DOCKERFILE
# 良いやり方:マルチステージビルド
# 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

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"]

▶ サンプル:アルパイン版

DOCKERFILE
# ===== 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
# 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ランタイムが必要;イメージサイズが増加
🔥 よくある間違い: Go コードで 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

YAML
# 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の最小限のデプロイ

YAML
# 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
100%
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のコンテナ化プロセス全体

GO
// 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
# 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 ファイル

TEXT
# .dockerignore
.git
.gitignore
*.md
bin/
tmp/
Dockerfile
.dockerignore
💡 ヒント: .dockerignore はビルド速度に大きな影響を与えます。不要なファイルを除外することで、Docker デーモンに送信されるコンテキストのサイズを縮小します。.git ディレクトリ(数百MBに達することもあります)を追加することで、ビルドを大幅に高速化できます。


❓ よくある質問

Q なぜマルチステージビルドではイメージのサイズが小さくなるのですか?
A 第1ステージ(ビルダー)では、バイナリをコンパイルするために完全なGoイメージを使用しますが、第2ステージ(実行)では、コンパイル済みのバイナリのみを最小限のベースイメージにコピーします。ビルドツール(コンパイラ、依存関係マネージャー、ソースコード)は最終的なイメージには含まれず、バイナリ自体のみが含まれます。
Q scratchイメージは安全ですか?
A scratchは最も安全です。完全に空の状態であり、シェル、ライブラリ、ツールなどは一切含まれていません。攻撃者はコンテナ内でコマンドを実行することはできません。ただし、プログラムでSSL証明書、タイムゾーンデータ、またはシェルが必要な場合は、ビルダーステージからこれらのファイルをコピーするか、alpine/distrolessに切り替える必要があります。
Q CGO_ENABLED=0 とはどういう意味ですか?
A デフォルトでは、Go は動的リンク(CGO_ENABLED=1、C ランタイムを使用)を使用します。CGO_ENABLED=0 を設定すると、Go は外部ライブラリに依存しない完全な静的バイナリを生成します。これにより、そのバイナリは scratch を含むあらゆる Linux システム上で実行可能になります。
Q クロスコンパイルはどのように行えばよいですか?
A 環境変数 GOOS(ターゲットOS)と GOARCH(ターゲットアーキテクチャ)を設定します。GOOS=linux GOARCH=amd64 go ビルド では、macOS上でLinux用バイナリをコンパイルします。Goはほぼすべてのプラットフォームの組み合わせに対応しています。クロスコンパイルは、CGO_ENABLED=0 の場合が最も簡単です。
Q ヘルスチェックとレディネスプローブの違いは何ですか?
A ヘルスチェックは、プロセスが正常に動作しているかどうかを確認するものです。正常でない場合、K8sはPodを再起動します。レディネスプローブは、サービスがトラフィックを受け入れる準備ができているかどうかを確認するものです。準備ができていない場合、K8sはそのPodをサービスから削除します。起動フェーズではレディネスプローブを、実行フェーズではライブネスプローブを使用します。
Q DockerとK8sの関係はどのようなものですか?K8sを学ぶ必要はありますか?
A Dockerはコンテナランタイムであり、アプリケーションを標準化された単位にパッケージ化します。K8sはコンテナオーケストレーションプラットフォームであり、複数のコンテナのデプロイ、スケーリング、ヘルスチェックを管理します。小規模なプロジェクトではDocker(またはDocker Compose)だけで十分ですが、大規模なプロジェクトではK8sが必要となります。このコースでは、K8sのオーケストレーション事例のみを取り上げ、K8sの概念については深く掘り下げません。
Q GoのDockerイメージは通常どのくらいのサイズですか?
A Goの静的バイナリ+スクラッチ ≈ 10~20 MB。Pythonアプリケーション(python:3.12ベース) ≈ 300 MB。Node.jsアプリケーション(node:22ベース) ≈ 400 MB。Javaアプリケーション(amazoncorretto:21ベース)は約500 MBです。Goのイメージはサイズ面で大きな優位性があります。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):単純な Go HTTP サービス用の Dockerfile(マルチステージビルド)を作成してください。イメージをビルドし、docker run -p 8080:8080 を使用してアクセスできることを確認してください。docker images を使用して、イメージのサイズを確認してください。

  2. 上級問題(難易度 ⭐⭐)データベースを伴う Go アプリケーションのフルスタックコンテナ化を実装してください。要件:(1) 3つのサービス:Go API、MySQL、Redis;(2) docker-compose.ymlによるオーケストレーション;(3) APIを起動する前に、データベースとRedisが利用可能であることを確認するためのヘルスチェックを実装すること;(4) Goコード内でos.Getenvを使用してデータベースの接続情報を読み込むこと;(5) .dockerignoreを使用して不要なファイルを除外すること。

  3. 課題(難易度:⭐⭐⭐)完全な CI/CD パイプライン(概念的な実装。実際の CI は不要)を実装してください。要件: (1) Makefileは、make ビルド(linux/amd64 + linux/arm64のクロスコンパイル)、make docker-ビルド、およびmake docker-プッシュに対応していること; (2) マルチステージの Dockerfile によるビルド;(3) 各ステージにおけるビルドキャッシュの最適化;(4) K8s deployment.yamlservice.yaml;(5) ライブネスプローブおよびレディネスプローブを含むこと;(6) リソース制限 (requests および limits)。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%