Docker 実戦 #6 クラウドデプロイ — Fly.io / Railway / ECS — トラックの締め

Docker トラックの最後の記事です。#1〜3 でコンテナイメージを作り、#4〜5 で CI でビルドして push しました。今回はそのイメージを 実際の運用インフラ に上げる段階です。

Docker 実戦 での今回の位置:

三つの選択肢を比較した後、Fly.io と Railway のフローを短く触れ、ECS は AWS 実戦トラック につなげます。最後に Docker 24 編の振り返り。

三つの分かれ道 #

まず一表で。

Fly.ioRailwayAWS ECS Fargate
ロケーションedge (世界 30+ リージョン)US/EU リージョンAWS 全リージョン
インフラモデルFirecracker VM (個別)コンテナ (Nomad)コンテナ (マネージド)
デプロイ単位App + MachineServiceTask + Service
マルチリージョン標準サポート (anycast)一部別途設計が必要
DB / Redis内蔵 (Postgres, Upstash)内蔵 (Postgres, Redis)RDS / ElastiCache
価格モデル使用量ベース (分単位)使用量ベース (月単位)時間/メモリベース
学習コスト低い最も低い高い
ロックイン低い低い (Docker イメージ)高い (AWS エコシステム)

選定基準を 1 行で:

  • 素早く立ち上げてすぐ移せる必要がある → Railway または Fly.io。Docker イメージさえあれば移しやすい。
  • すでに AWS 上で運用中 → ECS。他の AWS サービス(RDS, S3, IAM)との連携が自然。
  • グローバルユーザー / 低遅延 → Fly.io。edge が標準。

この記事は最初の 2 つのオプションを深く扱い、ECS は AWS 実戦 #1 ECS デプロイ につなげます。

Fly.io — fly launch フロー #

Fly.io は Docker イメージを受け取って Firecracker VM(=Machine)に上げるモデルです。一つの App の中に複数の Machine があり、各 Machine が一つのコンテナを焼く構造。

1. CLI インストール + ログイン。

Fly CLI
brew install flyctl
fly auth login

2. fly launch で開始。

fly launch はディレクトリを見て自動的に適切な fly.toml を作ってくれます。Dockerfile があればそれを優先使用します。

App 作成
cd my-fastapi-app
fly launch
# - App 名選択
# - リージョン選択 (最も近い場所が推奨される)
# - Postgres を一緒に作るか聞かれる → Yes なら一緒に作って DATABASE_URL 自動注入

生成される fly.toml:

fly.toml
app = "my-fastapi-app"
primary_region = "nrt"   # 東京

[build]
  # Dockerfile を使用 (自動検出)

[env]
  PYTHONUNBUFFERED = "1"

[http_service]
  internal_port = 8000
  force_https = true
  auto_stop_machines = "stop"      # トラフィックなしで自動 stop (コスト削減)
  auto_start_machines = true        # 初回リクエストで自動 start
  min_machines_running = 0

  [[http_service.checks]]
    interval = "30s"
    timeout = "5s"
    grace_period = "10s"
    method = "GET"
    path = "/healthz"

[[vm]]
  cpu_kind = "shared"
  cpus = 1
  memory_mb = 512

auto_stop_machines = "stop" が興味深いポイントです。トラフィックがなければ自動的にコンテナ停止、初回リクエストが来ると自動起動。cold start が 0.5〜2 秒程度 — サイドプロジェクトやトラフィックチャーンが大きい環境に有用です。

3. シークレット注入。

DATABASE_URL のようなシークレットは fly secrets で注入。fly.toml には絶対に刺さないでください。

シークレット
fly secrets set DJANGO_SECRET_KEY=$(openssl rand -hex 32)
fly secrets set DATABASE_URL="postgres://..."
fly secrets list

