「プレビュー環境で確認をお願い」 「はい、確認しました。プレビューサーバーを起動しました」

AIエージェントとの協業は快適です。彼らは文句も言わずに何度でもビルドし、確認してくれます。 しかし、ふとタスクマネージャーを見ると、そこには地獄のような光景が広がっていました。

Node.js のプロセスが、10個も20個も起動したまま放置されていたのです。

親切心が招く「ポート枯渇」

原因は、Astro(および多くの現代的なフレームワーク)が持つ「開発者への親切設計」にありました。 通常、Webサーバーを起動する際にポート(例えば4321)が既に使われていると、エラーで終了するのが古典的な挙動です。しかしAstroは気を利かせてくれます。

「おっと、4321は埋まってるね。じゃあ自動的に 4322 で立ち上げるよ!」

人間が開発している分には便利な機能ですが、状態管理が苦手なAIエージェントがこれを扱うと、「前のプロセスを殺し忘れたまま、新しいポートで次々とサーバーを立ち上げ続ける」 というゾンビ増殖が発生します。

設定ファイルでも止まらない

当初、package.json--port 4321 を指定したり、設定で strictPort: true を試みたりしました。 しかし、Astroの preview コマンド(静的ファイルサーバー)はこの設定を一部無視し、頑なに「空いているポートを探す」という親切な挙動を続けました。フレームワークの優しさは、時には残酷です。

Pythonで「殺して奪う」

フレームワークの設定で止められないなら、フレームワークを呼ぶ前に物理的に解決するしかありません。 「ポート4321を使っている奴がいるなら、そいつを殺してから自分が座る」 という、極めて野蛮かつ合理的なラッパースクリプト (strict_preview.py) を作成しました。

def kill_process_on_port(port):
    """ポートを占有しているプロセスを特定してKillする"""
    # netstat でPIDを特定し、taskkill で強制終了
    # (Windows環境の実装例)
    subprocess.run(f"taskkill /F /PID {pid}", shell=True)

if __name__ == "__main__":
    # 先住者を排除
    kill_process_on_port(4321)
    
    # 焼け野原になったポート4321で悠々と起動
    subprocess.run(f"npx astro preview --port 4321", shell=True)

そして package.json を書き換えます。

"scripts": {
  "preview": "uv run scripts/strict_preview.py"
}

結論:優しさはコードで殺す

これでようやくゾンビは死滅しました。 pnpm preview を叩けば、裏で古くなったサーバーが自動的に始末され、常に最新のサーバーがポート4321で立ち上がります。

「空いてるところを適当に使っていいよ」というブラックボックスな優しさよりも、「邪魔なやつはどけ」という明快な力技。これこそが、自動化された運用環境における唯一の正解でした。