12k
All articles

開発環境でポート番号を名前付きURLに置き換える

.localhost、リバースプロキシ、portlessでlocalhostのポートを名前付きURLに置き換え、ポート競合、Cookie漏れ、別タブ混乱を防ぎます。

OpenReplay Team
OpenReplay Team
開発環境でポート番号を名前付きURLに置き換える

localhostドメイン名とは、app.localhost のような人間が読める形式のホスト名で、127.0.0.1 に解決されるものです。これにより、各ローカルサービスは変動するポート番号ではなく、安定したアドレスを保持できます。

3つの開発サーバーを起動している状態で、修正内容を確認しようと localhost:3000 のタブに戻ったら、そこに表示されていたのは昨日のプロジェクトだった、という経験はおそらくあるでしょう。localhost:3000app.localhost に置き換えるだけで、日々の煩わしさ(ポートの競合、URLのずれ、Cookieの混線、そして「タブ間違い」問題)がまとめて解消されます。各アプリが独自のホスト名を持ち、それに伴って独立したブラウザスコープを得られるからです。本記事では、そこへ至る3つの方法を扱います。ブラウザ組み込みの .localhost TLD、自作のリバースプロキシ、そしてVercel Labsが専用に開発したローカルプロキシ portless です。

要点

  • .localhost TLDは RFC 6761 によってループバック用途に予約されているため、その配下の任意の名前はhostsファイルへの記載なしにChrome、Firefox、Edgeで 127.0.0.1 に解決されます。システムリゾルバに委ねるSafariでは、依然としてhostsエントリが必要になる場合があります。
  • ブラウザはCookieをホスト単位でスコープし、ポートを無視するため、app.localhostapi.localhost は分離される一方、localhost:3000localhost:3001 は同一のCookieジャーを共有します。
  • .localhost TLDだけではポートは省略できません。アプリは依然としてポートをリッスンしているため、ホスト名をそのポートにマッピングするリバースプロキシが必要です。
  • portless(Vercel Labs製、まだ1.0未満)は、PORT 環境変数を介して各アプリに4000〜4999番の範囲でエフェメラルポートを割り当て、安定した name.localhost URLをそこへルーティングします。HTTPSとHTTP/2はデフォルトで有効です。
  • agentsファイルに記録された安定した名前付きURLがあれば、AIコーディングツールはポート3001か8080かを推測することなく、正しいサービスにアクセスできます。

なぜ名前付きURLはポート番号に勝るのか?

ポートベースのローカル開発は、複数のサービスを動かし始めた途端、決まったパターンで破綻します。使用中のポートで2つ目のアプリを起動すればNodeは EADDRINUSE を投げます。自動的にポート番号をインクリメントするフレームワークはクラッシュこそ回避しますが、代わりに「ずれ」を生みます。今日は localhost:3001 にあったブログが明日は localhost:3002 になり、ブックマークは腐り、localhost:3000 のブラウザ履歴は無関係なプロジェクトが積み重なった、たどりようのない山になります。あるサーバーを終了して、空いたポートで別のサーバーを起動すれば、開きっぱなしのタブが何も言わずに別プロジェクトを表示します。これが「タブ間違い」問題です。

より気づきにくい失敗が、状態の混線です。ブラウザはCookieをホスト単位でスコープし、ポートを無視するため、localhost:3000localhost:3001 は同じCookieジャーに書き込みます。あるアプリのセッション状態が別のアプリに漏れ出すのです。名前付きサブドメインはこれをオリジンのレベルで解決します。app.localhostapi.localhost は別個のホスト名なのでCookieがきれいに分離され、さらに同一オリジンポリシーはスキーム・ホスト・ポートをキーとするため、localStoragesessionStorage も分離されます。このTLDに関するMicrosoftのガイダンスも同じ点を指摘しています。ローカルアプリごとに固有の名前を与えることで、Cookieのような名前スコープのアセットが分離され、アドレスバーの名前を見るだけでどのアプリを見ているのか一目でわかります。

.localhost TLDとは何か?

