SwingCompassAI · 本番 · 2026-10-16〜17

SwingCompass 本番修正記録

本番デプロイの残作業中に見つかった不具合と、その修正の記録です。コードの修正は 3 件、API イメージの版の固定が 2 件、決定事項の記録は 2 件、残りは設定の変更です。各項目に、症状・原因・修正・確認の結果を載せています。

本番の構成と、直した場所

flowchart LR
  U["利用者のブラウザ
swing.aicompass-golf.com"] --> FE["Vercel
frontend (Next.js 15)"] AD["管理者"] --> AF["Vercel
admin-frontend"] FE -- "CORS ③" --> API["Fly: swingcompass-api-prod
FastAPI"] AF --> API API -- "② 起動時にメール設定を検査" --> RS["Resend
noreply@aicompass-golf.com"] API -- "① パスワード" --> R[("Fly: swingcompass-redis-prod")] WK["Fly: swingcompass-worker-prod
Celery"] -- "①" --> R API --> DB[("Neon Postgres")] WK --> DB API --> S3[("Cloudflare R2")] WK --> S3 DNS["Cloudflare DNS
CNAME swing ③"] -.-> FE

① Redis の起動コマンド(6861a62) ② メール設定ガード(PR #11) ③ 設定のみの変更。ログイン停止(b9a2c57)は frontend の中だけの修正です。

1
6861a62infra/fly/redis/fly.tomlmain・本番反映・確認済み

Redis のパスワードが展開されていなかった

症状

Redis を使う処理が、すべて AuthenticationError で 500 になっていました。パスキーの challenge 保存(backend/app/routers/passkey.py:165)と、Celery への解析投入です。

原因

Fly の [processes] のコマンドはシェルを通らずに実行されるため、${REDIS_PASSWORD} が展開されません。この文字列そのものが Redis のパスワードになっていました。API と worker の REDIS_URL には本物のパスワードが入っているので、認証が合いませんでした。

  • 実測: 文字列 ${REDIS_PASSWORD} で redis-cli -a すると PONG、secret の値では WRONGPASS
修正
infra/fly/redis/fly.toml
@@ -10,11 +10,14 @@ # 起動コマンド+# Fly は process コマンドをシェルを通さず実行し ${VAR} を展開しない。+# 直書きすると "${REDIS_PASSWORD}" という文字列がパスワードになる (2026-10-16 本番で発生)。+# 必ず sh -c で包んで環境変数を展開させる。 [processes]-  app = "redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes --dir /data"+  app = "sh -c 'exec redis-server --requirepass \"$REDIS_PASSWORD\" --appendonly yes --dir /data'"
確認
  • Redis を再デプロイした後、API から ping → True
  • 文字列 ${REDIS_PASSWORD} での認証 → 失敗
  • パスキーのログイン開始(options)→ 200
2
b9a2c57frontend/src/app/auth/login/page.tsxmain・本番反映・確認済み

ログイン後に「LOGGING IN...」で止まる

症状

swing.aicompass-golf.com を初めて開いてログインすると、ボタンが「LOGGING IN...」のまま止まりました。ログインの API 自体は成功していて(トークン保存済み・/auth/me 200)、再読み込みするとダッシュボードが開きました。

原因

ログイン画面が表示されたときに /dashboard を router.prefetch していました。その時点では sca_token cookie が無いので、middleware(frontend/src/middleware.ts:12-16)が先読みを /auth/login?redirect=/dashboard にリダイレクトし、Next.js(15.1.9)がその結果をキャッシュします。ログイン後の router.push('/dashboard') はキャッシュを使って通信せず、ログイン画面に留まります。pending は成功時に戻さない作りなので、ボタンの表示がそのまま残りました。

sequenceDiagram
  participant B as ブラウザ(ログイン画面)
  participant RC as Router のキャッシュ
  participant MW as middleware
  participant API as API
  B->>MW: 表示時に prefetch /dashboard(cookie なし)
  MW-->>RC: /auth/login?redirect=/dashboard へリダイレクト
  Note over RC: この結果を /dashboard 用に保持
  B->>API: POST /auth/login
  API-->>B: 200(トークン)→ cookie sca_token を保存
  B->>RC: router.push('/dashboard')
  RC-->>B: キャッシュを返す → URL は /auth/login のまま
  • 本番で再現: cookie なしでログイン画面を開くと、/dashboard への通信は先読みの 1 回(104 ms)だけでした。cookie を戻して push しても 5 秒後にログイン画面のままで、router.refresh() の後に push すると /dashboard に移りました
  • next dev は prefetch しないので、ローカルでは起きません
修正
frontend/src/app/auth/login/page.tsx
@@ -56,12 +56,6 @@     return raw.startsWith("/") && !raw.startsWith("//") ? raw : "/dashboard";   }, [searchParams]); -  // 遷移先を事前コンパイル/プリフェッチしておき、ログイン直後のちらつきを防ぐ-  useEffect(() => {-    router.prefetch(redirectTo);-    router.prefetch("/dashboard");-  }, [router, redirectTo]);-
frontend/src/__tests__/app/auth/login.test.tsx(回帰テスト・抜粋)
// src/middleware.ts の PROTECTED_PATHS と同じconst PROTECTED_PATHS = ["/dashboard", "/sessions", "/admin", "/my-coach"]; it.each([  ["redirect 指定なし", ""],  ["redirect=/sessions/123", "redirect=/sessions/123"],])("ログイン前に保護ページを prefetch しない (%s)", (_label, search) => {  mockSearch = search;  // render は act で包まれるので、戻った時点でマウント時の effect は実行済み  render(<LoginPage />);  expect(prefetchedProtectedPaths()).toEqual([]);});
確認
  • 回帰テスト: 修正前のコードで 2 件とも失敗(受け取った値 ["/sessions/123", "/dashboard"])→ 修正後に通過。frontend の jest は 67 件すべて通過
  • Codex レビュー: 指摘 0 件
  • 本番(Vercel の production、commit b9a2c57): cookie なしでログイン画面を開いて 3 秒間、/dashboard への通信は 0 件。cookie を戻して push すると、5 秒後に /dashboard
  • 利用者として、ログアウト → パスキーでログインしてダッシュボードまで移れた

