# Echo 本番デプロイ完全ガイド：マルチステージ Docker・distroless・サーバータイムアウト・グレースフルシャットダウンで無停止運用する

> Go Echo（v5）を本番にデプロイする実装ガイド。12-factor の環境変数設定、CGO_ENABLED=0 のマルチステージ Docker と distroless/nonroot、StartConfig.BeforeServeFunc による http.Server タイムアウト（v5でTimeoutミドルウェア廃止）、SIGTERM を取りこぼさないグレースフルシャットダウン、liveness/readiness ヘルスチェック、ECS Fargate / Cloud Run の作法、シークレット管理、コスト最適化までを実コードで解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Go, Echo, アーキテクチャ設計, 可観測性, セキュリティ, コスト最適化
- URL: https://tomodahinata.com/blog/go-echo-deployment-docker-distroless-ecs-cloud-run-graceful-shutdown-guide
- カテゴリ: Go・Echo 本番運用
- 総合ガイド: https://tomodahinata.com/blog/go-echo-framework-production-guide

## 要点

- Goは単一バイナリ。CGO_ENABLED=0 のマルチステージビルド→distroless/static:nonroot で、シェルもパッケージも無い極小・最小権限イメージにする
- v5 は Timeout ミドルウェアを廃止。サーバータイムアウト（ReadHeaderTimeout/Read/Write/Idle）は StartConfig.BeforeServeFunc で http.Server に設定する。slowloris 対策の必須項目
- デプロイ無停止の肝は SIGTERM。StartConfig{GracefulTimeout} に signal.NotifyContext を渡し、ErrServerClosed は正常終了として扱う
- GracefulTimeout はオーケストレータの終了猶予より短く。逆だと片付け前に SIGKILL される。readiness を先に落として新規流入を止める
- 設定は12-factorで環境変数、シークレットはSecrets Manager等。イメージに焼かない。arm64(Graviton)ビルドでコストも削る

---

「ローカルでは動く」と「本番で無停止運用できる」の間には、深い谷があります。デプロイのたびに 502 が出る、巨大リクエストでメモリが溢れる、遅いクライアントが接続を握り続ける（slowloris）、コンテナイメージに認証情報が焼かれている——これらは**デプロイ設計の不備**であって、アプリのバグではありません。

この記事は、[Go Echo 本番運用ガイド](/blog/go-echo-framework-production-guide)のデプロイ編です。Go の「単一バイナリ」という強みを最大限に活かし、**極小・安全・無停止**なデプロイを、Echo v5 の正確な API で実装します。