secrets set は自動的に App を再デプロイして新しい環境変数を適用します。シークレットはビルド時点ではなく ランタイム にだけ入ります — イメージには刺さらない(#5 で見た原則です)。

4. デプロイ。

デプロイ
fly deploy
# またはすでに push 済みのイメージを使うとき
fly deploy --image ghcr.io/me/app:sha-a1b2c3d

Fly がイメージを受け取って新しい Machine を作り、ヘルスチェック通過を待った後にトラフィックを移します。rolling 戦略が標準 — zero-downtime。

5. ログ / 状態 / シェル。

運用
fly status
fly logs
fly ssh console        # コンテナにシェルで入る
fly machine restart
fly scale count 3      # Machine 3 個に水平スケール
fly scale memory 1024  # メモリ変更

CI で自動デプロイは単純:

.github/workflows/deploy-fly.yml
name: Deploy to Fly.io

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

--remote-only は Fly のビルダーでビルド — GHA ランナーでビルドしないのでワークフロー時間が短くなります。ただし、Fly ビルダー使用量制限があります。より大きなキャッシュが必要なら #4 のように GHA でビルド → レジストリ push → flyctl deploy --image ... フローを使ってください。

Railway — railway up #

Railway は最速の立ち上げが強み。UI がきれいで、Docker + 環境変数 + Postgres が同じ画面で一度に束ねられます。

1. CLI インストール + ログイン。

Railway CLI
brew install railwayapp/railway/railway
railway login

2. プロジェクト作成と接続。

Web コンソールでプロジェクトを作ると GitHub リポジトリ接続を聞かれます。接続すれば main push のたびに自動ビルド/デプロイ。Dockerfile があればそれを優先使用 (なければ Nixpacks 自動検出)。

CLI のみで行うなら:

CLI デプロイ
cd my-app
railway init      # 新規プロジェクト
railway up        # 現在のディレクトリをビルドしてデプロイ

3. シークレットとサービス接続。

コンソールで Postgres サービスを追加すると、自動的に DATABASE_URL のような環境変数を他のサービスに注入します。CLI でも可能。

環境変数
railway variables set DJANGO_SECRET_KEY=$(openssl rand -hex 32)
railway variables                # 現在の変数確認
railway run -- python manage.py migrate    # 環境変数注入したままコマンド実行

4. healthcheck と zero-downtime。

railway.json (または railway.toml) で healthcheck 設定。

railway.json
{
  "$schema": "https://railway.com/railway.schema.json",
  "build": {
    "builder": "DOCKERFILE",
    "dockerfilePath": "Dockerfile"
  },
  "deploy": {
    "healthcheckPath": "/healthz",
    "healthcheckTimeout": 30,
    "restartPolicyType": "ON_FAILURE",
    "numReplicas": 2
  }
}

Railway も healthcheck 通過後にトラフィックを移す rolling デプロイが標準です。numReplicas: 2 なら zero-downtime。

Fly vs Railway を短く。

Railway は本当に「Docker イメージをただ回す」に近いです。マルチリージョン、edge、anycast のような点では Fly が強い。シンプルなフルスタックアプリなら Railway が最速で立ち上げるオプションです。

ECS Fargate — 短く #

ECS は AWS トラック で深く扱ったので、ここでは輪郭だけ触れます。

主要概念:

  • Task Definition — どのイメージをどんなリソースでどう上げるか定義 (JSON)。
  • Task — Task Definition から作られた一つのインスタンス (= コンテナ一束)。
  • Service — Task を N 個維持するマネージャ。ALB と束ねてトラフィックを分配。
  • Cluster — 上のリソースを入れる器。

デプロイフローを 1 行で:

  1. ECR にイメージ push (CI が aws ecr get-login-passworddocker push)。
  2. Task Definition の image フィールドを新しい SHA タグに更新 (revision +1)。
  3. Service が新 revision で rolling デプロイ — 新 Task が立ち上がってヘルスチェック通過後に旧 Task 終了。

シンプルなワークフロー例:

.github/workflows/deploy-ecs.yml — 骨子
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::ACCOUNT:role/github-actions
    aws-region: ap-northeast-2

- uses: aws-actions/amazon-ecr-login@v2
  id: login-ecr

- name: Build, tag, push
  run: |
    docker build -t $ECR_REGISTRY/$REPO:sha-${{ github.sha }} .
    docker push $ECR_REGISTRY/$REPO:sha-${{ github.sha }}
  env:
    ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}

- name: Update task definition
  id: task-def
  uses: aws-actions/amazon-ecs-render-task-definition@v1
  with:
    task-definition: task-def.json
    container-name: web
    image: ${{ steps.login-ecr.outputs.registry }}/${{ env.REPO }}:sha-${{ github.sha }}

- name: Deploy
  uses: aws-actions/amazon-ecs-deploy-task-definition@v1
  with:
    task-definition: ${{ steps.task-def.outputs.task-definition }}
    service: my-service
    cluster: my-cluster
    wait-for-service-stability: true

詳細な IAM セットアップ、ALB / Target Group / ヘルスチェック、RDS 連携、コスト最適化は AWS 実戦 #1 からの 6 編で扱います。

Zero-downtime の共通原理 #

プラットフォームが違っても zero-downtime デプロイの原理は同じです。

rolling デプロイの流れ
初期:        [v1]  [v1]  [v1]   ← LB
v2 push →   [v1]  [v1]  [v1]
            [v2]                ← 新インスタンス立ち上げ
            ┌── healthcheck ──┐
            │  /healthz 応答? │
            └─────────────────┘
            [v1]  [v1]  [v2]   ← LB 登録、旧インスタンス 1 個削除
            [v1]  [v2]  [v2]
            [v2]  [v2]  [v2]   ← 完了