最もシンプルな名前付きURLの仕組みは、ブラウザに標準搭載されています。RFC 6761は .localhost TLDとその配下のすべての名前をループバックアドレス用に予約しており、だからこそ app.localhost は一切のセットアップなしに 127.0.0.1 で応答します。Chrome、Firefox、Edgeはこの解決を内部で処理し、任意の *.localhost 名を 127.0.0.1 または ::1 にマッピングするため、こうした名前は localhost で既に稼働しているものへのエイリアスとして機能します。注意すべきはSafariです。Safariは名前をシステムのDNSリゾルバに渡すため、リゾルバの設定によっては .localhost サブドメインに応答しないことがあります。その場合は /etc/hosts エントリが必要になるかもしれません。

ただし落とし穴があります。TLDだけではポートは省略できません。アプリは依然として :3000 をリッスンしており、ポートなしの app.localhostapp.localhost:80 にアクセスするだけで、そこでは何もリッスンしていません。実際に番号を消すには、Host ヘッダーを読んでアプリの実際のポートに転送する、80番または443番ポート上のリバースプロキシが必要です。

DIY: hostsファイル+リバースプロキシ

名前付きURLは、すでに馴染みのある部品を組み合わせて構築できます。/etc/hosts にホスト名を追加する(または .localhost の自動解決に頼る)、そして名前を開発サーバーのポートにマッピングするリバースプロキシを走らせる、という流れです。Caddy の設定はこれ以上ないほど簡潔です:

app.localhost {
  reverse_proxy localhost:3000
}
api.localhost {
  reverse_proxy localhost:8080
}

Caddyはローカル用のTLS証明書を自動でプロビジョニングします。nginxやTraefikでも同じことはできますが、設定はより煩雑になります。ワイルドカードのローカルドメインが必要な場合、dnsmasq を使えば *.test 空間全体を 127.0.0.1 に解決できるため、名前ごとのhostsエントリを省けます。また、プロキシが安定した転送先を持てるよう、各開発サーバー側でもホストとポートを固定する必要があります(Viteなら server.hostserver.port、webpackなら devServer)。

トレードオフは保守コストです。プロキシ設定、証明書の信頼設定、hostsエントリ、プロジェクトごとのポート割り当てを維持し、サービスが増減するたびにこの4つを手作業で同期させ続けることになります。長期運用のアプリが1つ2つなら問題ありません。しかしモノレポ全体となると、それ自体が一つの雑務になります。

portless: そのまま動く名前付きURL

portlessは、この一連の作業をすべて自動化するローカルプロキシです。開発コマンドの前に付けるだけで、next devportless run next dev になります。あるいは portless を単体で実行すれば、package.json、gitルート、またはディレクトリ名からアプリ名を推測してくれます。プロキシは自動起動し、4000〜4999番の範囲で空きポートを割り当て、それを PORT 環境変数経由で注入し、https://name.localhost をそこへルーティングします。Vite、Astro、Angular、Expoのように PORT を無視するフレームワークには、適切な --port フラグが代わりに渡され、必要な場合は対応する --host フラグも付与されます。

0.15.x リリースでは、portlessは443番ポート上でHTTP/2付きのHTTPSをデフォルトで有効化し、初回実行時にローカル認証局を生成して信頼登録します。 443番のバインドにはroot権限が必要なため、macOSとLinuxでは sudo で自動的に権限昇格します。プロンプトをスキップした場合は portless trust でCAを後から追加できます。オプトイン式の --https フラグと :1355 というデフォルトポートを紹介している過去の記事は、すでに置き換えられたバージョンについての説明です。HTTP/2がローカルで効く理由は明確です。ブラウザは1つのホストに対してHTTP/1.1接続を6本までしか同時に保持しないため、バンドルされていない数百のファイルを個別に配信する開発サーバーではリクエストがキューイングされてしまいます。一方、HTTP/2なら1本の接続ですべてを同時に運べます。portlessはNode.js 24以降を必要とします。

規模の大きな構成で真価を発揮する機能もいくつかあります。api.myapp.localhost のようなサブドメインでマイクロサービスを整理できますし、モノレポのルートに portless.json を1つ置けばワークスペースのパッケージを自動検出します。Dockerコンテナのようにポートを変更できない固定ポートのサービスに対しては、portless alias <name> <port> で名前付きURLをマッピングできます。また PORTLESS=0 を指定すればプロキシを完全にバイパスできるため、CIや簡易テストに便利です。カスタムTLDを使いたい場合、portlessはRFC 6761で同じく予約されている .test を推奨し、他の2つについては警告しています。.local はmDNSやBonjourと競合し、.dev はGoogleが保有していてHSTSによりHTTPSへの強制アップグレードがかかるためです。

