Vercel Labsのportlessでlocalhostのポート番号を名前付きURLに置き換えてみた

Vercel Labsのportlessでlocalhostのポート番号を名前付きURLに置き換えてみた

2026.05.06

こんにちは、豊島です。

最近、コードを直接書く時間より、AIエージェントに指示を出して動作を確認したり、生成されたコードを読む時間のほうが長くなりました。エディタも使うものの、主に触っているツールはWarpに移ってきています。

このようにdevtoolsとの関わり方が変わると、ツール側にも「人間が直接触ること」を前提にしない設計が求められてきます。
「人間のためのツール」から、「AIエージェントのためのツール」へ。devtoolsそのものが、AIコーディングエージェントが日常的にコードを書く時代に合わせて再設計されつつあります。

このシフトをそのままミッションに据えているのがVercel Labsです。Vercel CEOのGuillermo Rauch氏(@rauchg)はXで以下のように表現しています。

https://x.com/rauchg/status/2049216048831025232

Vercel Labsはagent-browserportlessskillschatjust-bashjson-renderといった一連のツール群を出していて、累計で22.8Mダウンロードに到達しているとのこと。
今回はその中から、普段意識しないだけで毎日踏んでいるローカル開発の課題を解決するportlessを実際に試してみました。

portlessとは何か

portlessは、ローカル開発サーバーのhttp://localhost:3000のような「ポート番号付きURL」を、https://myapp.localhostのような「名前付きURL」に置き換えるためのリバースプロキシです。

# Before
"dev": "next dev"                # http://localhost:3000

# After
"dev": "portless run next dev"   # https://myapp.localhost

導入はこれだけです。

なぜ「ポート番号」が問題なのか

正直なところ、最初にportlessを見たとき、「ポート番号って、そんなに困ってたっけ?」と思いました。localhost:3000で長年やってきましたし、慣れの問題のようにも感じます。

公式サイトのWhy portlessページには、ポート番号が引き起こす不便が列挙されています。確かに最近踏んでいるな、というものがいくつかありました。

  • APIは3001だっけ8080だっけ、と毎回ポート番号を思い出す必要がある
  • モノレポではサービスの数だけポートが必要になる
  • AIエージェントが誤ったポート番号を推測したり、ハードコードしがち
  • localhostに紐づくCookieやlocalStorageはポートを跨いで漏れる
  • localhost:3000のブラウザ履歴は無関係なプロジェクトの寄せ集めになり、検索しても役に立たない

試してみる

検証環境は以下のとおりです。

  • macOS(Darwin 24.6.0)
  • Node.js 24.5.0
  • portless 0.12.0
  • 検証アプリは最小構成のNode.js HTTPサーバー(process.env.PORTを読んでHostヘッダを返すだけのもの)

手順1: インストールと最小実行

# インストール
pnpm add -g portless        # npm install -g portlessでも可

# プロキシを起動(443はsudo必須のため、非特権ポートの1355で立てる)
portless proxy start --port 1355 --https

# プロジェクトディレクトリで実行
portless run node server.js

実際に動かしたときの出力がこちらです。

portless

-- Proxy is running
-- myapp.localhost (auto-resolves to 127.0.0.1)
-- Name "myapp" (from package.json)
-- Using port 4348

  -> https://myapp.localhost:1355

Running: PORT=4348 HOST=127.0.0.1 PORTLESS_URL=https://myapp.localhost:1355 NODE_EXTRA_CA_CERTS="/Users/.../.portless/ca.pem" node server.js

listening on 4348