> **この記事のルール**：Echo の API は **公式ドキュメント / ソース（v5・2026年6月時点）** に基づきます。特に v5 は `middleware.Timeout` が廃止され、サーバータイムアウトの設定方法が変わっています（[第3章](#3-サーバータイムアウトv5はstartconfigbeforeservefuncで設定する)）。**シークレット（DB URL・署名鍵）は環境変数／シークレットマネージャ前提**で、コードにもイメージにも絶対に焼き込みません。

---

## 1. 設定は 12-factor で：環境変数から読む

設定値（ポート・DB URL・ログレベル・鍵）は**環境変数**から読みます。これにより、同じイメージを dev/staging/prod で**ビルドし直さずに**使い回せます。

```go
type Config struct {
	Port        string
	DatabaseURL string
	JWTKey      []byte
	LogLevel    string
}

func LoadConfig() (*Config, error) {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080" // 既定値（Cloud Run は PORT を注入してくる）
	}
	dsn := os.Getenv("DATABASE_URL")
	if dsn == "" {
		return nil, errors.New("DATABASE_URL is required") // 起動時に fail-fast
	}
	return &Config{
		Port:        port,
		DatabaseURL: dsn,
		JWTKey:      []byte(os.Getenv("JWT_SIGNING_KEY")),
		LogLevel:    os.Getenv("LOG_LEVEL"),
	}, nil
}
```

必須設定が無ければ**起動時に即エラー（fail-fast）**にします。「動き始めてから設定不足で落ちる」より、「起動を拒否する」方が遥かに安全です。

---

## 2. マルチステージ Docker：極小・最小権限イメージ

Go の単一バイナリを活かし、**ビルド環境と実行環境を分離**します。実行イメージは `distroless`（シェル・パッケージマネージャなし）+ `nonroot`。

```dockerfile
# --- build stage ---
FROM golang:1.25 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download                       # 依存だけ先に取りレイヤキャッシュを効かせる
COPY . .
# CGO 無効で静的リンク、-trimpath とシンボル除去で軽量化・再現性向上
RUN CGO_ENABLED=0 GOOS=linux go build \
      -trimpath -ldflags="-s -w" \
      -o /app ./cmd/server

# --- runtime stage ---
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app /app
USER nonroot:nonroot                       # 非 root 実行（最小権限）
EXPOSE 8080
ENTRYPOINT ["/app"]
```

| 設定 | 効果 |
| --- | --- |
| `CGO_ENABLED=0` | 静的リンク。distroless/static で動く・libc 依存を断つ |
| `-trimpath` | バイナリからローカルパスを除去（情報漏洩防止・再現性） |
| `-ldflags="-s -w"` | デバッグ情報除去でサイズ削減 |
| `distroless/static` | シェルもパッケージも無い＝**攻撃面が最小** |
| `nonroot` | 侵害時の被害を限定（最小権限の原則） |
| 依存を先に `COPY` | レイヤキャッシュでビルド高速化（コスト効率） |

> **コスト最適化**：本番ターゲットが arm64（AWS Graviton / Cloud Run）なら、`GOARCH=arm64` でビルドして arm 系インスタンスに載せると、同等性能で**単価が安く**なります。Go のクロスコンパイルは容易なので、マルチアーキビルドの恩恵を受けやすい言語です。

---

## 3. サーバータイムアウト：v5 は `StartConfig.BeforeServeFunc` で設定する

**ここが v5 の重要な落とし穴です。** v4 にあった `middleware.Timeout` は**v5 で廃止**されました。代わりに、コネクションレベルのタイムアウトを **`http.Server` に設定**します。v5 では `StartConfig.BeforeServeFunc`（サーバー起動直前に `*http.Server` を触れるコールバック）がその正規の場所です。

```go
sc := echo.StartConfig{
	Address:         ":" + cfg.Port,
	GracefulTimeout: 10 * time.Second,
	BeforeServeFunc: func(s *http.Server) error {
		// slowloris 等の遅延攻撃・リソース占有を防ぐ
		s.ReadHeaderTimeout = 5 * time.Second   // ヘッダ読み取りの上限（最重要）
		s.ReadTimeout = 15 * time.Second        // ボディ含むリクエスト全体
		s.WriteTimeout = 30 * time.Second       // レスポンス書き込み
		s.IdleTimeout = 120 * time.Second       // keep-alive 接続のアイドル上限
		return nil
	},
}
```

| タイムアウト | 守るもの |
| --- | --- |
| `ReadHeaderTimeout` | **slowloris**（ヘッダをだらだら送って接続を占有する攻撃） |
| `ReadTimeout` | 巨大／低速なリクエストボディによる占有 |
| `WriteTimeout` | 応答を受け取らないクライアントによる占有 |
| `IdleTimeout` | アイドルな keep-alive 接続の滞留 |

**ハンドラ単位**のタイムアウト（重いクエリの打ち切り）は、`context.WithTimeout` で表現します（[DB 層の記事](/blog/go-echo-database-postgresql-pgx-sqlc-gorm-transaction-guide#1-最重要contextをハンドラからdbまで貫通させる)）。サーバーレベル（接続）とハンドラレベル（処理）は別物で、両方を設定します。リクエストボディは[`BodyLimit`](/blog/go-echo-middleware-cors-csrf-jwt-rate-limit-security-guide)でサイズ上限も掛けます。

---

## 4. グレースフルシャットダウン：SIGTERM を取りこぼさない

デプロイ・スケールインのたびに、オーケストレータは**まず `SIGTERM`** を送ります。これを無視すると処理中のリクエストが切れて 502 になります。v5 は `StartConfig{GracefulTimeout}` に `signal.NotifyContext` を渡す形に一本化されています。**`http.ErrServerClosed` は正常終了**として扱うのが正しい作法です。

```go
func main() {
	cfg, err := LoadConfig()
	if err != nil {
		slog.Error("config error", "error", err)
		os.Exit(1)
	}

	e := buildEcho(cfg) // ルート・ミドルウェア・DI 済みの Echo

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	sc := echo.StartConfig{
		Address:         ":" + cfg.Port,
		GracefulTimeout: 10 * time.Second,
		BeforeServeFunc: applyServerTimeouts, // 第3章
		OnShutdownError: func(err error) {
			e.Logger.Error("graceful shutdown error", "error", err)
		},
	}

	// ErrServerClosed は SIGTERM での正常停止。エラー扱いしない
	if err := sc.Start(ctx, e); err != nil && !errors.Is(err, http.ErrServerClosed) {
		e.Logger.Error("server failed", "error", err)
		os.Exit(1)
	}
	slog.Info("server stopped gracefully")
}
```

`SIGTERM` 受信後の流れ：**①新規接続の受付停止 → ②`GracefulTimeout`（既定10秒）の範囲で処理中リクエストの完了を待つ → ③DB プール等のクリーンアップ（`defer pool.Close()`）→ ④終了**。

> **致命的な設定整合**：`GracefulTimeout` は、**オーケストレータの終了猶予より必ず短く**します（ECS の `stopTimeout`、Kubernetes の `terminationGracePeriodSeconds` 等）。逆だと、片付けの途中で `SIGKILL` され、結局リクエストが切れます。例：猶予 30 秒なら `GracefulTimeout` は 10〜20 秒に。

---

## 5. ヘルスチェック：liveness と readiness を分ける

オーケストレータはヘルスチェックで「生きているか」「トラフィックを流してよいか」を判断します。**この2つを混同しない**のが無停止運用の鍵です。

```go
// liveness: プロセスが生きているか（依存はチェックしない。落ちていれば再起動される）
e.GET("/healthz", func(c *echo.Context) error {
	return c.NoContent(http.StatusOK)
})

// readiness: トラフィックを受けられるか（DB 等の依存も確認）
e.GET("/readyz", func(c *echo.Context) error {
	ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)
	defer cancel()
	if err := pool.Ping(ctx); err != nil {
		return c.JSON(http.StatusServiceUnavailable, map[string]string{"db": "down"})
	}
	return c.NoContent(http.StatusOK)
})
```

- **liveness（`/healthz`）**：依存を見ない。DB が一時的に落ちても**プロセスは再起動すべきでない**（再起動しても直らない）。
- **readiness（`/readyz`）**：依存を見る。DB が落ちている間は**トラフィックを流さない**（が、プロセスは殺さない）。
- **グレースフル停止との連携**：理想は、`SIGTERM` 受信時にまず readiness を `503` にしてロードバランサからの**新規流入を止め**、その後に既存リクエストを捌き切ること。

これらのヘルスチェックや[RequestLogger（slog）](/blog/go-echo-middleware-cors-csrf-jwt-rate-limit-security-guide)、相関 ID を組み合わせると、本番の可観測性が一段上がります（[OpenTelemetry 連携](/blog/opentelemetry-observability-production-tracing-metrics-logs)）。

---

## 6. ECS Fargate / Cloud Run の作法

Echo は常駐コンテナとして、どのコンテナ基盤にも素直に載ります。プラットフォーム別の要点だけ押さえます。

| 項目 | AWS ECS / Fargate | Google Cloud Run |
| --- | --- | --- |
| ポート | タスク定義のポートマッピング | **`PORT` 環境変数を必ず読む** |
| 終了シグナル | `SIGTERM` → `stopTimeout`（既定30秒） | `SIGTERM` → 猶予（既定10秒、延長可） |
| 猶予との整合 | `GracefulTimeout` < `stopTimeout` | `GracefulTimeout` < 終了猶予 |
| クライアント IP | ALB 経由→ `Echo.IPExtractor` を設定 | プロキシ経由→同上 |
| シークレット | Secrets Manager / SSM → 環境変数注入 | Secret Manager → 環境変数/ボリューム |
| オートスケール | Target Tracking 等 | リクエスト並行数ベース（ゼロスケール可） |

`c.RealIP()` を[レート制限](/blog/go-echo-middleware-cors-csrf-jwt-rate-limit-security-guide#9-ratelimiter重い処理の前で過剰アクセスを遮断)や監査ログで使うなら、**ロードバランサ背後では `Echo.IPExtractor` で信頼できる前段を設定**しないと、ヘッダ偽装で誤った IP を掴みます。

ECS/Fargate のオートスケールや Blue/Green、コスト最適化は[ECS Fargate 本番ガイド](/blog/aws-ecs-fargate-production-guide)、Cloud Run のゼロスケール・並行性・課金は[Azure Container Apps ガイド](/blog/azure-container-apps-production-guide)のサーバーレスコンテナの考え方とも地続きです。

---

## 7. シークレット管理とイメージ衛生

- **イメージに焼かない**：`ENV JWT_KEY=...` や `.env` のコミットは厳禁。鍵はランタイムで**シークレットマネージャ → 環境変数**注入。
- **`.dockerignore`**：`.git`・`.env`・テスト・ローカル設定をイメージから除外（漏洩・肥大化防止）。
- **イメージスキャン**：CI でコンテナ脆弱性スキャン（Trivy 等）をゲートに。`golangci-lint` と `govulncheck`（Go の既知脆弱性検査）も合わせる。
- **鍵レス CI/CD**：デプロイ認証は長期キーではなく **OIDC（GitHub Actions → AWS/GCP の一時credential）**で。長期アクセスキーをリポジトリ Secrets に置かない。

このイメージ衛生・鍵レス CI/CD の思想は、[AI駆動開発の品質ゲート](/blog/ai-driven-development-quality-gates-ci-type-safety-test-security)で扱う「秘密情報・脆弱性スキャンを CI で機械強制する」設計と同じです。

---

## まとめ：無停止デプロイの7原則

1. **設定は 12-factor で環境変数**。必須欠如は起動時 fail-fast。
2. **CGO_ENABLED=0 のマルチステージ + distroless/nonroot** で極小・最小権限イメージに。
3. **v5 はサーバータイムアウトを `BeforeServeFunc`** で設定（`ReadHeaderTimeout` で slowloris 対策）。
4. **`StartConfig{GracefulTimeout}` + `signal.NotifyContext`**、`ErrServerClosed` は正常終了。
5. **`GracefulTimeout` はオーケストレータ猶予より短く**。readiness を先に落として流入を止める。
6. **liveness と readiness を分ける**。依存は readiness だけで見る。
7. **シークレットはイメージに焼かず**、OIDC の鍵レス CI/CD とイメージスキャンをゲートに。

デプロイ設計は、コードの正しさを**本番の現実（再起動・スケール・攻撃・障害）**に耐えさせる最後の工程です。Echo の単一バイナリ + v5 の `StartConfig` は、この工程を素直に・正確に組めるように設計されています。クラスタ全体は[Go Echo 本番運用ガイド](/blog/go-echo-framework-production-guide)から辿れます。