AIコーディングエージェントにとって安定したローカルURLが重要な理由

AIコーディングエージェントは、ポートに関して人間と同じ失敗を、しかも静かに犯します。コンテキストの中で以前見かけた番号をハードコードするか、あるいは間違った推測をするのです。AGENTS.md ファイルから固定の https://api.myapp.localhost を読み取るエージェントは、セッションごとに3001と8080を行き来したり、確認のために作業を中断させたりすることなく、常に正しいサービスを対象にします。これは開発ツール全般に及ぶ変化です。安定したエンドポイントは自動化のためのインフラなのです。portlessはスキルファイルを同梱しており、0.15.xリリースではMarkdownのドキュメントページと llms.txt インデックスが追加され、そのURLが初期状態からエージェントに発見可能になっています。

アプローチの選び方

.localhost TLDのみTLD + リバースプロキシportless
ポートを省略できるか?不可
追加ツールなしCaddy/nginx/Traefikグローバルインストール1つ
HTTPS手動プロキシが提供デフォルトで有効
モノレポの自動検出なしなしあり
エージェント対応部分的部分的あり(スキルファイル、llms.txt)
セットアップの手間最小中(手動同期)

一言で言えばこうです。新しいツールを一切増やしたくなく、設定の保守も苦にならないなら、組み込みTLD+リバースプロキシを選びましょう。多数のサービス、モノレポ、あるいはAIエージェントとの組み合わせで名前付きURLをそのまま動かしたいなら、portlessを選びましょう。

名前付きで安定した、人間が読めるローカルURLは、ポート番号より明確に優れています。しかも導入は数分で済みます。今日2行のCaddyfileを追加するか、開発スクリプト1つに portless を付けるだけで、EADDRINUSE のことは二度と考えずに済むようになります。

FAQ

.localhostサブドメインを/etc/hostsファイルに追加する必要はありますか?

Chrome、Firefox、Edgeでは不要です。RFC 6761が.localhost TLDをループバック用途に予約しているため、これら3つのブラウザは.localhost TLD配下の任意の名前を自前で127.0.0.1に解決します。したがってapp.localhostやapi.localhostは設定ゼロで動作します。例外はSafariで、名前解決をシステムのDNSリゾルバに委ねるため、リゾルバの設定によっては.localhostサブドメインに応答しない場合があります。名前が読み込めない場合は、/etc/hostsエントリを追加してください。

.localhostドメインを使うと開発サーバーのポート番号は省略できますか?

いいえ。.localhost TLDはホスト名を127.0.0.1に解決するだけです。アプリは元のポートをリッスンし続けるため、ポートなしのapp.localhostはapp.localhost:80にアクセスすることになり、そこでは何も動いていません。実際に番号を省略するには、Hostヘッダーを読んでアプリの実際のポートに転送する、80番または443番ポート上のリバースプロキシが必要です。Caddyやportlessのようなツールは、まさにこれを自動化するものです。

なぜCookieはlocalhost:3000とlocalhost:3001の間では漏れるのに、app.localhostとapi.localhostの間では漏れないのですか?

ブラウザはCookieをホスト単位でスコープし、ポートを無視します。そのためlocalhost:3000とlocalhost:3001は同じlocalhostというホストを共有し、結果として同じCookieジャーを共有します。名前付きサブドメインは異なるホストを持つため、app.localhostとapi.localhostは別々のCookieを保持します。また、同一オリジンポリシーはスキーム・ホスト・ポートをキーとするため、異なるホスト名はlocalStorageとsessionStorageもきれいに分離します。ポートベースのオリジンではこれができません。

portlessが必要とするNode.jsのバージョンは何ですか?また、sudoなしで動作しますか?

portlessはNode.js 24以降を必要とします。macOSとLinuxでは、HTTPSのために443番ポートをバインドするにはroot権限が必要なため、初回実行時にsudoで自動的に権限昇格します。HTTPSは初期状態からHTTP/2付きで動作し、portlessは初回実行時にローカル認証局を作成して信頼登録します。初回のプロンプトをスキップした場合は、portless trustを使って後からCAを追加してください。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.