メインコンテンツへスキップ

next dev が https://myapp.localhost になった日——portless で気づいた想定外の恩恵3つ

localhost:3000 のポート競合に疲れて portless を導入した。ポート問題は解消されたが、むしろ効いたのは Cookie の分離・オートコンプリートの整理・Windows 対応という想定外の3つの恩恵だった。

$ next dev
Error: listen EADDRINUSE: address already in use :::3000

どうせ別のプロジェクトが 3000 番を使っているやつです。PORT=3001 next dev と打ち直して、また別の日に 3001 も埋まって——この繰り返しに疲れて portless を入れました。

ポート競合が解消されるのは知っていました。でも実際に使い始めて気づいた恩恵は、そこではありませんでした。

portless とは

portless は Vercel Labs が開発したローカル開発用の npm ツールです(執筆時点で GitHub Stars 9,800 以上)。next dev のような既存のコマンドをラップして、https://myapp.localhost という URL でアプリが立ち上がるようにしてくれます。

http://localhost:3000 → https://myapp.localhost

セットアップ:2コマンドで終わります

# グローバルインストール
npm install -g portless

# ローカル CA を生成してシステムのトラストストアに登録する(初回のみ)
# HTTPS と HTTP/2 がデフォルトで有効になり、以降はブラウザに警告が出なくなります
portless trust

事前に確認:Node.js 24 以上が必要です。 v24 は現在の Active LTS です。node -v でバージョンを確認してから進めてください。

インストールが終わったら package.json にコマンドを追加します。dev スクリプトを直接書き換えると、まだ portless を入れていないチームメンバーが困るので、別のスクリプトとして追加するほうが安全です。

{
  "scripts": {
    "dev": "next dev",
    "portless:dev": "portless run"
  }
}

portless run はコマンド省略時に package.jsondev スクリプトを自動で読み取って実行します。portless run next dev と明示するのと同じ結果ですが、dev の中身が変わっても portless:dev 側は変更不要なのでメンテが楽です。アプリ名はディレクトリ名または package.jsonname フィールドから自動推論されます。

あとは npm run portless:dev を実行するだけです。

想定どおりの恩恵:ポート競合が消えた

portless は 4000〜4999 の範囲で空きポートを自動で割り当て、そのポートを myapp.localhost に紐づけます。URL 側は常に https://myapp.localhost で固定されるので、何番が割り当てられたかを気にする必要がありません。

複数プロジェクトを同時に起動しても EADDRINUSE は出ません。また localhost:3000 を止めて別のアプリを立ち上げたとき、開きっぱなしのタブをリフレッシュすると別のアプリが表示されてしまうあの現象もなくなります。

ここまでは想定どおりでした。

想定外の恩恵①:ログイン状態が混ざらなくなった

localhost の Cookie はポートをまたいで共有される

ブラウザの Cookie は「ドメイン」で管理されます。ポート番号は Cookie のスコープに影響しません。

つまり localhost:3000localhost:8080 は同じ localhost として扱われ、どちらかのアプリで Set-Cookie したものがもう一方でも見えてしまいます。

ログイン機能を持つサービスを複数ローカルで立ち上げているとき、この挙動がじわじわ問題になります。A のセッション Cookie が B 側のリクエストに混入して、「ログインしているはずなのに 401 が返ってくる」「別のアカウントでログインしたつもりが A のユーザーとして認識される」といった状況が発生します。

.localhost サブドメインになると分離される

portless を使うと myapp.localhostapi.localhostadmin.localhost それぞれが独立したドメインとして扱われます。Cookie のスコープが分離されるので、A のセッションが B に混入することがなくなります。

ログイン必須のサービスを複数持つプロジェクト構成では、これだけでもポート競合の解消と同じくらいの恩恵があります。

想定外の恩恵②:オートコンプリートがプロジェクトごとに分かれた