各段階で必要なもの:

  • /healthz エンドポイント — App が実際に受け取れる状態か確認。DB 接続可能かまで見て初めて正確。早すぎる 200 を返すとウォームアップ前にトラフィックが入って初回リクエストが壊れます。
  • graceful shutdown — 旧インスタンスを殺すとき SIGTERM を受けて、in-flight リクエストを終わらせてから終了。PID 1 がシグナルを正確に受けるのが前提 (#2 の exec "$@"上級 #6 で扱った内容)。
  • 十分なインスタンス数 — 1 個では zero-downtime ができません。少なくとも 2 個から rolling。
  • DB マイグレーション互換性 — 新しいコードが動いている間、旧コードも一緒に動きます。マイグレーションは常に backward-compatible に (例: カラム追加は nullable、カラム削除は二段階で)。

シークレット管理 — プラットフォーム別 #

ランタイムシークレット(DATABASE_URL、API キー)は絶対にイメージに刺さず、プラットフォームのシークレット管理に置きます。

プラットフォームシークレットの保管先
Fly.iofly secrets set (encrypted at rest, コンテナに環境変数として注入)
Railwayコンソールの Variables、または railway variables set
ECSSecrets Manager / Parameter Store + Task Definition の secrets セクション
KubernetesSecret リソースまたは ExternalSecrets / SOPS

共通原則:

  • ビルド時点に刺さないこと (--build-arg ではなくランタイム環境変数)。
  • リポジトリ(.env.production)にも絶対コミット禁止。
  • CI が直接シークレットを扱う必要があれば GHA の secrets.* または OIDC でクラウド資格を一時発行。

バックエンド + フロント + DB の一般的配置 #

Docker トラックの成果物を 1 枚の絵で:

実戦配置 — よくある形
              ユーザー
        ┌───────────────┐
        │   CDN/LB      │   (Cloudflare / ALB / Fly anycast)
        └───┬───────┬───┘
            │       │
       ┌────▼─┐  ┌──▼────────┐
       │  Web │  │   API     │   (Next.js / FastAPI · Django)
       │ (#3) │  │  (#1, #2) │   ← コンテナでデプロイ
       └──────┘  └─────┬─────┘
                ┌──────▼───┐
                │   DB     │   (RDS / Fly Postgres / Railway PG)
                │ Postgres │
                └──────────┘

各レイヤーで Docker トラックが扱ったもの:

  • Web コンテナ#3 の standalone / 静的 export。
  • API コンテナ#1 の uv・マルチステージ・non-root。
  • DB — プロダクションはマネージドサービスが定石 (RDS / Fly Postgres / Railway PG)。#2 の compose パターンはローカル・開発用。
  • CI ビルド/push#4#5
  • デプロイ — 今回。

Docker トラック 24 編の振り返り #

基礎 6 編でコンテナの位置から押さえ、中級 6 編でマルチステージ/compose/環境変数、上級 6 編で BuildKit/セキュリティ/リソース/PID 1、そして実戦 6 編で FastAPI / Django / Next.js / CI / タグ / デプロイまで — 一サイクルが閉じました。

基礎 #1 の最初の文を思い出すと、「自分のマシンでは動くんだけど」を解決するためにコンテナが登場した、と書きました。24 編の終わりで同じ文に答えられます — イメージ 1 個がどこでも同じ動作をして、CI がビルドして push して、クラウドがそれを受け取って回す。 その中の細かいしわ (PID 1、healthcheck、マルチアーキ、シークレット、タグ) が運用の実際の論点です。

ここからさらに進める方向:

  • Kubernetes — 1 つのコンテナでなく 数十個のサービス を運用する世界。ECS/Fly/Railway が隠していた抽象が K8s ではそのまま現れる。大きな組織 / マルチチーム / セルフホスティングがトリガー。
  • Service Mesh (Istio, Linkerd) — コンテナ間通信に mTLS・観測・ポリシーを乗せる層。
  • Container Native CI/CD — Tekton、ArgoCD のような GitOps フロー。

このトラックは 1 人〜小さなチームの運用を整える段階で終わり、上の項目は別トラックとして育ちます。

まとめ #

  • クラウドデプロイの最初の分かれ道は Fly.io vs Railway vs ECS。素早く立ち上げるなら Railway、edge なら Fly、AWS 上なら ECS。
  • どこでも共通: イメージを受け取って → ヘルスチェック通過まで新インスタンス焼いて → LB がトラフィック移して → 旧インスタンス終了。rolling デプロイ。
  • zero-downtime の 4 つの要件: /healthz エンドポイント / graceful shutdown / インスタンス ≥ 2 / backward-compatible マイグレーション。
  • シークレットは ランタイムだけ。プラットフォームのシークレットマネージャに。イメージには絶対刺さないこと。
  • 運用マニフェストの image:SHA タグ (#5)。ロールバックが 1 行変更。
  • Docker トラック 24 編はここで閉じます。より大きな組織 / セルフホスティングへ行くなら Kubernetes が次です。

Docker トラックはここで締めくくります。他のトラック — モダン Python / Django / Go / TypeScript / React / Angular / AWS — の最終回で Docker が常に登場しました。今やその Docker がこのトラックで最初から最後まで扱われたので、すべてのトラックのデプロイを同じ道具で解けるようになりました。

X