コンテンツにスキップ

Vendure Fly.io デプロイメントガイド

概要

この文書では、Vendure を Fly.io で本番環境にデプロイするための完全な手順を説明します。

正規 deploy 契約

  • feature/* から develop への PR を merge すると、CI (push develop) が post-merge build / typecheck と staging 用 image build を実行します。staging deploy は自動実行せず、必要なタイミングで just manual-deploy-staging または GitHub Actions の Deploy Staging を手動起動し、その同一 image を flyctl deploy --image ... で deploy します。
  • develop で検証が通った変更だけを main へ昇格し、GitHub Actions が staging で build 済みの同一 image を production へ promote して deploy します。
  • Fly.io 側の remote build は通常運用でも緊急時でも使いません。staging / production の手動 deploy も DEPLOY_IMAGE_REF 必須で、緊急時は local で image を build/push してからその image_ref を deploy します。
  • release phase は migration + structural repair に限定し、fixture / baseline / policy seed は deploy に含めません。
  • production の release phase は DB を変更し得るため、GitHub Actions / local just deploy-fly production のどちらでも deploy 直前に just backup-postgres-production を実行します。backup が失敗した場合は deploy に進みません。 just manual-deploy-vendure production 経路では backup が image build と並列に background 起動し、deploy-fly 直前で wait してから release_command を起こすため、契約 (release_command 起動前に backup 存在) は維持したまま wall-clock を ~30–60s 短縮します。 BACKUP_POSTGRES_PRODUCTION_SKIP=true は manual-deploy 経由で自動付与され、deploy-fly 内の二重 backup を抑止します (直接 just deploy-fly production を叩く運用では従来通り backup が走ります)。
  • local で just backup-postgres-production が既に成功した直後の再実行では、deploy wrapper 側の 二重 backup を避けるため precompleted_backup=<dump名またはR2 URI> を渡します。この引数は BACKUP_POSTGRES_PRODUCTION_SKIP=true を wrapper 内に閉じて設定し、ログにも直前復元点を残します。
  • bookkeeping 専用 deploy の例外: live machine が desired image / desired schema に揃っているのに Fly release だけ failed の場合は、同一 image で --skip-release-command --strategy immediate を使って release status のみ整えてよい。

Production maintenance guardrails

  • just migrate-fly productionjust vendure-drift-migrate productionjust vendure-drift-repair-structural production は、実行前に just backup-postgres-production を自動実行します。
  • just vendure-drift-sync productionjust vendure-drift-sync-baseline production は廃止済みです。policy / commercial-rule / test-customers の fixture repair を 経由し得るため、confirm token では許可しません。production drift は just vendure-drift-audit production の結果を見て、backup 後に対象を絞った audited repair で直します。
  • just proxy-postgres production は wrapper 以後の psql 操作を捕捉できないため、 just proxy-postgres production auto production-db-proxy の明示 confirm が必要です。 recipe は proxy を開く前に just backup-postgres-production を実行します。
  • just fly-console-vendure production は app 内で任意操作できるため、 just fly-console-vendure production production-console の明示 confirm が必要です。
  • production の test-customer / reservation-product sync は標準導線から無効化しています。 production smoke は既存の production-smoke principal を使い、fixture seed で顧客や商品を更新しません。

現在の構成

  • アプリケーション名: ritsubi-ecommerce(本番)、ritsubi-ecommerce-staging(ステージング)
  • PostgreSQL: Dockerコンテナ(ritsubi-postgres-db、PostgreSQL 17 + pg_trgm
  • Redis: 本番・ステージングともに Fly.io 上の専用 Redis アプリ
  • 設定ファイル: apps/vendure-server/fly.toml(本番)、apps/vendure-server/fly.staging.toml(ステージング)

詳細は PostgreSQL Dockerコンテナ設定 を参照してください。

前提条件

必要なツール

# Node.js (24.7.0以上)
node --version  # >= 24.7.0(`apps/vendure-server/package.json` の engines を参照)

# pnpm パッケージマネージャー
corepack enable
corepack prepare pnpm@11.1.2 --activate

# Fly.io CLI
curl -L https://fly.io/install.sh | sh

# Git
git --version

アカウント作成

  1. Fly.io アカウント: https://fly.io/app/sign-up
  2. Cloudflare アカウント: https://dash.cloudflare.com/sign-up (R2 ストレージ用)

Step 1: Fly.io 初期設定

Fly.io CLI セットアップ

# Fly.io にログイン
flyctl auth login

# 組織確認
flyctl orgs list

# 組織作成(必要に応じて)
flyctl orgs create ritsubi

# 支払い情報設定
flyctl billing show

release bookkeeping 復旧の判断基準

次の 3 つが揃っていれば、障害の正本は Fly release history ではなく runtime は復旧済み と判断してよい。

flyctl machine list -a ritsubi-ecommerce
flyctl checks list -a ritsubi-ecommerce --json
curl -sS https://commerce.ritsubi-platform.com/health/ready | jq
  • app machine が started
  • app checks が passing
  • /health/readydashboardApiCanary=ok / schemaDrift=ok

この状態で release history だけ failed の場合は、ghost machine を除去してから 同一 image の bookkeeping 専用 deploy を行う。

flyctl machine destroy <ghost-machine-id> -a ritsubi-ecommerce -f

flyctl deploy \
  --config apps/vendure-server/fly.toml \
  -a ritsubi-ecommerce \
  --image registry.fly.io/ritsubi-ecommerce:deployment-<image-id> \
  --skip-release-command \
  --strategy immediate \
  --wait-timeout 10m

プロジェクト初期化

# プロジェクトルートに移動
cd <repo-root>

# Fly.io アプリを作成(既に存在する場合はスキップ)
flyctl apps create ritsubi-ecommerce

# 設定ファイルは apps/vendure-server/fly.toml に配置済み

Step 2: データベース設定

PostgreSQL Dockerコンテナデプロイ

現在の構成では、PostgreSQLをDockerコンテナとしてデプロイしています。詳細な手順は PostgreSQL Dockerコンテナ設定 を参照してください。

クイックスタート

# 1. Postgresアプリ作成
cd <repo-root>/apps/postgres
flyctl apps create ritsubi-postgres-db --org ritsubi

# 2. ボリューム作成
flyctl volumes create postgres_data \
  --region nrt \
  --size 10 \
  -a ritsubi-postgres-db

# 3. パスワード設定
flyctl secrets set POSTGRES_PASSWORD="$(openssl rand -base64 32)" -a ritsubi-postgres-db

# 4. デプロイ
flyctl deploy -a ritsubi-postgres-db

# 5. Vendureアプリに接続設定
flyctl secrets set DATABASE_URL="postgres://postgres:PASSWORD@ritsubi-postgres-db.internal:5432/ritsubi_vendure" -a ritsubi-ecommerce

データベース初期化

デプロイ後、Vendureアプリケーションが自動的にデータベーススキーマを初期化します。

Step 3: Redis 設定(本番・ステージングとも専用 Redis)

現在の構成

本番は ritsubi-redis-prod、ステージングは ritsubi-redis-staging のように Fly.io 上の専用 Redis アプリを使用します。REDIS_* が未設定の場合は in-memory キャッシュにフォールバックします。

Redis の SSOT は現行の専用 Redis アプリ運用です。

# staging Redis app 作成
flyctl apps create ritsubi-redis-staging --org ritsubi
flyctl volumes create redis_data --region nrt --size 1 -a ritsubi-redis-staging
flyctl secrets set REDIS_PASSWORD="$(openssl rand -hex 32)" -a ritsubi-redis-staging
flyctl deploy --config apps/redis/fly.toml -a ritsubi-redis-staging

# staging Vendure app に Redis 接続情報を設定
flyctl secrets set REDIS_HOST="ritsubi-redis-staging.internal" -a ritsubi-ecommerce-staging
flyctl secrets set REDIS_PASSWORD="..." -a ritsubi-ecommerce-staging
flyctl secrets set REDIS_TLS="false" -a ritsubi-ecommerce-staging
flyctl secrets set USE_REDIS_SESSION="true" -a ritsubi-ecommerce-staging

本番 も同様に ritsubi-redis-prod.internalREDIS_HOST に設定し、 REDIS_TLS=false で内部接続します。

Step 4: 環境変数・シークレット設定

必須シークレット設定

# セッション・認証関連
flyctl secrets set SESSION_SECRET="$(openssl rand -base64 32)" -a ritsubi-ecommerce
flyctl secrets set COOKIE_SECRET="$(openssl rand -base64 32)" -a ritsubi-ecommerce

# 管理者アカウント
flyctl secrets set ADMIN_EMAIL="admin@ritsubi.co.jp" -a ritsubi-ecommerce
flyctl secrets set ADMIN_PASSWORD="secure_admin_password" -a ritsubi-ecommerce

# Production hardening: production deploy 前に必ず設定する (未設定だと vendure 起動が abort する)
# 詳細は docs/03-implementation/infrastructure/security-evidence.md §6 参照
flyctl secrets set VENDURE_HARDEN_PLUGIN_ENABLED=true -a ritsubi-ecommerce
# CORS_ORIGIN / STOREFRONT_URL / ADMIN_URL のいずれかが必須 (空だと originValidator が起動時 abort)
flyctl secrets set CORS_ORIGIN="https://order.ritsubi-platform.com,https://dashboard.ritsubi-platform.com" -a ritsubi-ecommerce
# ADMIN_URL (React Dashboard origin) は public URL のため AWS Secrets Manager に置かず deploy-targets.sh を SSOT にする。
# `just sync-fly-secrets {env}` が deploy-targets の DEPLOY_REACT_DASHBOARD_BASE_URL から ADMIN_URL を每回再注入し、
# verify-vendure-deploy の check-fly-runtime-secrets.sh が runtime 実値を canonical と照合して drift を fail-closed で止める。
# 上の手動 set は bootstrap / 緊急時のみ。値を手書きすると次回 sync で SSOT 値に上書きされる。
# VENDURE_API_DOCS_ENABLED は production では設定しない (=Swagger UI/OpenAPI は serve されない)。
# 一時的な調査などで有効化したい場合のみ true を設定する。

# SES 関連 env (EMAIL_TRANSPORT / EMAIL_FROM / EMAIL_TEST_RECIPIENT / AWS_ACCESS_KEY_ID/SECRET) は
# AWS Secrets Manager (`b2b-ecommerce/{env}/vendure`) を正本にする。AWS_REGION / AWS_DEFAULT_REGION
# は `b2b-ecommerce/{env}/shared`。詳細は ./secrets-manager-operations.md を参照。
# 反映: `just sync-fly-secrets {env}` で AWS → Fly に同期。下の flyctl 例は bootstrap / 緊急時のみ。

## Vendure で SES を使う場合

### 1. AWS 側で確認する値

reputation / suppression list / 監視メトリクスを完全に分離するため、SES identity を環境別に持つ:

| 環境 | identity | EMAIL_FROM | MAIL FROM |
| --- | --- | --- | --- |
| production | `ritsubi.co.jp` | `order@ritsubi.co.jp` | `bounces.ritsubi.co.jp` |
| staging | `ritsubi-platform.com` | `noreply@ritsubi-platform.com` | `mail.ritsubi-platform.com` |

両環境共通:
- `AWS_REGION=ap-northeast-1` / `AWS_DEFAULT_REGION=ap-northeast-1`
- IAM user `ritsubi-vendure-ses-sender`  inline policy `ritsubi-vendure-ses-send` が両 identity への `ses:SendEmail` / `ses:SendRawEmail` を許可
- configuration set `ritsubi-transactional` を両 identity が共有

### Secrets Manager 更新 → Fly 同期の流れ

env vars (EMAIL_FROM ) の正本は **AWS Secrets Manager**。直接 `flyctl secrets set` で恒久値を書かない。

```bash
# 1) Secrets Manager の対象 secret を更新 (例: production EMAIL_FROM)
payload="$(aws secretsmanager get-secret-value --region ap-northeast-1 --profile ritsubi \
  --secret-id b2b-ecommerce/prod/vendure --query SecretString --output text)"
updated="$(printf '%s' "$payload" | jq -c '.EMAIL_FROM = "order@ritsubi.co.jp"')"
aws secretsmanager update-secret --region ap-northeast-1 --profile ritsubi \
  --secret-id b2b-ecommerce/prod/vendure --secret-string "$updated"

# 2) Fly secrets へ同期 (内部で scripts/ops/sync-fly-secrets.sh が AWS → Fly を反映)
just sync-fly-secrets production
just sync-fly-secrets staging

# sync-fly-secrets は `flyctl secrets import --stage` の後に `flyctl secrets deploy`
# まで実行する。手動で import した場合も、staged のままでは running machine の
# env に出ないため必ず deploy する。
flyctl secrets deploy -a ritsubi-ecommerce

# 3) drift 確認 (Secrets Manager / SES / Fly の3点照合)
just vendure-ses-email-audit production true    # 末尾 `true` = Fly secrets まで照合
just vendure-ses-email-audit staging true
``` env で対象となる secret:

| env | EMAIL_FROM / EMAIL_TEST_RECIPIENT / EMAIL_TRANSPORT / AWS_ACCESS_KEY_ID/SECRET | AWS_REGION / AWS_DEFAULT_REGION |
| --- | --- | --- |
| production | `b2b-ecommerce/prod/vendure` | `b2b-ecommerce/prod/shared` |
| staging | `b2b-ecommerce/staging/vendure` | `b2b-ecommerce/staging/shared` |

詳細手順は [`./secrets-manager-operations.md`](./secrets-manager-operations.md) を参照。

### 2. 独自ドメインの DNS レコード

#### production identity `ritsubi.co.jp` (dnsv.jp / Value-Domain 権威管理)

```text
CNAME o46nxijgtlxm23u5kwrfjy2zmrpka7kh._domainkey.ritsubi.co.jp  o46nxijgtlxm23u5kwrfjy2zmrpka7kh.dkim.amazonses.com
CNAME jwrqhy7v5uv63xoofzpicvnbafs273j2._domainkey.ritsubi.co.jp  jwrqhy7v5uv63xoofzpicvnbafs273j2.dkim.amazonses.com
CNAME ncot23ndibnjwwnmhca2rzps6e545xtk._domainkey.ritsubi.co.jp  ncot23ndibnjwwnmhca2rzps6e545xtk.dkim.amazonses.com
MX    bounces.ritsubi.co.jp 10 feedback-smtp.ap-northeast-1.amazonses.com
TXT   bounces.ritsubi.co.jp "v=spf1 include:amazonses.com ~all"
TXT   _dmarc.ritsubi.co.jp "v=DMARC1; p=none; rua=mailto:dmarc@ritsubi.co.jp"
```

- dnsv.jp の管理画面で手動追加する (`just vendure-ses-cloudflare-dns-sync` は対象外)。
- apex  SPF には既に `include:amazonses.com` が含まれているので apex は変更不要。
- apex  MX (Microsoft 365) は受信用なので触らない。SES MAIL FROM  subdomain で独立。

#### staging identity `ritsubi-platform.com` (Cloudflare 権威管理)

Cloudflare 側の DNS  `infra/terraform/cloudflare-apps/ritsubi-platform.com/`  Terraform と、
`scripts/ops/sync-ses-cloudflare-dns.mjs` (= `just vendure-ses-cloudflare-dns-sync`) で同期する。

```bash
just vendure-ses-cloudflare-dns-sync true   # dry-run
just vendure-ses-cloudflare-dns-sync
```

DNS 反映後の確認:

```bash
aws sesv2 get-email-identity --email-identity ritsubi.co.jp        --region ap-northeast-1 --profile ritsubi
aws sesv2 get-email-identity --email-identity ritsubi-platform.com --region ap-northeast-1 --profile ritsubi
# 各 identity の DkimAttributes.Status / MailFromAttributes.MailFromDomainStatus / VerifiedForSendingStatus を確認
```

### SES 推奨設定

送信認証に加えて、SES  configuration set `ritsubi-transactional` を両 identity
(`ritsubi.co.jp` / `ritsubi-platform.com`) に関連付ける。configuration set では次を有効化する。

- reputation metrics
- configuration set suppression options: `BOUNCE`, `COMPLAINT`
- EventBridge event destination: `SEND`, `REJECT`, `BOUNCE`, `COMPLAINT`, `DELIVERY`,
  `RENDERING_FAILURE`, `DELIVERY_DELAY`
- CloudWatch metrics event destination: 同上、dimension `ses_config_set=ritsubi_transactional`

```bash
# 目的: SES の到達性監視・bounce/complaint 抑止・event publishing を console 手作業ではなく再実行可能にする。
just vendure-ses-recommended-settings-sync
just vendure-ses-email-audit staging
```

Open / click tracking  transactional email では不要な計測・リンク書き換えを増やすため、既定では有効化しない。

SES production access を申請する際の Website URL は、公開到達できる会社サイトを指定する。
`ritsubi.co.jp` は会社ドメインで apex  Web サイトもあるため、SES production access の
Website URL にはこちらを指定する。送信用ドメインと会社サイトが同一であることを use case に明記する。

Secrets Manager  Vendure 設定で実メール経路を確認する場合は、全 transactional template `EMAIL_TEST_RECIPIENT` 宛に送る smoke を使う。dry-run はテンプレート render と宛先解決だけを行い、
通常実行は runtime IAM credential  SESv2 `SendEmail` を呼ぶ。

```bash
# 目的: staging / production の実 runtime secret で、全 transactional template が SES 送信できることを確認する。
just vendure-transactional-email-smoke staging true
just vendure-transactional-email-smoke staging
just vendure-transactional-email-smoke production true
just vendure-transactional-email-smoke production
```

SES sandbox 中は recipient  verified identity である必要がある。最小権限 IAM policy `Resource` を送信元 domain identity に絞っている場合、sandbox smoke 用の verified recipient
email identity も同 policy に含める。

### 3. 送信テスト

環境変数を投入したら Vendure のメール送信フロー(会員登録やパスワード再発行)を実行し、SES 側の配信イベント(delivery/bounce/complaint)で成功を確認する。

### 決済設定(PayPay)

```bash
flyctl secrets set PAYPAY_API_KEY="your_paypay_api_key" -a ritsubi-ecommerce
flyctl secrets set PAYPAY_SECRET_KEY="your_paypay_secret_key" -a ritsubi-ecommerce
flyctl secrets set PAYPAY_MERCHANT_ID="your_merchant_id" -a ritsubi-ecommerce
```

### ファイルストレージ(Cloudflare R2)

```bash
flyctl secrets set VENDURE_R2_ACCESS_KEY_ID="your_access_key" -a ritsubi-ecommerce
flyctl secrets set VENDURE_R2_SECRET_ACCESS_KEY="your_secret_key" -a ritsubi-ecommerce
flyctl secrets set VENDURE_R2_BUCKET_NAME="ritsubi-vendure-media-prod" -a ritsubi-ecommerce
flyctl secrets set VENDURE_R2_REPORTS_BUCKET_NAME="ritsubi-vendure-reports-prod" -a ritsubi-ecommerce
flyctl secrets set VENDURE_R2_ACCOUNT_ID="your_cloudflare_account_id" -a ritsubi-ecommerce
flyctl secrets set VENDURE_R2_ENDPOINT="https://<ACCOUNT_ID>.r2.cloudflarestorage.com" -a ritsubi-ecommerce
flyctl secrets set VENDURE_R2_REGION="auto" -a ritsubi-ecommerce
flyctl secrets set VENDURE_R2_FORCE_PATH_STYLE="true" -a ritsubi-ecommerce
flyctl secrets set ASSET_STORAGE_STRATEGY="r2" -a ritsubi-ecommerce
flyctl secrets set VENDURE_ASSET_URL="https://ec-assets.ritsubi-platform.com/" -a ritsubi-ecommerce
```

### 外部連携(SMILE ERP)

```bash
flyctl secrets set SMILE_API_ENDPOINT="https://api.smile.co.jp" -a ritsubi-ecommerce
flyctl secrets set SMILE_API_KEY="your_smile_api_key" -a ritsubi-ecommerce
flyctl secrets set SMILE_COMPANY_ID="your_company_id" -a ritsubi-ecommerce
```

```bash
# fly.toml で設定される環境変数を確認
flyctl config show -a ritsubi-ecommerce
```

## Step 5: Cloudflare R2 ストレージ設定

### バケット作成(Cloudflare R2 の例)

```bash
# Cloudflare R2 バケット作成(Cloudflare ダッシュボードで実行)
# 1. https://dash.cloudflare.com にログイン
# 2. R2 Object Storage に移動
# 3. "Create bucket" をクリック
# 4. Bucket name: ritsubi-assets
# 5. Location: Asia Pacific (推奨)
```

### API トークン作成(Cloudflare R2 の例)

```bash
# 1. Cloudflare ダッシュボード > My Profile > API Tokens
# 2. "Create Token" > "Custom token"
# 3. Permissions:
#    - Account: Cloudflare R2:Edit
#    - Zone Resources: Include All zones
# 4. 生成されたトークンをシークレットに設定(上記参照)
```

## Step 6: ボリューム作成

### 永続データ用ボリューム

```bash
# データ永続化用ボリューム作成
flyctl volumes create vendure_data \
  --region nrt \
  --size 5 \
  -a ritsubi-ecommerce

# ボリューム確認
flyctl volumes list -a ritsubi-ecommerce
```

## Step 7: Depotビルダー設定(高速ビルド)

### 手動 deploy の短縮入口

手動 deploy で毎回 `gh workflow run`  build/deploy/verify/Sentry marker を手でつなぐ代わりに、次の `just` レシピを正本にする。

```bash
# staging workflow_dispatch を起動して、そのまま完了まで追跡
just manual-deploy-staging

# staging で対象 surface を絞る
just manual-deploy-staging ref=develop vendure=true storefront=false dashboard=true

# Storefront Worker version の再デプロイを起動して追跡(version_ref は必須)
# UUID 形式なら version ID、それ以外は commit sha として自動判別
just manual-deploy-storefront-version environment=production version_ref=<commit_sha_or_version_uuid>

# Vendure の緊急 local manual deploy を build -> deploy -> verify -> Sentry marker まで一括実行
just manual-deploy-vendure staging <commit_sha>

# 既存 image を使う場合は build を省略
just manual-deploy-vendure production "" registry.fly.io/ritsubi-ecommerce:vendure-<source_sha>
```

### Storefront Worker deploy の共通入口

Storefront  Cloudflare Worker デプロイは **`scripts/ops/deploy-storefront-worker.sh <production|staging|preview|mock>` を唯一の入口 (SSOT)** にする。「AWS secret hydrate (`with-env.sh`, `shared,storefront`)  Cloudflare 認証 (`with-cloudflare-auth.sh`)  build  `cloudflare-deploy.mjs`」の連鎖を 1 箇所に持たせ、経路ごとに secret 注入を再実装して diverge するのを防ぐ。

```bash
# 手動デプロイ (nx target も内部でこのスクリプトを呼ぶ)
scripts/ops/deploy-storefront-worker.sh staging
scripts/ops/deploy-storefront-worker.sh production
# 事前ビルド済み bundle / promote 用に build を省略
STOREFRONT_DEPLOY_SKIP_BUILD=1 scripts/ops/deploy-storefront-worker.sh production --upload-only
```

背景と不変条件:

- `cloudflare-deploy.mjs`  `[vars]` テーブルを丸ごと上書きする。secret  hydrate されていない経路で叩くと `SENTRY_DSN` 等が欠落し、Worker  `enabled:false` で起動して Sentry が沈黙する(本番が約1週間サイレント化した事故の原因)。共通入口が常に secret  hydrate するため、bare 実行でも欠落しない。実行には AWS SSO ログイン(`RITSUBI_AWS_PROFILE` 既定 `ritsubi`)が必要。
- backstop として `cloudflare-deploy.mjs`  `assertDeployedRuntimeEnv`  staging/production 時に `SENTRY_DSN` / `VITE_PUBLIC_SENTRY_DSN` 欠落を deploy fail させる(黙った fail-open 防止)。
- `nx run ritsubi-storefront:cloudflare:deploy[:staging|:preview|:mock]` / `pnpm -C apps/storefront cloudflare:deploy*` はいずれもこの共通スクリプトに委譲する薄いラッパ。
- `SENTRY_ENVIRONMENT`  canonical  `production` / `staging`(Sentry 履歴・vendure・code 既定と一致)。AWS secret 側で `prd` / `stg`  override しないこと。

### 新 migration / 新 entity を含む deploy

新しい TypeORM migration  entity を追加した branch  deploy するとき、`manual-deploy-vendure`  pre-deploy drift audit  **必ず fail** する。理由:

-  entity  table / column はまだ DB に存在しない  `structural schema drift`
-  column  migration  drop する  `unexpected custom field columns`
-  migration はまだ applied 履歴にない  `migration history missing expected migrations`

これらは release-phase  migration が直後に解消する **reconcilable drift** で、本来 deploy を止める drift(baseline 編集 / `unknown applied migration` / policy 不整合)とは別物。区別するため、専用入口を使う:

```bash
# 推奨: pre-deploy audit を allow-pending-migrations モードで実行し、reconcilable drift を許容
just deploy-vendure-with-migration staging
just deploy-vendure-with-migration staging <commit_sha>
just deploy-vendure-with-migration production "" registry.fly.io/ritsubi-ecommerce:vendure-<source_sha>
just deploy-vendure-with-migration production "" "" true precompleted_backup=ritsubi_vendure_YYYYMMDDHHMMSS.dump
```

この入口は内部で `manual-deploy-vendure ... allow_pending_migration=true` を渡し、audit  reconcilable  issue  WARN として表示し、それ以外の本物の drift が出た場合は通常通り abort する。

deploy 後の確認も普通の audit で済む:

```bash
# release-phase の migration が走った後、drift が 0 になっているはず
just vendure-drift-audit staging
just vendure-drift-audit staging auto true   # allow-pending-migrations を残したまま確認したい場合
```

`migrate-fly` を先に流しても **No pending migrations になるだけ** で意味がない。`migrate-fly` は現在 deploy 中の image 内の migration を実行するため、新 image  deploy する前に走らせても新 migration は見つからない。**code 変更を含む migration は必ず `deploy-vendure-with-migration`  deploy + release-phase を同時に行う**。

### 破壊的 migration (DROP COLUMN / RENAME) を含む deploy

release phase  migration  bluegreen deploy  health check **より前** に実行される。
破壊的 SQL (DROP COLUMN / DROP TABLE / RENAME COLUMN) を含む migration  release phase で
走らせると、health check 失敗  rollback  旧コードが drop 済み column を参照  crash loop
という不可逆障害が発生する (2026-06-08 障害の根本原因)。

このため `ALLOW_DESTRUCTIVE_MIGRATIONS=true` が明示的に設定されていない限り(デフォルト)、
pending migration に破壊的 SQL パターンが含まれると release phase  abort する。

**手順: 破壊的 migration を安全に deploy する**

1. migration  **additive (CREATE / ADD COLUMN)**  **destructive (DROP / RENAME)** に分割する。
   同一 migration ファイル内で ADD  DROP を混ぜない。
2. additive migration だけを含む状態で `deploy-vendure-with-migration` を実行する。
3. bluegreen health check  pass し、新コードが全 machine で稼働していることを確認する。
4. 破壊的 migration  post-deploy step として実行する:

```bash
# staging
just migrate-fly-destructive staging

# production (backup 必須)
just backup-postgres-production
just migrate-fly-destructive production
```

`migrate-fly-destructive` は内部で `ALLOW_DESTRUCTIVE_MIGRATIONS=true` を設定して
Fly machine 上の新 image  migration を実行する。旧コードは既に停止しているため、
drop された column を参照するプロセスは存在しない。実行後に自動で drift audit を行う。

**既存の破壊的 migration が全て適用済みの場合**: ガードは pending migration のみを
スキャンするため、過去に適用済みの破壊的 migration は影響しない。

#### 破壊的判定の SSOT と検出範囲

破壊的パターンの定義は
[`apps/vendure-server/src/migration/destructive-migration-patterns.ts`](../../../apps/vendure-server/src/migration/destructive-migration-patterns.ts) **単一の正本 (SSOT)** とし、runtime guard・PR CI gate・irreversible 分類が全てこれを参照する
(grep パターンの二重定義を持たない)。検出は **raw SQL** (`query('... DROP COLUMN ...')`) と
**TypeORM builder メソッド** (`queryRunner.dropColumn(...)` / `dropTable` / `renameColumn` /
`renameTable`) の両形式をカバーする。`up()` メソッド本体のみを対象とし、`down()` の逆操作は
検出しない。

`block: true` (DROP COLUMN / DROP TABLE / RENAME COLUMN / RENAME TABLE) のみ release phase で
hard block する。`ALTER COLUMN` / `changeColumn` は誤検出が正常 deploy を止めるリスクがあるため
runtime では block せず、PR review でのみ要注意扱い (`block: false`)#### expand / contract 規律

破壊的 (contract) migration には header コメントで対になる additive (expand) migration を宣言する:

```ts
// @destructive: DROP COLUMN product.legacy_code (smile_code へ移行済み)
// @expand-migration: AddSmileCode1779000000000
```

予め deploy 済みの additive migration が不要な純削除 (どのコードからも参照されない column の除去) は
理由付きで `NONE` を明示する:

```ts
// @expand-migration: NONE (application code から一度も参照されていない)
```

二段の防波堤がこれを強制する:

1. **PR gate** (`pnpm run lint:vendure-destructive-migrations`, `_vendure-db-dry-run-guard.yml` ):
   変更された migration が破壊的なのに `@destructive` / `@expand-migration` 注釈を欠く場合 fail。
   既存の legacy migration  PR diff に現れないため grandfather される。
2. **runtime 検証** (`migrate-fly-destructive` 実行時): `ALLOW_DESTRUCTIVE_MIGRATIONS=true` でも、
   宣言された `@expand-migration` が対象 DB  **未適用** なら fail-closed で停止する
   (expand  deploy  contract を流すと旧コードが crash するため)。注釈が読めない場合
   (コメント除去等)  PR gate  source で担保済みのため runtime では warn に留める。

### `manual-deploy-vendure` の fan-in 並列構造

`just manual-deploy-vendure`  `just deploy-vendure-with-migration`  orchestration `scripts/ops/manual-deploy-vendure.sh` に分離されており、wall-clock 短縮のため次の 4 系統を
**同時 background 起動** する fan-in pattern を採用している:

1. **drift audit** (`just vendure-drift-audit`)  `skip_drift_audit=true` 時は省略
2. **image build** (`scripts/ops/build-vendure-image.sh`)  `image_ref` 引数が空のときのみ
3. **Dashboard deploy** (`nx run ritsubi-vendure-server:dashboard:deploy:<env>`)    `deploy_dashboard=true` かつ `DEPLOY_PARALLEL_DASHBOARD=true` (既定) のときのみ
4. **production backup** (`just backup-postgres-production`)  `env=production` のときのみ
   (`precompleted_backup` 指定時は wrapper  backup  skip し、指定した dump 名/URI  log に残す)

これらを `wait` した後で順序保証が必要なステップ (`flyctl deploy`  必要なら verify  Sentry marker) を
sequential で実行する。

#### staging manual deploy が image build/push で待つ場合

`just manual-deploy-vendure staging "" "" false` のように `deploy_dashboard=false` を渡しても、
`image_ref` 引数が空なら Vendure backend image  build/push は残る。特に Shop API / Vendure
plugin / server runtime を含む変更では、staging 反映の重い phase  Dashboard deploy ではなく
次の `docker buildx build` になる:

```text
just manual-deploy-vendure staging "" "" false scripts/ops/manual-deploy-vendure.sh
→ scripts/ops/build-vendure-image.sh
→ docker buildx build --platform linux/amd64 \
    --file apps/vendure-server/Dockerfile.fly \
    --output type=image,name=registry.fly.io/ritsubi-ecommerce-staging:vendure-<sha>,push=true,...
```

この phase  local 端末から `registry.fly.io`  amd64 image  push するため、Dockerfile の
cache hit 状況だけでなく registry への upload 帯域にも律速される。drift audit が成功済みなのに
deploy が進んでいない場合は、まず DB  Dashboard ではなく image build/push  in-flight process を見る:

```bash
pgrep -af 'docker buildx build|build-vendure-image|manual-deploy-vendure'
```

同じ SHA  image がすでに registry に存在する local 再実行では、`build-vendure-image.sh` `docker buildx imagetools inspect "$image_ref"` を確認し、`VENDURE_IMAGE_BUILD_IF_EXISTS=skip`
(local 既定) なら重い build  skip する。逆に source SHA が変わった、または image が未作成なら
`--cache-from registry...:vendure-buildcache`  local filesystem cache を使っても build/push は必要。
local filesystem cache は既定で `BUILDX_LOCAL_FS_CACHE_MODE=min` を使う。`mode=max` は中間
layer まで保存できるが、`~/.cache/ritsubi/vendure-buildcache`  10GB 超へ肥大化し、cache export
I/O  deploy 待ち時間を支配することがある。中間 layer まで local に保持したい調査時だけ
`BUILDX_LOCAL_FS_CACHE_MODE=max` を明示する。

CI  image build  local filesystem cache を使わず、2 層の remote cache を使う:

- `type=registry,ref=registry.fly.io/<app>:vendure-buildcache,mode=max`: durable  BuildKit cache 正本。
- `type=gha,scope=vendure-image,mode=min`: GitHub Actions runner 向けの warm cache。`BUILDX_GHA_CACHE=false`
  で無効化、`BUILDX_GHA_CACHE_MODE=max` で中間 layer 保存へ拡張できる。

R2 / GitHub artifact  BuildKit cache 本体には使わない。cache tarball  artifact/R2 に積むと upload/download
自体が deploy 待ち時間になり、BuildKit の差分 cache とも相性が悪い。代わりに CI  build log `.summary.json`  `vendure-image-build-<env>-<sha>` artifact として 14 日保持し、遅い step の分析に使う。

build stage 単位の遅さを調べるだけなら、registry  image  push しない計測入口を使う:

```bash
scripts/ops/measure-vendure-image-build.sh
scripts/ops/measure-vendure-image-build.sh --no-cache
```

出力は `tmp/build-bench/vendure-image-<timestamp>.json`  `.log`。実 deploy の待ち時間を短縮したい場合は、
先に CI /  terminal で同じ source SHA  image を作っておき、`manual-deploy-vendure` `image_ref` を渡して build phase を省く。
実 deploy  image build が走った場合も、BuildKit  plain log と遅い step summary `${TMPDIR:-/tmp}/ritsubi-vendure-image-build/<env>-<sha>-<timestamp>.log` / `.summary.json` に保存される。
tail だけで判断せず、summary  `slowestSteps`  `pnpm install``COPY``exporting``pushing`
のどれが支配的だったかを確認する。

**失敗時の挙動 (不変条件):**

- **drift audit fail**:  3 系統に `SIGTERM` を送って unwind。`registry.fly.io`  partial manifest
  が残らないよう `docker buildx`  in-flight blob を破棄する想定 (BuildKit の標準動作)
- **image build fail**: Dashboard  `wait` してから skew 警告を出して abort
  (Dashboard が先に Cloudflare push 済みなら backend  version skew が残るため、
   SHA で再 deploy する手順を log に出す)
- **production backup fail**: `flyctl deploy` に進ませず abort
  (release_command  migration  backup なしで走るのを止めるため)
- **Vendure candidate preflight fail**: `DEPLOY_IMAGE_REF`  `vendure-<sha>` tag   `target_ref` が一致しない、または local image build  Docker context  `target_ref` と一致しない場合は
  `flyctl deploy` 前に abort。provenance mismatch は切替後 rollback で直さない
- **current production health fail**: production では切替前に現行 release  machine / checks /
  public readiness / `/version` を検証し、すでに degraded なら abort。
  緊急 override  `PRE_DEPLOY_ALLOW_DEGRADED_CURRENT_PRODUCTION=true`
- **Vendure deploy safety gate fail**: `flyctl deploy` は完了したが、新 release  runtime health   揃わない状態。production の自動 rollback は既定 off。
  runtime health failure で自動 rollback を許可する場合のみ
  `PRODUCTION_AUTO_ROLLBACK_ON_POST_DEPLOY_HEALTH_FAILURE=true`
- **post-deploy verify fail**: production でも自動 rollback せず非 0 exit。
  Sentry marker fail  image  live のため WARN に留め、backfill コマンドを log に出す

#### Vendure deploy safety gate

`manual-deploy-vendure`  `flyctl deploy` 前に `flyctl releases --json --image` `flyctl machine list --json`  snapshot を取り、直近成功 release  image / build metadata /
machine summary / scale 表示を記録する。これは **切替前 health 検証と rollback 手動判断用の証跡**であり、
scale count を修正しない。

この gate の目的は、過去 incident のように **新 Machine が実際には ready ではないのに production
deploy を成功扱いにして運用者が気付けない状態**をなくすこと。Fly  `bluegreen`  traffic
swap そのものを担当するため、repo  wrapper だけで「一瞬も絶対に traffic が向かない」とは表現しない。
代わりに、次の fail-closed contract  deploy 経路の正本にする:

- deploy 前に `DEPLOY_IMAGE_REF`  `vendure-<sha>` tag、runtime `BUILD_*` metadata、
  local build context  target SHA と照合し、provenance mismatch を切替前に止める
- production では deploy 前に現行 release  Machine / process group / Fly checks /
  public `/health/ready` / public `/version` を検証し、すでに degraded  production へ追加の
  traffic switch を作らない
- `flyctl deploy --strategy bluegreen`  deploy-time health contract を使い、新 Machine  health
  pass 前に通常の traffic migration が起きない前提にする
- deploy 完了直後に repo  gate  **新 release  Machine / process group / Fly checks /
  public `/health/ready` / public `/version`** を二重検証する
- deploy  snapshot  started process group 数を期待値にする。scale count は修正しないが、
  例として production  `app=2 / worker=1` で動いているなら、新 release  `app:2,worker:1`
  を満たすまで成功扱いにしない
- production  gate または post-deploy verify が失敗した場合は、成功扱いにせず非 0 終了する。
  自動 rollback は追加の production switch を発生させるため既定 off とし、runtime health failure に限って
  `PRODUCTION_AUTO_ROLLBACK_ON_POST_DEPLOY_HEALTH_FAILURE=true`  opt-in する
- operator output では、切替前に止まったのか、切替後に rollback したのかを
  `rollback_attempted` / `rollback_result`  summary fields で明示する

`flyctl deploy` が成功した直後、Dashboard  Storefront など下流 surface を進める前に
`scripts/ops/vendure-deploy-safety.mjs gate` を実行する。gate の成功条件は次の通り:

- target image  started machine  deploy  snapshot と同じ process group capacity を満たす
  (snapshot 不在時の fallback  production: `app:2,worker:1`、staging: `app:1`)
- `flyctl checks list --json`  critical がなく、target image  started machine に紐付く check   warning / critical / unknown ではない
- public `/health/ready`  200 で、health payload  build commit  target SHA と一致する
- public `/version`  200 で、version payload  commit  target SHA と一致する

この gate  Fly  bluegreen internal check を信用しないという意味ではなく、Fly  deploy-time check と
Ritsubi  runtime contract (`/health/ready` / `/version` / process group) を別々に確認するための
二重検証である。Fly  `bg_deployment` check  deploy 時専用で、deploy 完了後の routing health を
永続的に表すものではない。

production  gate または `post-deploy-check.sh` が失敗した場合、wrapper は既定では旧 image へ
rollback deploy しない。rollback  DB migration を巻き戻さず、追加の bluegreen switch も発生させるため、
operator  snapshot  `previousImageRef`  `previousBuildEnv` を確認して手動判断する。
自動 rollback を許可する場合も、provenance mismatch ではなく runtime health failure に限定する。

手動確認コマンド:

```bash
node scripts/ops/vendure-deploy-safety.mjs snapshot \
  --fly-app ritsubi-ecommerce

node scripts/ops/vendure-deploy-safety.mjs gate \
  --fly-app ritsubi-ecommerce \
  --ready-url https://commerce.ritsubi-platform.com/health/ready \
  --version-url https://commerce.ritsubi-platform.com/version \
  --expected-sha <commit_sha> \
  --expected-image registry.fly.io/ritsubi-ecommerce:vendure-<commit_sha> \
  --expected-process-groups app:1,worker:1
```

**drift audit  `Cannot find module 'terser'` / `html-minifier-terser` で落ちる場合:**

これは DB drift ではなく、ローカルの pnpm virtual store / `node_modules` が壊れており
`@vendure/email-plugin`  `mjml`  `html-minifier-terser` の依存 symlink が欠けている状態。deploy
を bypass せず、次で依存関係を再構築してから `just manual-deploy-vendure` を再実行する。

```bash
CI=true pnpm install --force --prefer-offline --prod=false
pnpm exec node -e "require('./node_modules/.pnpm/html-minifier-terser@7.2.0/node_modules/html-minifier-terser'); console.log('html-minifier-terser ok')"
```

**従来直列に戻したい場合:**

- `DEPLOY_PARALLEL_DASHBOARD=false`  export すると Dashboard  fly deploy 後に直列実行
- `BACKUP_POSTGRES_PRODUCTION_SKIP=false` (既定)  `deploy-fly` 内の backup を再有効化

並列化の効果は Phase 4 plan に記載 (Dashboard ~60–90s、backup ~30–60s prod のみ、drift audit ~15s)### Depotビルダーとは

Fly.ioにはDepotビルダーが組み込まれており、`--depot`フラグを使用するだけで、追加のアカウント作成や設定なしでDockerビルドを高速化できます。ビルドキャッシュを活用することで、デプロイ時間を大幅に短縮できます。

### デプロイ時のDepot使用

#### 推奨デプロイ導線

```bash
# 通常運用: release_command まで含めて一括で流す
just deploy-fly production
just deploy-fly staging

# app deploy だけに分けるのは、復旧や検証で release_command を意図的に skip するときだけ
# skip-release は migration / structural repair を飛ばすため、明示確認が必要
just deploy-fly production auto false true image_ref=registry.fly.io/ritsubi-ecommerce:vendure-<source_sha> confirm_skip_release=true
just migrate-fly production

just deploy-fly staging auto false true image_ref=registry.fly.io/ritsubi-ecommerce:vendure-<source_sha> confirm_skip_release=true
just migrate-fly staging

# 既定の復旧導線
just recover-fly-promoted image_ref=registry.fly.io/ritsubi-ecommerce:vendure-<source_sha> env=production
just recover-fly-promoted image_ref=registry.fly.io/ritsubi-ecommerce:vendure-<source_sha> env=staging

# 直接 flyctl を使う場合も、リポジトリルートから --remote-only --depot を付ける
cd <repo-root>
flyctl deploy --config apps/vendure-server/fly.toml -a ritsubi-ecommerce --remote-only --depot

# またはデプロイスクリプトを使用(build input を検証し、正式な package deploy script へ委譲)
./apps/vendure-server/scripts/fly-deploy.sh production
```

`deploy-fly`  Fly  release command まで含めた通常導線です。Vendure `release_command`  migration + structural repair に限定しているため、通常
deploy では core relation / join table の補修までここで完了します。

`deploy-fly-app` 相当の導線は Fly  `--skip-release-command` を使い、config /
secrets / image だけ先に反映する復旧用の導線です。schema 変更がある場合は、
その後に `migrate-fly` を明示的に実行して release contract(migration +
structural repair)を流します。skip-release  migration / structural repair
漏れをそのまま traffic へ露出させるため、明示確認なしでは実行できないようにし、
通常の復旧は `recover-fly-promoted` を正本とします。policy seed  deploy 後の標準手順には含めず、
staging  drift 復旧で必要な場合だけ明示リペアとして実行します。

また、`staging` / `production`  manual deploy  **GitHub Actions  build して
`registry.fly.io`  push した image、または local で同じ規約で build/push した
image  `DEPLOY_IMAGE_REF` で指定する運用を正本**とします。
`apps/vendure-server/scripts/deploy-with-metadata.sh` は、この 2 環境で image 未指定の
deploy を拒否し、Fly remote build へフォールバックしません。

通常の昇格フローでは、`develop` への merge  staging deploy、`main` への merge
が production deploy の契機です。両方とも `flyctl deploy --image ...`
ベースで、Fly  build を正本にはしません。

promoted image を手元で特定する場合は、次を使います。

```bash
# 既知の source SHA から image ref を組み立てる
bash scripts/ops/resolve-vendure-image-ref.sh staging <source_sha>
bash scripts/ops/resolve-vendure-image-ref.sh production <source_sha>

# production 昇格時に、main 上の merge から元の source SHA と image ref を解決する
FLY_API_TOKEN=... GITHUB_REPOSITORY=<user_or_org>/ritsubi-local GITHUB_SHA=<main_commit_sha> \
  bash scripts/ops/resolve-vendure-promotion.sh production --env

# local / emergency で CI と同じ規約の image を build/push する
bash scripts/ops/build-vendure-image.sh staging <source_sha>
bash scripts/ops/build-vendure-image.sh production <source_sha>
```

手動復旧の最短導線は次です。

```bash
just recover-fly-promoted image_ref=registry.fly.io/ritsubi-ecommerce:vendure-<source_sha> env=staging
just recover-fly-promoted image_ref=registry.fly.io/ritsubi-ecommerce:vendure-<source_sha> env=production
```

GitHub Actions の標準 deploy workflow では、この後に drift audit `post-deploy-check.sh` による検証が続きます。drift を復旧したい場合は専用の
sync 導線を明示的に実行してください。drift 障害の切り分けや復旧は
[Vendure Drift 運用 Runbook](schema-drift-runbook.md) を参照してください。

Storefront 側の post-deploy verification  `Production Smoke Storefront` が担当し、
Sentry Feedback は関係者が `?sentry-feedback=1` を付けた manual 確認導線で扱います。
`Production Sentry Smoke Storefront`  manual  observability 切り分け導線です。`develop` から
`main` への昇格は明示的な手動操作 (`just promote-main` / `Promote Main`  workflow_dispatch) に
一本化しており、production  post-deploy verification は昇格後の `Production Smoke Storefront` が担います。

staging / production  deploy 経路では seed を実行しません。fixture / baseline / policy
seed  deploy target に接続せず、drift 復旧などで必要な場合だけ、事前に対象差分を確認して
明示的な repair として実行します。

staging  storefront visibility / payment policy を非破壊に復旧したい場合は、deploy
手順ではなく drift audit  manual 指示を入口にします。policy / commercial seed repair は
staging でも production でも使いません。deploy workflow / post-deploy summary からも案内しません。

production のデータ復旧は直前 backup を作成したうえで、対象を絞った audited repair で行います。
`drift:repair:policy` / `drift:repair:commercial` のような seed-based repair  shared env では使いません。

### ビルドキャッシュの確認

Depotビルダーを使用すると、ビルドログにキャッシュヒット情報が表示されます:

```text
=> CACHED [base 3/8] COPY package.json pnpm-lock.yaml...
=> CACHED [base 4/8] RUN pnpm fetch --frozen-lockfile
```

### Dockerfileの最適化

`apps/vendure-server/Dockerfile.fly`は、ビルドキャッシュを最大限活用するように最適化されています:

- **レイヤー分離**: workspace manifest (`package.json` / `pnpm-workspace.yaml` / `pnpm-lock.yaml`) を先にコピーし、ソースコード変更で依存解決 layer が無効化されないようにする
- **pnpm store  BuildKit cache mount**: `RUN --mount=type=cache,id=pnpm-store-vendure,target=/pnpm/store,sharing=locked pnpm install --frozen-lockfile`  store  builder 横断で再利用し、lockfile が変わらない限り fetch コストをゼロに近づける
- **`pnpm deploy` による flatten**: deploy stage  symlink を解消し、runtime image を最小化

詳細は`apps/vendure-server/Dockerfile.fly`のコメントを参照してください。

## Step 8: アプリケーションデプロイ

### 初回デプロイ

```bash
# プロジェクトルートで依存関係インストール
pnpm install

# Vendure サーバーをビルド
pnpm exec nx run ritsubi-vendure-server:build:production

# 初回デプロイ(ルートディレクトリから実行)
# Depotビルダーを使用して高速ビルド(追加設定不要)
flyctl deploy --config apps/vendure-server/fly.toml -a ritsubi-ecommerce --remote-only --depot

# デプロイ状況確認
flyctl status -a ritsubi-ecommerce
flyctl logs -a ritsubi-ecommerce
```

### データベースマイグレーション実行

```bash
# マイグレーション実行(Vendureは起動時に自動実行)
# 手動で実行する場合:
flyctl ssh console -a ritsubi-ecommerce
# コンテナ内で:
cd /app/apps/vendure-server
pnpm run migrate
```

staging / production  deploy・migration 後に `pnpm run db:seed` は実行しません。
データ復旧が必要な場合は drift audit の結果に基づき、backup 付きの対象限定 repair を使います。

## Step 9: ヘルスチェック・動作確認

### 基本動作確認

```bash
# アプリケーション URL 確認
flyctl info -a ritsubi-ecommerce

# ヘルスチェック(Tier 0 Safe Probe)
# liveness: プロセスが生きているか(依存なし)
curl https://ritsubi-ecommerce.fly.dev/health/live
# readiness: DB・Redis 接続が確立しているか(トラフィック受け入れ可否)
curl https://ritsubi-ecommerce.fly.dev/health/ready
# バージョン確認(デプロイ検証)
curl https://ritsubi-ecommerce.fly.dev/version

# GraphQL Playground(開発時のみ)
curl https://ritsubi-ecommerce.fly.dev/admin-api
```

### 管理画面アクセス

```bash
# 管理画面URL: https://ritsubi-ecommerce.fly.dev/admin
# 設定したSUPERADMIN_USERNAME/SUPERADMIN_PASSWORDでログイン
```

## Step 10: カスタムドメイン設定(Vendure API)

### ドメイン追加

```bash
# カスタムドメイン追加
flyctl certs add commerce.ritsubi-platform.com -a ritsubi-ecommerce

# DNS設定確認
flyctl certs show commerce.ritsubi-platform.com -a ritsubi-ecommerce
```

### DNS レコード設定

```bash
# Cloudflare プロキシ運用時は external proxy 設定を使用する。
# flyctl の案内に従い、AAAA のみを Cloudflare 側へ追加する。
#
# 例:
# Type: AAAA
# Name: commerce
# Value: <fly_ipv6_for_app>
# TTL: 300
```

## Step 11: SSL/TLS 証明書設定

### 自動SSL証明書

```bash
# Let's Encrypt 証明書自動発行(Fly.io 標準機能)
flyctl certs check commerce.ritsubi-platform.com -a ritsubi-ecommerce

# 証明書確認(Tier 0 Safe Probe)
curl -I https://commerce.ritsubi-platform.com/health/live
curl -I https://commerce.ritsubi-platform.com/health/ready
```

## Step 12: 監視・ログ設定

### ログ監視設定

```bash
# リアルタイムログ確認
flyctl logs -a ritsubi-ecommerce --follow

# 過去ログ確認
flyctl logs -a ritsubi-ecommerce --since 1h
```

### メトリクス確認

```bash
# アプリケーションメトリクス
flyctl metrics -a ritsubi-ecommerce

# PostgreSQL メトリクス
flyctl metrics -a ritsubi-postgres-db

# Redis メトリクス
# flyctl redis metrics ritsubi-ecommerce-redis
```

## Step 13: スケーリング設定

### 水平スケーリング

```bash
# インスタンス数変更
flyctl scale count 2 -a ritsubi-ecommerce

# リージョン追加(将来的)
flyctl regions add osa -a ritsubi-ecommerce  # 大阪リージョン
```

### 垂直スケーリング

```bash
# VM サイズ変更
flyctl scale vm performance-1x -a ritsubi-ecommerce

# メモリ設定変更
flyctl scale memory 2048 -a ritsubi-ecommerce
```

## Step 13: バックアップ設定

詳細な current status / restore / gap analysis の正本は
[`docs/05-delivery/maintenance/backup-and-restore.md`](../../05-delivery/maintenance/backup-and-restore.md)
を参照してください。

現行 production の要点だけを書くと、次のとおりです。

```bash
# Vendure PostgreSQL: GitHub Actions (hourly, 30d retention) -> Cloudflare R2
gh run list --workflow backup-postgres-prd.yml --limit 5
just backup-postgres-production  # 手動実行の入口(workflow と同じ script)

# WordPress MariaDB: production VPS timer (hourly, 30d retention) -> Cloudflare R2
just wp-backup-status-vps production
just wp-backup-run-vps production
```

> [!IMPORTANT]
> 現在の Fly Postgres  managed `flyctl postgres backups`
> 前提ではありません。復元は `pg_restore` + `just proxy-postgres <env>`
> を正本とします。

## Step 14: CI/CD パイプライン設定

### テストワークフロー(`.github/workflows/test.yml`)

ストーリーを含む UI テストまでを自動化するため、GitHub
Actions 上で以下の手順を実行します。

- **起動条件(重複回避)**: 詳細は README の「CI運用ルール(重複起動の回避)」を参照
  - PR  `pull_request` のみで実行(`main` / `develop` 宛てのみ)
  - `main` / `develop` への push のみ `push` で実行
  - docs のみ変更(`docs/**`, `**/*.md`, `**/*.mdx`)は CI をスキップ
  - `concurrency` はコミット SHA をキーにして同一コミットは 1 本に抑制
- **環境**: `ubuntu-latest` / Node.js 24.7.0 / pnpm 11.0.0
- **依存サービス**: PostgreSQL 17`pg_trgm` を利用)、Redis
  7(いずれもヘルスチェック付き)
- **テスト手順**:
  1. GraphQL codegen(PR  `nx affected` / push  `--all`  2. Lint(PR は変更ファイルのみ / push は全体)
  3. Typecheck(PR  `nx affected` / push  `--all`  4. Unit tests(PR  `nx affected` / push  `--all`  5. Vendure / Storefront  E2E・統合テストは PR のみ(変更検知時)
  6. Build Test  PR  Vendure 変更時のみ、push は全体

```yaml
# .github/workflows/test.yml(抜粋)
jobs:
  test-core:
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Resolve Nx base/head
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            echo "NX_BASE=origin/${{ github.base_ref }}" >> $GITHUB_ENV
            echo "NX_HEAD=${{ github.sha }}" >> $GITHUB_ENV
          else
            echo "NX_BASE=${{ github.event.before || 'HEAD~1' }}" >> $GITHUB_ENV
            echo "NX_HEAD=${{ github.sha }}" >> $GITHUB_ENV
          fi
      - name: Run typecheck (PR: affected / push: all)
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            pnpm exec nx affected --target=typecheck --base="$NX_BASE" --head="$NX_HEAD"
          else
            pnpm run typecheck
          fi
```

- MEMO: Dashboard React Cosmos 上での GraphQL 通信は本番 API と切り離し、MSW ハンドラ +
  `GraphQLMockResponse` 形式のモックデータで擬似レスポンスを返す構成です。
  これにより UI 検証時も安定したデータセットを再現できます。

### デプロイワークフローにおける事前テスト

- `CI (push develop)`
  - `develop` 反映後の統合ゲートとして `nx affected` ベースの build / typecheck を実行
  - test / coverage  merge 前の `CI (PR -> develop)` で完了させ、post-merge では
    deploy-readiness に直結する compile 系チェックへ絞る
  - Storefront  Cloudflare build / Wrangler types check もここで確認
  - knip  CI から除外し、必要に応じて開発者がローカルまたは手動ジョブで実行
- `deploy-staging.yml`
  - `develop` への push を契機に、affected  staging  Vendure / Storefront /
    Dashboard  deploy
- `Staging Smoke Storefront`
  - staging deploy 後に health / read-only / critical auth smoke     外部決済契約に依存しない軽量 staging checkout
    (`checkout.staging-gate.real.spec.ts`) を実行
  - `@smoke:prod-safe` 全体は local/manual smoke に残し、CI  staging gate では
    time budget を守るため read-only probes  `@smoke:critical` に絞る
  - SBPS live checkout  #556 の vendor-side 残件があるため deploy gate にしない。
    `checkout.sbps.real.spec.ts`  staging/local guard  production URL への誤実行を
    fail-closed で止め、手動 opt-in の再確認に限定する
  - smoke 成功後に別 workflow  `Prepare Production Storefront Version`  production     Storefront Worker version  build し、commit SHA タグで事前 upload する
    (production deploy  build せずこの version  `--deploy-version-id` 配信する)
- `Promote Main`
  - 手動入口 (`just promote-main` / workflow_dispatch)。staging smoke が通った後に
    `develop  main` 昇格 PR を作成・更新する。GitHub  workflow_run 3 段制限により
    自動連鎖は成立しないため、明示的な手動操作へ一本化している
- `deploy-production.yml`
  - 本番リリース前に `pnpm run test` + `pnpm run typecheck` +
    `pnpm run lint:check`
  - Node.js 24 で実行(`mise.toml` のバージョンに合わせた設定)
- `Production Smoke Storefront`
  - `Deploy Production` 成功後と定期実行で、production     shadow-readonly probe + synthetic smoke を継続確認
  - Sentry Feedback は関係者が `?sentry-feedback=1` 付きで manual 確認
- `Production Sentry Smoke Storefront`
  - 手動実行で observability 導線を個別に切り分ける

### 補足: 手動での依存関係分析

CI から knip を外したため、依存関係分析は以下のコマンドで任意実行します。

```bash
just knip            # Knip v6 の全体分析を確認
just knip-production # 本番ビルド用エントリで最適化確認
just knip-gate       # 手動 Knip gate(unlisted / unresolved)
```

### GitHub Secrets 設定

```bash
# GitHub リポジトリの Settings > Secrets and variables > Actions で設定:
# FLY_API_TOKEN: Fly.io API トークン

# Fly.io API トークン生成
flyctl auth token
```

## リリースタグ運用(release-please / Changesets)

### 概要

- ルートの集約リリースは `release-please`
  が担当し、`main` への push で実行される(`.github/workflows/release.yml`)。
- Storefront / Vendure Server  app version / CHANGELOG  `Changesets`
  が担当する(`.github/workflows/changesets.yml`)。
- **SSOT は各 package  `package.json`  `version`**。ただし更新責務は分離し、
  root  `release-please`、app  `Changesets` が更新する。

### タグ形式

- ルート(全体): `vX.Y.Z`
- Storefront: `storefront-vX.Y.Z`
- Vendure Server: `vendure-server-vX.Y.Z`

### 運用フロー

1. Storefront / Vendure Server を変更する PR では `pnpm changeset`  changeset
   を追加する
2. `main` への反映後、Changesets workflow  app version PR を作成する
3. version PR  merge されると app  `package.json` / `CHANGELOG.md` が更新される
4. 同じ `main` push  `storefront-vX.Y.Z` / `vendure-server-vX.Y.Z`  tag  GitHub
   Release が自動作成される
5. ルート全体の `vX.Y.Z` は従来どおり release-please  release PR / tag で管理する

### 実行結果の確認(CI)

#### GitHub Actions 画面

- ワークフロー:
  - `Release Please``.github/workflows/release.yml`): root release
  - `Changesets``.github/workflows/changesets.yml`): app version PR / package tag
- `main` への push 後に実行される
- 失敗時は該当 workflow のステップログを確認する

#### gh CLI

```bash
# 直近の root release 実行一覧
gh run list --workflow release.yml

# 直近の app package release 実行一覧
gh run list --workflow changesets.yml

# 失敗した実行の詳細
gh run view <run-id>

# ログのダウンロード(必要に応じて)
gh run download <run-id>
```

### 既存タグの整合チェック

```bash
# 既存タグの一覧
git tag --list

# リモートタグも含めて確認
git fetch --tags --prune
git tag --list
```

```bash
# リモートタグの一覧(GitHub)
gh release list
```

### 手動作業(原則非推奨)

```bash
# annotated tag の例(緊急対応時のみ)
git tag -a storefront-v0.1.0 -m "Release storefront v0.1.0"
git push origin storefront-v0.1.0
```

## Step 15: ステージング環境設定

### ステージング環境デプロイ

```bash
# ステージング用アプリ作成(既に存在する場合はスキップ)
flyctl apps create ritsubi-ecommerce-staging

> **重要: メモリ要件**
> Vendure サーバーの安定動作には最低 **1024MB (1GB)** のメモリが必要です。512MB 以下ではガベージコレクションによる遅延やヘルスチェック失敗が発生する可能性が高いため、必ず 1GB 以上を割り当ててください。

# ステージング用データベース(Dockerコンテナ)
cd <repo-root>
flyctl apps create ritsubi-postgres-db-staging --org ritsubi
flyctl volumes create postgres_data_staging --region nrt --size 5 -a ritsubi-postgres-db-staging
flyctl secrets set POSTGRES_PASSWORD="$(openssl rand -base64 32)" -a ritsubi-postgres-db-staging
flyctl deploy --config apps/postgres/fly.staging.toml -a ritsubi-postgres-db-staging

# ステージング用 Redis(セッション用途)

# ステージング環境デプロイ
cd <repo-root>
flyctl deploy --config apps/vendure-server/fly.staging.toml -a ritsubi-ecommerce-staging --remote-only --depot
```

## 運用・メンテナンス

### 定期メンテナンス

```bash
# アプリケーション再起動
flyctl machine restart -a ritsubi-ecommerce

# production config 既定の bluegreen 戦略で再デプロイ
cd <repo-root>
flyctl deploy --config apps/vendure-server/fly.toml -a ritsubi-ecommerce --remote-only --depot

# データベースメンテナンス
flyctl ssh console -a ritsubi-postgres-db
psql -U postgres ritsubi_vendure -c "VACUUM ANALYZE;"
```

### ログローテーション

```bash
# Fly.io ログは自動ローテーション
# 長期保存が必要な場合は外部ログサービス使用
# 例: Datadog, CloudWatch Logs, etc.
```

### セキュリティアップデート

```bash
# ベースイメージ更新
cd <repo-root>
flyctl deploy --config apps/vendure-server/fly.toml -a ritsubi-ecommerce --remote-only --depot

# 依存関係更新
pnpm update
pnpm audit fix
```

## トラブルシューティング

### よくある問題と解決方法

#### 1. アプリケーション起動失敗

```bash
# ログ確認
flyctl logs -a ritsubi-ecommerce

# SSH でコンテナ調査
flyctl ssh console -a ritsubi-ecommerce

# 環境変数確認
env | grep DATABASE_URL
```

#### 2. データベース接続エラー

```bash
# データベース接続テスト
flyctl ssh console -a ritsubi-postgres-db
psql -U postgres ritsubi_vendure

# 接続URL確認
flyctl secrets list -a ritsubi-ecommerce
```

#### 3. Redis接続エラー

```bash
# Redis状態確認(使用している場合)
# flyctl redis status ritsubi-ecommerce-redis

# Redis接続テスト(使用している場合)
# redis-cli -h hostname -p 6379 -a password ping
```

#### 4. デプロイ失敗

```bash
# 前のバージョンにロールバック
flyctl releases list -a ritsubi-ecommerce
flyctl rollback -a ritsubi-ecommerce

# ヘルスチェック確認(ロールバック後の疎通確認)
curl https://ritsubi-ecommerce.fly.dev/health/ready
curl https://ritsubi-ecommerce.fly.dev/version
```

#### 5. CI image build pipeline 失敗時の切り分け (#867)

`workflow_dispatch`  Deploy Staging を起動して `Resolve Vendure Image Artifact` job が以下のように falling-through  fail する場合:

- `Resolve promoted Vendure image` step  `ERROR: ... not found`  `continue-on-error` (期待挙動)
- `Build staging Vendure image on demand` step は緑だが `image_ref` 出力が空
- `Select Vendure image artifact` step  `Unable to resolve a Vendure image artifact ...`  exit 1

これは `scripts/ops/build-vendure-image.sh`  silent  exit 0 を返す経路を踏んでいる症状で、現在は次の防御層で fail-loud に倒す:

1. `build-vendure-image.sh`  `cleanup()` trap: `image_ref`  fd3  emit せずに exit 0 した場合、exit 70 + diag ログ (`env_name` / `source_ref` / `image_ref` / `build_if_exists` / `buildx_builder` / `buildx_driver` / `CI`)  stderr に出す
2. `.github/actions/build-vendure-image/action.yml`:  `image_ref`  command substitution  `::error::`  fail に倒す
3. `.github/workflows/_deploy-environment-stack.yml`  `Select Vendure image artifact`:  step  `outcome`  error log に出して、どの経路が空文字を返したか明示する

切り分け手順:

```bash
# 1. 該当 run の Resolve Vendure Image Artifact job の logs を全行確認
gh run view <run-id> --log | rg 'build-vendure-image|Resolve Vendure|Select Vendure'

# 2. local で同じ env を再現 (FLY_API_TOKEN は staging vendure secret を with-env で投入)
CI=true BUILD_VENDURE_IMAGE_DEBUG=1 \
  bash scripts/ops/build-vendure-image.sh staging $(git rev-parse HEAD) 2>&1 | tail -120

# 3. 上記の出力が exit 70 + cleanup() guard diag を出した場合は、build_if_exists / buildx_builder /
#    buildx_driver のうち想定外の値を持つものを確認する
# 4. exit 65 (resolve 空) / exit 66 (buildx ls に '*' builder 無し) / exit 67 (emit 直前 empty) は
#    それぞれ対応する原因を明示する
```

#### 6. 「本番だけ修正が反映されない」= image fallback (`/version` が実コードを偽る)

**症状**: develop→main 昇格まで成功し `commerce.ritsubi-platform.com/version` も新 commit を返すのに、**本番だけ**で修正前の挙動が残る(例: 2026-06-02  browse pagination 修正 `86cc4e6`。staging では直っているのに本番で「9 ページあるのに 3 ページ目以降へ行けない」が残存)。

**根本原因**: 本番 Vendure image  **`registry.fly.io/ritsubi-ecommerce:vendure-<source_sha>`  prod registry から解決**する。該当 SHA  image  prod registry に無いと、`resolve-vendure-image-ref.sh`  本番 deploy  **git 祖先 commit  image  silent fallback** する(`Promotion artifact was not found for source SHA ...; scanning git ancestors ... Found image at ancestor SHA <older>`)。さらに **`/version`  deploy 時に `BUILD_COMMIT_SHA`  env 注入しているだけ**なので、実際に動いている image とは無関係に新 SHA を表示する=**`/version` を信じてはいけない**。

典型的な誘発手順: staging  `manual-deploy-staging <env> true false false`(**vendure のみ**)で deploy  staging registry には新 commit image が出来るが **prod registry には出来ない**  昇格が source_ref  develop HEAD を捕捉  本番 deploy  prod registry で見つからず祖先へ fallback。staging image registry (`ritsubi-ecommerce-staging`)  prod image registry (`ritsubi-ecommerce`) は**別**である点に注意。

**診断(`/version` ではなく実 image を見る)**:

```bash
source apps/vendure-server/scripts/prepare-fly-cli-env.sh
# 本番マシンが実際に動かしている image tag (= 実コードの真実)
flyctl machines list -a ritsubi-ecommerce --json | jq -r '.[].config.image' | sort -u
# 期待した SHA の image が prod registry に存在するか
flyctl auth docker
docker buildx imagetools inspect registry.fly.io/ritsubi-ecommerce:vendure-<expected_sha> >/dev/null 2>&1 && echo EXISTS || echo MISSING
# fallback したかは deploy run の Resolve Vendure Image Artifact job ログで確認
gh run view <run-id> --log | rg -i 'not found|scanning git ancestors|Found image at ancestor'
```

**復旧**: 修正を含む runtime と同一の staging image  **prod registry へ期待 SHA で複製**してから本番 deploy を再実行する。runtime が同一の隣接 commit(例: deploy script のみ差分)なら image を流用してよい。

```bash
flyctl auth docker
# staging の修正入り image を prod registry の期待 tag へ複製
docker buildx imagetools create \
  --tag registry.fly.io/ritsubi-ecommerce:vendure-<expected_sha> \
  registry.fly.io/ritsubi-ecommerce-staging:vendure-<fix_sha>
# prod registry に出来たことを確認後、本番 deploy を再実行(fallback せず期待 image を解決する)
just manual-deploy-production main
# 再確認: machines image が <expected_sha> に揃ったか(/version ではなく image を見る)
flyctl machines list -a ritsubi-ecommerce --json | jq -r '.[].config.image' | sort -u
```

**恒久対策**: 昇格対象 commit  **prod-registry image を昇格前に必ずビルド**する(staging deploy  storefront/dashboard 込みで通すか、build pipeline  prod registry  push)。fallback  fail-loud にするか、`/version` に「実 image tag」も併記して乖離を検知できるようにするのが望ましい。

### 緊急時対応

```bash
# アプリケーション緊急停止
flyctl machine stop -a ritsubi-ecommerce

# 緊急スケールダウン
flyctl scale count 0 -a ritsubi-ecommerce

# 緊急時バックアップ復元
# 正本 runbook を参照:
# docs/05-delivery/maintenance/backup-and-restore.md
# 現行の Vendure dump は custom format (*.dump) のため、psql ではなく
# pg_restore を使う。
```

## React Dashboard を Cloudflare Workers で配信する手順

Vendure 本体は Fly.io 上で稼働しつつ、ダッシュボード静的アセットを Cloudflare
Workers  static assets 機能から配信する構成。API は直接 Fly.io オリジンを呼ぶ。

1. **ビルド**
   deploy 前に monorepo 全体をビルドし過ぎないよう vendure-server のみ対象にする。

   ```bash
   corepack enable pnpm &&
   pnpm install --frozen-lockfile --filter ritsubi-vendure-server... &&
   pnpm exec nx run ritsubi-vendure-server:dashboard:build

   ```

   出力先: `apps/vendure-server/dist/dashboard/`
   SPA ルーティングは `apps/vendure-server/wrangler.jsonc`    `assets.not_found_handling = "single-page-application"` を正本にする。

2. **Cloudflare Workers 設定**
   - Worker name: production = `vendure-dashboard`、staging = `vendure-dashboard-staging`
   - Wrangler config: `apps/vendure-server/wrangler.jsonc`
   - Static assets directory: `apps/vendure-server/dist/dashboard`
   - staging / production  `cloudflare-dashboard-deploy.mjs` で別 Worker 名の
     config を生成して deploy する
   - カスタムドメインを使う場合は `dashboard.ritsubi-platform.com` /
     `dashboard-staging.ritsubi-platform.com`  Worker へ向ける

3. **API 接続方式(方式1: 直接 Fly.io オリジンを呼ぶ)**
   - build 時環境変数として
     `VITE_ADMIN_API_URL=https://ritsubi-ecommerce.fly.dev/admin-api`
     を注入する(必要なら `VITE_SHOP_API_URL` も)。
   - Worker runtime  API URL を持たせず、build artifact に閉じ込める。
   - Fly.io 側のシークレット/環境変数として `CORS_ORIGIN`
      Dashboard ドメインを追記して許可。

4. **キャッシュ方針**
   - HTML: no-cache を基本とする
   - `/assets/*`: Cloudflare CDN でキャッシュ許可。必要に応じて Cache
     Rules で適用。

5. **デプロイ動作確認**
   - `https://<dashboard-domain>/` でトップが表示されること

- `/report-templates/:id`
  などダイレクトアクセスが 200 になること(Workers Assets  SPA フォールバック)

local の手動 deploy  Secrets Manager  Vendure 環境変数も同時に注入して実行する。
`dashboard:deploy:*` target  build 時に production/staging  origin validator を通るため、
raw `pnpm exec nx run ...` では `CORS_ORIGIN` などが不足して abort する。

```sh
VENDURE_BASE_URL=https://commerce-staging.ritsubi-platform.com \
SECRETS_CONFIG=staging_vendure \
  ./scripts/ops/with-env.sh -- ./scripts/ops/nx.sh run ritsubi-vendure-server:dashboard:deploy:staging

VENDURE_BASE_URL=https://commerce.ritsubi-platform.com \
SECRETS_CONFIG=production_vendure \
  ./scripts/ops/with-env.sh -- ./scripts/ops/nx.sh run ritsubi-vendure-server:dashboard:deploy:production
```

deploy target の内部では `scripts/ops/with-cloudflare-auth.sh` 経由で AWS Secrets Manager 上の
`CLOUDFLARE_API_TOKEN` を注入し、Cloudflare Workers へ配備する。
CI  deploy workflow も同じく AWS Secrets Manager を正本とし、OIDC で取得した
`shared + vendure`  secret を使って Workers へ配備する。GitHub secret `CLOUDFLARE_API_TOKEN` を別正本として持たない。

- `https://<dashboard-domain>/admin-api` に到達し、CORS エラーが出ないこと

1. **環境変数の分離**
   - Dashboard 用のビルド時環境変数は
     `apps/vendure-server/src/dashboard/.env.example`
     を参考にしつつ、環境変数として設定し、Vite が参照する。
   - Vendure サーバーランタイム用の機密設定とは混在させない(機微情報をフロントに漏らさないため)。
   - Vite が読むのは `VITE_`
     プレフィックスのみ。GitHub Actions から build 時に明示注入する。

## コスト最適化

### リソース監視

```bash
# 使用量確認
flyctl billing show

# アプリごとの使用量
flyctl apps list --billing
```

### 自動スケーリング設定

```yaml
# fly.toml の最適化設定
[http_service]
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 1  # 深夜時間帯は最小1台
```

---

**文書バージョン**: 1.0 **作成日**: 2025年9月17日
**メンテナンス**: 月次で内容見直し