ブラウザのアカウント補完(ID・パスワード)も Cookie と同様、ドメインをキーにして管理されます。

localhost で開発していると、すべてのプロジェクトのログイン情報が同じ localhost に紐づいて蓄積されます。ログインフォームを開くたびに無関係なプロジェクトのアカウント候補が混ざってくる状態です。

portless を使うと myapp.localhostadmin.localhost それぞれに補完候補が紐づくので、フォームを開けばそのアプリのアカウントだけが提案されます。

目立たない変化ですが、複数サービスを並行開発している環境だと体感として大きく変わります。

想定外の恩恵③:Windows でも動いた(OpenSSL のひと手間あり)

Vercel 系のツールで Windows を公式サポート対象として明記しているものは、意外と少ないです。ドキュメントに Windows の手順がなかったり、/etc/hosts の編集コマンドが Unix 前提だったりすることがよくあります。

portless の対応 OS には macOS や Linux と並んで Windows が明記されています。ただし HTTPS 証明書の生成に OpenSSL を使う都合上、Windows では別途インストールが必要です(macOS はデフォルトで同梱されており、Linux もほぼすべてのディストリビューションに含まれています)。

まず OpenSSL を入れます。winget が使える環境なら 1 コマンドです。

winget install -e --id ShiningLight.OpenSSL.Dev

Git for Windows を導入済みであれば、C:\Program Files\Git\usr\bin\openssl.exe がすでに存在している可能性があります。パスが通っていれば追加インストールは不要です。

OpenSSL を準備した後は macOS や Linux と同じ手順で動きます。初回の portless trust でポート 443 バインドのために管理者権限ダイアログが出ますが、それだけです。

Windows 限定の既知の問題(v0.9.2 以降で修正済み) winget でインストールした ShiningLight.OpenSSL.Dev を含む一部のビルドは、OPENSSLDIR が存在しないパスを参照するため、portless trust の実行時に Can't open openssl.cnf というエラーで失敗することがあります(issue #181)。この問題は v0.9.2 で openssl.cnf の自動検出が実装されて修正されています(PR #183)。古いバージョンを使っている場合は npm install -g portless でアップデートしてください。

「また Mac 専用か」という心配をしなくて済むのは、Windows で開発しているエンジニアにとって地味にありがたいところです。

入れる前に確認しておくこと

4点だけ補足しておきます。

Node.js 24 以上が必要です。 v24 が現在の Active LTS です。node -v で確認してください。バージョンマネージャーを使っているなら .nvmrc.node-version の更新も忘れずに。

Windows では OpenSSL の別途インストールが必要です。 macOS はデフォルトで同梱されており、Linux もほぼすべてのディストリビューションに含まれていますが、Windows は別途用意が必要です。winget install -e --id ShiningLight.OpenSSL.Dev か、Git for Windows に付属の OpenSSL で対応できます。なお一部ビルドで openssl.cnf が見つからないエラーが出る既知の問題があります(詳細は恩恵③のセクションを参照してください)。

pre-1.0 です。 state ファイルのフォーマットが変わる可能性があります。アップデート後に動作がおかしくなったときは portless trust を再実行すると解消することが多いです。

Safari は別途対応が必要な場合があります。 Chrome・Firefox・Edge は .localhost サブドメインを自動で 127.0.0.1 に解決しますが、Safari はシステムの DNS リゾルバーに依存するため、環境によっては解決しないことがあります。Safari をメインブラウザにしている場合は portless proxy start --tld test.test ドメインを使うか、/etc/hosts への追記で対応できます(詳細は公式ドキュメントを参照してください)。

おわりに

ポート競合を解決しようとして入れたツールですが、いちばん効いたのは Cookie とオートコンプリートの分離でした。localhost での開発でじわじわ感じていたモヤモヤが、URL に名前がつくだけでまとめて消えた感覚があります。

npm run portless:dev に変えてから、もう元に戻る気がしません。