変わる点: ログイン後の /dashboard の読み込みは、ボタンを押した後に始まります。その間は「LOGGING IN...」の表示が続きます。

3
PR #11bcfae3abackendmain・本番反映・確認済み

本番でメールが送れない設定なら API を起動しない

症状

10/16、本番で新規登録しても確認メールが届きませんでした。本番 API に EMAIL_PROVIDER が設定されておらず、既定値の console(backend/app/config.py:242)のままでした。console はメールを送らず、本文をログに書くだけなので、確認トークン入りの本文が本番のログに出ていました。

当日の対処は Fly secrets の設定だけです(EMAIL_PROVIDER=resend ほか)。この PR は、同じ設定ミスが再び起きたときに止めるためのものです。

置き場所

本番 worker は ENVIRONMENT=production ですが、EMAIL_PROVIDER を持っていません(未設定なので console 扱い)。共通の Settings の validator に入れると worker が起動しなくなるので、API の lifespan にだけ置きました。worker はメールを送らず、app.main も import しません(scripts/worker_entry.py → app.celery_app)。

修正
backend/app/services/email_service.py
+def assert_email_configured_for_production() -> None:+    """本番で送信できない EMAIL_PROVIDER 設定なら API を起動させない。"""+    if settings.ENVIRONMENT != "production":+        return+    # _send_email と同じ判定にそろえる (ここだけ緩めると起動後に送信で落ちる)+    provider = settings.EMAIL_PROVIDER.lower()+    if provider != "resend":+        raise RuntimeError(+            f"本番では EMAIL_PROVIDER=resend が必要です (現在: '{settings.EMAIL_PROVIDER}')。"+            " console のままだとメールは送られず、本文がログに出ます"+        )+    if not settings.RESEND_API_KEY.strip():+        raise RuntimeError("本番では RESEND_API_KEY の設定が必要です (EMAIL_PROVIDER=resend)")
backend/app/main.py
 async def lifespan(app: FastAPI):-    # 起動時: DBテーブルを自動作成+    # 起動時: 本番でメールが送れない設定なら、DB に触る前に止める+    assert_email_configured_for_production()+    # DBテーブルを自動作成     await init_db()