ポイントは2つです。

  1. プロジェクト名はpackage.jsonnameフィールドから自動で抜き出される(今回はmyapp
  2. アプリがlistenするポートはportlessが空きを探してPORT環境変数で渡す(今回は4348)

portless listで現在のルーティング状況も確認できます。

Active routes:

  https://myapp.localhost:1355  ->  localhost:4348  (pid 40636)

https://myapp.localhost:1355curlで叩いてみると、HTTP/2で200が返り、Hostヘッダは内部ポート(127.0.0.1:4348)に書き換わったうえでアプリに届いていることが分かります。

$ curl -sk https://myapp.localhost:1355/ -w "\nHTTP/%{http_version} %{http_code}\n"
Hello from myapp on PORT=4348
Host: 127.0.0.1:4348
URL: /

HTTP/2 200

確かに、ブラウザから見えるURLは安定したmyapp.localhost:1355のままで、内部のポート番号は意識する必要がない、という設計どおりに動いていました。

手順2: package.jsonに書き込む

実用上はこちらが本命です。

package.json
{
  "scripts": {
    "dev": "portless run next dev"
  }
}

これでpnpm dev一発で常に同じ名前付きURLにアクセスできるようになります。手元の他プロジェクトと並行して立ち上げても、URLが衝突しません。

手順3: サブドメインとモノレポ

モノレポのケースでは、サービスごとにサブドメインを切ることができます。

portless api.myapp pnpm start
# -> https://api.myapp.localhost:1355

portless docs.myapp next dev
# -> https://docs.myapp.localhost:1355

加えて、ワイルドカードサブドメインも追加登録不要で動きます。マルチテナントSaaSのようにサブドメインを切ってアクセスを分けたい開発時には便利です。

tenant1.myapp.localhost:1355  -> myappに転送
tenant2.myapp.localhost:1355  -> myappに転送

手順4: Git Worktreeとの統合

個人的に「これは効く」と感じたのが、Git Worktreeとの統合です。portless runは、現在のチェックアウトがLinked worktreeかどうかを自動検出します。Linked worktreeの場合、ブランチ名がサブドメインのプレフィックスとして付与されます。

実機でgit worktree add ../myapp-fix-ui -b fix-uiしてから、worktree側でportless runしてみました。

portless

-- Proxy is running
-- fix-ui.myapp.localhost (auto-resolves to 127.0.0.1)
-- Name "myapp" (from package.json)
-- Prefix "fix-ui" (from git branch)
-- Using port 4897

  -> https://fix-ui.myapp.localhost:1355

Prefix "fix-ui" (from git branch)というメッセージが出ているとおり、ブランチ名がサブドメインのプレフィックスとして自動付与されました。portless listで見ると、main側とfix-ui側が別ルートとして同居しています。

Active routes:

  https://myapp.localhost:1355         ->  localhost:4348  (pid 40636)
  https://fix-ui.myapp.localhost:1355  ->  localhost:4897  (pid 41766)

両方を同時にcurlしたところ、それぞれ別のNodeプロセス(PORT=4348PORT=4897)に正しく振り分けられました。手動でポート番号を割り当てなくても、worktreeを切るだけで自動的に別URL・別ポートに分離されます。

この自動分離が効くのは、複数ブランチを並走させたい場面です。具体的にはこんなシーンを想定しています。

  • レビュー対象ブランチをworktreeで切り出して、別タブでhttps://review.myapp.localhost:1355を開けば、メインworktreeを止めずにPRの動作確認まで進められる
  • Claude Codeをfix-uiブランチのworktreeに張り付かせて作業させつつ、自分はmainで別作業、という並列フローが成立する。AIエージェントには「https://fix-ui.myapp.localhost:1355」と固定で渡せば、ポート番号の推測ミスも起きない
  • myapp.localhostfix-ui.myapp.localhostは別オリジン扱いになるので、Cookieやログイン状態が片方からもう片方へ漏れない(これは複数プロジェクト間でも効きます。詳細は後段の「Cookie/localStorageの汚染回避」で扱います)
  • バグ再現用のworktreeを起動しっぱなしにしておいても、https://repro-bug-403.myapp.localhost:1355のようにURLが固定なので、別作業から戻ってきやすい

これまで同じことをやろうとすると、PORT=3001 pnpm devのように手で割り当てるか、Cookie衝突を避けるためにブラウザプロファイルを切り替えるか、認知負荷の高い運用が必要でした。portlessなら、portless runを一度仕込んでおくだけで済みます。

複数プロジェクトを同時開発するケースでも同じ仕組みが効きます。projA.localhost:1355projB.localhost:1355は別ホスト名なので、片方が起動しっぱなしでも他方の起動を妨げません。AIエージェントを複数並走させながら複数プロジェクトを行き来する運用とも相性が良さそうです。

手順5: portlessを切りたいとき

CIや特定のスクリプトでだけ素のポート番号で動かしたい、ということもあります。その場合は環境変数で切れます。

PORTLESS=0 portless run node server.js
# -> プロキシを経由せず、PORT=3000で直接listen

実際に試したところ、PORTLESS=0を付けるとportless run配下のコマンドはそのまま素のnode server.jsとして走り、http://127.0.0.1:3000で応答するようになりました。package.jsonscripts.devportless run ...に置き換えていても、必要なときだけ素の挙動に戻せるのは安心感があります。

仕組み

仕組み自体はシンプルです。

  1. リバースプロキシがHTTPSで443ポート(または--portで指定したポート、検証では1355)で待ち受ける
  2. portless runで起動したアプリには、空いているポートをPORT環境変数で渡す(検証では4000番台が割り当てられた)
  3. プロキシはHostヘッダ(myapp.localhost)を見て、対応するアプリのポートに転送する

実際の検証でも、PORT=4348でNode.jsサーバーが立ち上がり、https://myapp.localhost:1355のリクエストがそのポートに転送される様子を確認できました。

.localhostサブドメインはChrome、Firefox、Edgeでは自動的に127.0.0.1に解決されるため、/etc/hostsをいじる必要はありません。SafariはOSのリゾルバに依存するので、必要ならsudo portless hosts sync/etc/hostsに書き込む補助コマンドが用意されています。

ほとんどのフレームワーク(Next.js、Express、Nuxt等)はPORT環境変数を読みますが、ViteやAstro、React Router、Angular、Expo、React NativeのようにPORTを見ないものもあります。これらに対してはportlessが自動で--port--hostフラグを差し込んでくれるので、設定変更なしで動きます。

ほかに効きそうなシーン

ここまでの基本機能を踏まえたうえで、実運用で効きそうなシーンを2つ挙げておきます。

マルチテナントSaaSのサブドメインルーティング検証

サブドメインでテナントを分離するタイプのSaaSをローカル開発するとき、これまでは/etc/hoststenant1.example.localtenant2.example.localを追加してlocalhostに向ける、といった手間が必要でした。

portlessの場合、ワイルドカードサブドメインがそのまま動くので、/etc/hostsを一切触らずに次のようなアクセスができます。

https://tenant1.myapp.localhost:1355  -> myappに転送
https://tenant2.myapp.localhost:1355  -> myappに転送

Hostヘッダがそのままアプリに届くので、テナント振り分けロジック(subdomainを見てDBやスキーマを切り替える等)の動作確認をローカルで完結できます。AIエージェントに「テナントAでログインしてからテナントBに切り替える挙動を再現して」のような指示を出すときも、URLが安定しているので意図どおりに動きます。

Cookie/localStorageの汚染回避

手順4で触れた「別ホスト名は別オリジン」というメカニズムは、複数プロジェクトを行き来する開発でも効きます。http://localhost:3000でprojAにログイン → projAを止めて同じポートでprojBを起動 → projAのCookieがprojBに残って表示が崩れる、というトラブルに遭遇したことがある人は多いのではないでしょうか。
これはlocalhostのCookieやlocalStorageがホスト名単位で管理されており、ポート番号が違っても同じlocalhostオリジンとして扱われる挙動が原因です。

portlessでプロジェクト名ごとに別ホスト名(projA.localhostprojB.localhost)が割り当たれば、ブラウザ側でそれぞれ独立したオリジンとして扱います。
Cookie・localStorage・IndexedDB・Service Workerすべてがプロジェクト単位で隔離されるので、片方の状態がもう片方に漏れる事故がなくなります。複数PJを並列開発する運用だと、これだけでも導入価値があります。

まとめ

portlessは、ローカル開発の「ポート番号」というレイヤーを名前付きURLに置き換えるリバースプロキシです。

  • 設定不要・コード変更不要で導入でき、抜けるのも環境変数1つ
  • Git Worktreeと組み合わせると、並列AIエージェント開発の体験が一段階上がる
  • 複数プロジェクトの同時開発でもサブドメイン分離によりポート競合がなくなる
  • マルチテナントSaaSのサブドメインルーティング検証や、プロジェクト間のCookie/localStorage汚染回避にも効く

引き続きVercel Labsのライブラリを検証して、ブログにしていこうと思います。

この記事をシェアする

関連記事