.gitignoreの`logs`パターンがNext.jsのAPIルートを消した話

Tips

はじめに

自宅で育てている野菜の成長記録をAIが分析してくれる家庭菜園アプリ(AgriSnap)を作って、ホームサーバーにデプロイしました。 開発環境では問題なく動いていたのに、本番サーバーで記録を保存しようとすると必ず失敗する——そんなトラブルに遭遇しました。

原因は .gitignore に書いたたった1行でした。

ちなみにアプリは↓のようなものです。

家庭菜園アプリAgriSnapの植物一覧画面。オクラ・バジル・きゅうり・トマトの写真と成長記録がカード形式で表示されている様子
家庭菜園アプリAgriSnapの植物一覧画面。オクラ・バジル・きゅうり・トマトの写真と成長記録がカード形式で表示されている様子

症状

ブラウザのDevTools(Networkタブ)で確認すると、次のような挙動でした。

  • 写真アップロードPOST /api/photos/upload)→ 201 Created、正常
  • 記録保存POST /api/plants/:plantId/logs)→ 404 Not Found

フロントエンドには「記録の保存に失敗しました」と表示されるだけ。サーバーのログには何も出ない。

docker compose logs web --tail=100 2>&1 | grep -E "error|Error|ERROR"
# → 何も出力されない

調査の過程

まずDBスキーマを疑った

コードを調べると、記録保存のAPIルートは途中で追加したマイグレーションの列を使っています。「初回デプロイ時にそのマイグレーションが適用されていないのでは?」と考え、本番DBを確認しました。

docker compose exec postgres psql -U agrisnap -d agrisnap -c "\d watering_logs"

列はちゃんとある。DBスキーマは問題なし。

環境変数を疑った

Next.js のルートハンドラで植物データの取得がnullを返すと 404 を返す実装になっています。 まだ開発初期でユーザーIDを環境変数で管理していたため、「環境変数の値がDBのデータと一致していないのでは?」と疑い、本番サーバーで確認しました。

docker compose exec web printenv DEFAULT_USER_ID
docker compose exec postgres psql -U agrisnap -d agrisnap \
  -c "SELECT user_id FROM plants WHERE id = '...'"

環境変数とDBの値は完全に一致。環境変数も問題なし。

アップロード先のパーミッションを疑った

docker compose exec web ls -la /uploads
# → drwxr-xr-x nextjs nodejs

書き込み権限もある。問題なし。

gitのトラッキング状態を確認した

データは正しい、環境変数も正しい、でもルートハンドラが 404 を返す。「もしかしてルートファイル自体が本番に存在しないのでは」と疑いはじめ、git の状態を調べました。

git ls-files web/src/app/api/ | grep -v "^$"

出力されたのは以下のファイルのみ:

web/src/app/api/ai/status/[jobId]/route.ts
web/src/app/api/photos/[...path]/route.ts
web/src/app/api/photos/upload/route.ts
web/src/app/api/plants/[plantId]/route.ts    ← logsは無い
web/src/app/api/plants/route.ts
web/src/app/api/tasks/[taskId]/complete/route.ts
web/src/app/api/tasks/today/route.ts
web/src/app/api/weather/route.ts

plants/[plantId]/logs/route.ts がない! git ls-files --others web/src/app/api/ で未追跡ファイルを調べると:

web/src/app/api/plants/[plantId]/logs/[logId]/route.ts
web/src/app/api/plants/[plantId]/logs/route.ts

この2ファイルが未追跡でした。しかも git status では「nothing to commit, working tree clean」と表示されます。gitignoreで除外されているため、未追跡ファイルとして表示すらされていなかったのです。

犯人

.gitignore の先頭にこんな行がありました:

# Logs
logs
*.log

この .gitignore は GitHubが公式に提供する Node.js 用テンプレート(github/gitignore)をベースにしていました。そこに含まれる logs は「ログディレクトリを無視する」パターンですが、このパターンはリポジトリ内のどこにある logs という名前のディレクトリ・ファイルにも一致します。

ここに Next.js App Router のファイルシステムルーティングが組み合わさります。App Router では ディレクトリ名がそのままURLのパスセグメントになる仕様のため、/api/plants/:plantId/logs というエンドポイントを作るには、logs という名前のディレクトリをファイルシステム上に作る必要があります。Express のようにコードでルートを定義する方式なら logs というディレクトリ自体が不要なので問題になりません。しかし App Router では URLに logs が現れる限り、必ず logs/ ディレクトリが生まれます。

つまり web/src/app/api/plants/[plantId]/logs/ というNext.jsのAPIルートディレクトリが、まるごとgit管理外になっていたのです。

.gitignore の logs パターン

web/src/app/api/plants/[plantId]/logs/ を無視

git commit に含まれない

本番サーバーにファイルが存在しない

Next.js がルートハンドラを見つけられない

404 Not Found(サーバーログなし)

サーバーログが何も出ないのも、アプリのコードが一切実行されていないからでした。Next.jsのルーティング層が「このパスには対応するハンドラがない」と判断して404を返していたため、console.error が呼ばれる余地すらなかったのです。

修正

.gitignorelogs/logs(リポジトリルート直下の logs/ のみを対象にする)に変更し、ファイルを追加してコミットしました。

 # Logs
-logs
+/logs
 *.log

除外されていたAPIルートファイルも一緒にコミットし、本番サーバーで git pull してコンテナを再ビルドして解決。

教訓

gitignoreのパターンは思わぬ場所にも一致する。

logs と書くと、プロジェクト内の logs という名前のディレクトリがすべて無視されます。ログディレクトリをルートにだけ作る運用であれば /logs と書くべきです。

パターン 一致するパス
logs リポジトリ内のすべての logs/ および logs ファイル
/logs リポジトリルート直下の logs/ のみ
logs/ リポジトリ内のすべての logs/ ディレクトリ(ファイルは除く)

また、今回のようにgitignoreで除外されたファイルは git status に表示されないため、「デプロイしたのになぜか動かない」という状況になりやすいです。この経験以来、新機能をコミットするときは git ls-files --others --exclude-standard で未追跡ファイルが残っていないかを必ず確認するようにしています。

本番デプロイ後にルートが丸ごと消えているというのはなかなか気づきにくいバグでした。同じところで詰まっている方の参考になれば幸いです。

コメント

タイトルとURLをコピーしました