backend/requirements.txt(本番イメージに入っている版に固定)
-google-genai>=1.0.0+google-genai==1.55.0-rtmlib>=0.0.13+rtmlib==0.0.15

保証するのは「resend が選ばれ、キーが入っている」ことまでです。キーの有効性と送信元ドメインの検証状態は見ません。送信に失敗しても send_* がログ(Failed to send email)に書くだけで、呼び出し元には返りません。

テスト

backend/tests/test_email_guard.py: ガード単体 7 件と、fastapi_app.router.lifespan_context を通すテスト 2 件。既存の client fixture は ASGITransport なので lifespan を実行しません。lifespan からの呼び出しを消すと、このテストが 1 件失敗することを確かめました。

同じ条件(docker run)で全テスト失敗成功skip
main123013
このブランチ123103

失敗している 12 件は main と同じテストです。test_passkey 4 件、test_sso 3 件、test_phase2〜4 の DB 保存 4 件、test_storage_service の署名付き URL 1 件。

レビュー
  • 実装前の仕様を Codex がレビュー: P1 2 件・P2 8 件。8 件を取り込み、2 件は見送り(ENVIRONMENT の大文字小文字の揺れ: 既存の JWT ガードも完全一致で判定し、値は infra/fly/api/fly.toml に固定。alembic stamp head がガードより先に走る: 今も起動のたびに走っている既存の動き)
  • 実装後のブランチを Codex がレビュー: 指摘 0 件
デプロイ

2026-10-17、版を本番とそろえたイメージ deployment-01M540H8PK2EFWK9K6FHGCK3DD(次の項目)を --strategy bluegreen で入れました。API のマシンは 1 台です。

  • /health → 200 {"status":"ok",...}。Fly の health check は passing
  • 起動ログ: Application startup complete.。RuntimeError なし
  • パスワード再設定メール: 受信を確認。ログに Email sent to … (subject: 【SwingCompassAI】パスワード再設定のご案内)。Failed to send は 0 件
  • 戻すときは前のイメージ deployment-01M1PF7ADZ5C5YY8MZ6AQR72V3 を指定する。このイメージにはガードが無いので、戻す前にメールの設定を確かめる(DEPLOY.md §8)
4
PR #1347045e6PR #1403e8bafbackend/Dockerfilemain・本番反映・確認済み

API イメージの版を本番に固定する

症状

PR #11 を入れるためにイメージを作り直すと、稼働中のイメージ(2026-09-04 ビルド)と比べて Python 3.11.16 → 3.11.17、pip の部品 28 件、OS の部品 31 件の版が変わりました。python:3.11-slim と、requirements.txt の版指定 36 件だけでは固定されません。python:3.11.16-slim も同じ名前のまま作り直されていました(2026-09-19)。

モデル(backend/models/、git 管理外)は、ビルドした Mac に 12 件ありませんでした(motionagformer_b_h36m.onnx ほか。backend/app/config.py:123 の既定値が参照するファイル)。

