dev サーバーを止めずにデプロイできるよう、Nuxt のビルド出力先と Content DB を分けた
dev サーバーを止めずにデプロイできるよう、Nuxt のビルド出力先と Content DB を分けた
ビルドとデプロイに何分かかっているかを測ろうと、所要時間計測スクリプトを叩いた。
=== Nuxt Build & Deploy Phase Timer ===
ヘッダーだけ出て、そこで止まった。 ポート3000で dev サーバーが動いている、というのが理由だった。
前は動いていたはずだ。 localhost を立ち上げっぱなしのままデプロイして、それで困った覚えがない。
デプロイを止めていたガード
止めていたのは assert-no-dev-server.mjs で、ポート3000を検査して pnpm generate を中断する。
入ったのは 2026-07-30 の 14:21、つまり前日だった。
理由はコミットメッセージにそのまま書いてあった。
pnpm dev と pnpm generate は、既定で .nuxt と .data/content/contents.sqlite を共有する。
同時に走ると prerender 中の content query が 500 を返し、prerender.failOnError: false がそれを握り潰す。
結果として、記事が抜け落ちた dist が「ビルド成功」として出来上がる。
7/30 はこれで記事189本が消えていた。
厄介なのは、失敗が失敗として現れない点だ。 ビルドは成功で終わる。
自分の記憶では「本番に出てから気づいた」ことになっていたが、記録を読ませたら違った。
dist から記事が抜けた段階で、postgenerate の verify-blog-payload が止めていた。
本番までは行っていない。
そしてこの日の午前、そのガードにもう一度引っかかっている。 10:52 に dev サーバーを止めてデプロイを流したのに、10:53:19 に新しい node がポート3000を掴んでいた。 自分では止めたつもりでも、1分後には別の node が同じポートを握っている。
ビルド出力先と Content DB の分離
ガードの発想は「dev を止めてから generate しろ」である。 筋は通っているが、別セッションが dev を立て直すたびにデプロイできなくなる。 立てた覚えのない dev サーバーのせいで、こちらのデプロイが止まる。
そこで方針を変えて、競合そのものを消すことにした。
調べさせたら、@nuxt/content に _localDatabase という内部オプションがあった。
SQLite の置き場を差し替えられる。
generate 側だけ、そこで別ファイルに逃がす。
dev 側の設定は動かさない。 止めさせないための分離なので、そこを触っては意味がない。
実装に入る直前で、共有していたのが .data だけではないと分かった。
.nuxt も共有していて、しかも計測スクリプトの227行目がそれを無条件で消していた。
dev が使っているビルドディレクトリを、デプロイのたびに消していたことになる。
buildDir も分ける必要がある。
数行で済む話ではなくなった。
nuxt.config.ts は環境変数で切り替えるだけにして、既定値は従来どおり残した。
渡すのは pnpm generate にだけである。
NUXT_BUILD_DIR=.nuxt-build
NUXT_CONTENT_LOCAL_DB=.data/content-build/contents.sqlite
nuxt.config.ts を触る以上、ビルド中に混ぜるわけにはいかない。
先にデプロイを済ませ、それから分離を入れる順序にした。
デプロイは約20分かかり、14:12 に完了した。
ポートチェックに代わる分離ガード
ポートを見るガードは要らなくなる。 ただし外すだけだと、分離が壊れたときに何も止めなくなる。
代わりに assert-build-dirs-separated.mjs を新設して、毎回3点を検査させた。
- 環境変数が渡っているか
nuxt.config.tsがそれを読んでいるか- 値が dev 側と別物か
判定できなければ止める fail-closed にした。
新しいガードで怖いのは「入れたのに素通りしていた」ことなので、壊れた状態を作って本当に止まるかを試させた。 両方のケースで止まった。
途中、検証用に作ったディレクトリを消そうとして、再帰削除ガードに変数展開を拒否された。 消すのはやめて、別の新しいディレクトリで検証を続けた。
判定ロジックは純粋関数に切り出させ、テストを4件足した。 既存と合わせて9件が通った。
dev を動かしたままの generate 検証
設定が揃ったので、dev を起動したまま pnpm generate を回す。
これが本番相当の検証になる。
一度目は prerender の途中、tags ページのあたりで止まった。 自分では何も止めていない。
exit code は 3221225794、つまり 0xC0000142(STATUS_DLL_INIT_FAILED)だった。
Windows がプロセスを起動できなくなったときのコードで、典型的な原因はメモリかハンドルの枯渇である。
空きメモリは 3.9GB。 node が 9.6GB 以上を持っていた。 generate は 8GB を要求するので、そもそも届かない。
内訳を出させると、二つの塊が並んだ。
- dev サーバー3つ(4.9GB)
- chrome-devtools MCP 4つ(4.8GB)
合わせて 9.7GB。
MCP の起動時刻は 06:37、06:39、09:32、10:16 と古い。
どれもこのセッションからは接続できず、ハングしていたものだった。
list_pages が応答しなかったのも、これが原因だった可能性がある。
掃除させたら空きが 3.1GB から 6.7GB になった。 3000で動いている別セッションの dev は残す。 それが DB を掴んでいない状態で完走しても、検証にならない。
再実行の途中でまた 1.9GB まで落ちたので、使っている 3003 だけ残して他を落とさせた。 1.9GB から 5.5GB に戻った。
このあたりで、走っているのがデプロイなのか分からなくなって聞いた。
答えは「デプロイではない」だった。
pnpm generate はビルドと postgenerate の検証7本までで、Cloudflare へのアップロードは含まない。
ローカルで完結する。
二度目は完走した。
検証7本すべて通過。
7/30 に落ちた verify-blog-payload(250/250 リンク)と verify-tag-related-output(欠落0件)が、dev を動かしたまま両方パスした。
走りっぱなしのプロセスをどう検知するか
メモリを食い潰していたのは、閉じ忘れたセッションの残骸である。
/clear は打っている。
ただし /clear が消すのはコンテキストだけで、そのセッションが立てた dev サーバーや MCP サーバーは残る。
セッションを並べて使っていると、どれがどれの残骸かが分からない。
そこで、ステータスラインに走っている数を出せないか頼んだ。
計測から入らせた。
ステータスラインは頻繁に呼ばれるので、重い処理を足すと体感を壊す。
netstat は 54ms、tasklist は 665ms。
後者は使えない。
ところが netstat の結果が空で返ってくる。
原因は文字コードだった。
Windows の netstat は cp932 で出力するのに、デコードで出た例外を握り潰していた。
直しても、今度は dev サーバーだけ出てこない。
-p TCP が IPv4 に限定していて、dev は [::1](IPv6 ループバック)でリッスンしていた。
そこまで直して 3001 と 3003 を 95ms で拾えるようになり、ステータスラインの4行目に数が出た。
出たものを見て、狙いから外れているのに気づいた。 表示されるのはマシン全体の数であって、セッションごとではない。 どのセッションのプロセスかが分からなければ、「そこで止めてくれ」と伝えられない。
セッション固有の判定ができないか試させた。
psutil 7.0.0 が入っていて、祖先チェーンから claude.exe(PID 37432)まで辿れた。
原理的にはできる。
ただし 205ms かかり、children(recursive=True) は 242ms。
一括取得に変えると、かえって遅くなった。
ステータスラインが毎回払うコストではない。
結局、ステータスラインは数を出すだけにして、/procs コマンドを別に作らせた。
一覧にはリポジトリ名まで出るので、止めていいものを見て選べる。
ステータスラインには /procs への案内を足した。
異常に増えたことに気づければ、この用途は足りる。
止めた MCP のその後
しばらくして一覧を見たら、さっき落としたはずの MCP がまた走っていた。 3つ止めさせた。
MCP を止めた側のセッションは、そのままでは使えない。
/mcp から reconnect が要る。
繋ぎ直せば当然また立ち上がるので、止めて終わりにはならない。
更新で効かなくなっていた権限設定
別セッションで claude を起動したら、見慣れない警告が出た。
Permission allow rule (.claude\settings.local.json): Write(.claude/**) is not
matched by file permission checks — only Edit(path) rules are.
Use Edit(.claude/**) instead
事故か設定崩れかと思って調べさせたが、どちらでもなかった。
7ヶ月前から置いてある1行を、Claude Code 2.1.210 が新しく警告するようになっただけだった。
ファイル権限のチェックにマッチするのは Edit(path) のルールで、Write(...) 形式は元から効いていない。
効かないものを残す理由はないので、削除させた。
JSON として壊れていないこと、Write(...) 形式のルールが0件になったことまで確認済みである。
Edit(.claude/**) は残してあるので、.claude 配下の編集許可はこれまでどおり効く。
残ったもの
- 「dev を止めろ」というガードは、止められない事情(別セッションが立てた dev)を想定していなかった。禁止で守るより、競合そのものを消すほうが運用に耐える
- 新しいガードは、壊した状態を作って止まるところを見るまで信用しない
- 分離ガードが見ているのは generate の開始時だけで、走り出したあとに dev が立ち上がる分には無防備なまま
/clearはプロセスを片付けない。セッションを増やした日は、残骸がメモリを持っていく