修正
backend/Dockerfile
-FROM python:3.11-slim AS deps+FROM python:3.11.16-slim@sha256:9534e5a8e315485d4061ed659af0fd78a284c015f9b73661b41d6bab25604534 AS deps+ARG DEBIAN_SNAPSHOT=20260904T150500Z-RUN apt-get update && apt-get install -y \+RUN sed -i (apt の取得先を snapshot.debian.org/archive/…/${DEBIAN_SNAPSHOT} に置き換え) \+    && test "$(grep -c '^URIs: http://snapshot.debian.org/archive/' …)" = 2 \+    && ! grep -q '^URIs: http://deb.debian.org/' … \+    && apt-get -o Acquire::Check-Valid-Until=false update && apt-get install -y \-COPY requirements.txt .+COPY requirements.txt constraints.txt ./-    && pip install --no-cache-dir -r requirements.docker.txt+    && pip install --no-cache-dir -r requirements.docker.txt -c constraints.txt
  • digest は docker-library/repo-info の 2026-09-02 時点の記録です。amd64(sha256:d1053354…)の 4 層が、稼働中イメージの先頭 4 層と一致しました
  • DEBIAN_SNAPSHOT は、稼働中イメージの履歴で apt-get update が走った時刻です。置き換えが当たらず deb.debian.org が残った場合はビルドを止めます
  • backend/constraints.txt(新規): 稼働中イメージの pip freeze 116 件
  • backend/.dockerignore: !constraints.txt(既存の *.txt 除外に当たるため)、models/*.pt、.claude/
  • モデル 12 件は稼働中イメージから取り出して backend/models/ に置きました(md5 17/17 一致)
比較
項目稼働中 01M1PF7A… → 投入 01M540H8…
Python3.11.16 / 3.11.16
pip の部品116 件すべて同じ版
OS の部品(dpkg)374 件すべて同じ版
.py(app/ alembic/、改行そろえ)違いは app/main.py・app/services/email_service.py の 2 件(PR #11)
models/17 件、名前とサイズが同じ
/app にだけある稼働中: scripts/ 55 件・app/ の .prev 14 件 / 投入: constraints.txt・tests/test_email_guard.py・benchmarks/coach_rating_ground_truth.todo.json

投入前に手元(amd64 エミュレーション)で import app.main, cv2, onnxruntime, mediapipe, rtmlib が通ることを確認しました。

レビュー
  • PR #13 を Codex がレビュー: P1 1 件「mediapipe 経由の jax==0.7.1 が numpy>=2.0 を要求し解決できない」。稼働中イメージの jax / jaxlib のメタデータは numpy>=1.26、pip check は No broken requirements found.、ビルドも成功したので対応していません
  • PR #14 を Codex がレビュー: 指摘 0 件
影響
  • OS の部品は、DEBIAN_SNAPSHOT を書き換えるまで 2026-09-04 の版のままです。Debian の更新は自動では入りません(稼働中のイメージも同じ状態でした)。版の上げ方は DEPLOY.md §8
  • ビルドのたびに snapshot.debian.org を使います
  • docker compose の backend / worker も、次に作り直すと同じ固定になります。arm64 での作成は確かめていません
  • 本番の worker は 2026-09-04 のイメージのままです(infra/fly/worker/fly.toml:9)
5
コードの変更なし本番反映済み

設定だけの変更

目的場所変更
CORSFly swingcompass-api-prod secretsALLOWED_ORIGINS に swingcompass.vercel.app・swingcompass-admin.vercel.app・swing.aicompass-golf.com
メール送信同上EMAIL_PROVIDER=resend、RESEND_API_KEY(送信専用・ドメイン限定)、EMAIL_FROM_ADDRESS=noreply@aicompass-golf.com、EMAIL_FROM_NAME=SwingCompassAI
メール内リンク同上FRONTEND_URL=https://swing.aicompass-golf.com、ADMIN_FRONTEND_URL=https://swingcompass-admin.vercel.app
パスキー同上WEBAUTHN_RP_ID=aicompass-golf.com、WEBAUTHN_ALLOWED_ORIGINS=https://swing.aicompass-golf.com(既定値は localhost・backend/app/config.py:228)
独自ドメインVercel swingcompassswing.aicompass-golf.com を追加(Let's Encrypt、期限 2027-01-14)
独自ドメインCloudflare DNS(aicompass-golf.com)swing の CNAME を Vercel へ(proxy なし)

決定事項は DEPLOY.md に記録しました: メール送信ドメイン(876f334・§4)、パスキーの親ドメイン(571c42a・§9)。パスキーは独自ドメインで開いたときだけ使え、*.vercel.app では RP ID が一致しないので使えません。

まだ残っていること

項目場所状態
メールの送信に失敗しても、呼び出し元に返らない(ログだけ)backend/app/services/email_service.py の send_*別タスク
main でも失敗しているテスト 12 件backend/tests/未着手