portlessは、http://localhost:3000 のようなポート番号つきURLを https://myapp.localhost という固定の名前付きURLに置き換えるVercel Labs製のCLIだ。GitHubで★11,022を集めているが、日本語の紹介記事はすでに複数あり「便利だった」までは十分に語られている。そこで本記事は、v0.15.5を実際にインストールして、公開情報だけでは分からない3点を実測した——Node.js 24要件が実際どこで効くのか、導入時にマシンへ何が書き込まれるのか、そして --port の自動注入がどのコマンドで効いてどこで効かないのか。

portless v0.15.5 の実測値:Node 22でも起動、ローカルCAは有効期限10年、--port自動注入は8ケース中5ケースで発火、READMEはmain先行で407行差
portless 0.15.5 を実際にインストールして測定した結果(2026-08-21 実測)

この記事は開発サーバー向けリバースプロキシ「portless」を解説します。自動化ツール全体の地図はAI自動化ツール|ノーコードからコードまで2026年版の比較と選び方をご覧ください。

30秒でわかる portless

portless myapp next dev を実行すると、開発サーバーが https://myapp.localhost で開く。ポート番号は4000-4999からランダムに割り当てられ、開発者は一切意識しない
・git worktreeを検出してブランチ名をサブドメインに付ける(fix-ui.myapp.localhost)ので、複数ブランチの同時起動でポートが衝突しない
・HTTPSが既定。初回起動時にローカルCAを生成してOSの信頼ストアに登録するため、「マシンに何が入るか」を理解してから入れるべきツールでもある
・Apache-2.0/実行時依存ゼロ/Node.js 24以上を要求(ただし実測では22でも動いた)

この記事のポイント

Node.js 24要件は「インストール時には効かない」。22.13.1でもnpm installは警告のみで成功し、HTTPモードの実行まで通った。止まるのはdoctorの自己診断だけ
--portの自動注入は、渡し方によって効いたり効かなかったりする。v0.15.5ではportless run viteは注入されるが、package.jsonのdevスクリプト経由では注入されない
GitHubのREADMEはnpm公開版より先行している。READMEが説明する挙動の一部は0.15.5に存在しない

portlessとは——ポート番号を名前付きURLに置き換えるリバースプロキシ

portlessの仕組みはシンプルで、ローカルに常駐するリバースプロキシが1つあり、そこへ各アプリが「この名前で登録してほしい」と申告する構造になっている。ブラウザは常にプロキシ(既定では443番ポート)へ接続し、プロキシがHostヘッダを見て転送先の開発サーバーを選ぶ。

flowchart LR B["ブラウザ
myapp.localhost"] --> P["portless proxy
443番ポートで待機"] B2["ブラウザ
api.myapp.localhost"] --> P P -->|"Hostヘッダで振り分け"| A1["開発サーバー :4375
myapp"] P -->|"Hostヘッダで振り分け"| A2["開発サーバー :4001
api"] R["routes.json
~/.portless/"] -.->|"登録内容を参照"| P

ポート番号が消える仕掛けは、開発サーバー側のポートをportlessが決めることにある。portless run はアプリを起動する前に4000-4999の範囲から空きポートを選び、PORT 環境変数として子プロセスに渡す。実際に手元で観測した割り当ては 4375・4652・4001・4585・4391・4813・4143・4317・4160 などで、毎回変わっていた。開発者はこの数字を知る必要がない——ブラウザで開くのは常に myapp.localhost だからだ。

実行時に何が起きているかは、portless自身が出力する。次は実際に手元で portless run を実行したときの出力で、割り当てポートと子プロセスへ渡される環境変数がそのまま見える。

portless

Starting proxy...
HTTP proxy started on port 1355
-- n22probe.localhost (auto-resolves to 127.0.0.1)
-- Name "n22probe" (from package.json)
-- Using port 4375

  -> http://n22probe.localhost:1355

Running: PORT=4375 HOST=127.0.0.1 PORTLESS_URL=http://n22probe.localhost:1355 node -e ...

.localhost というTLDが使われているのは、Chrome・Firefox・Edgeがこのドメインを問答無用で127.0.0.1に解決するためだ。DNS設定も/etc/hostsの編集も要らない。ただしSafariはOSのリゾルバに従うため、環境によっては解決できない。その場合の逃げ道としてportless hosts syncが用意されており、登録済みルートを/etc/hostsへ書き出す。

読者の3問への答え

何ができるのか:ローカル開発サーバーに、ポート番号を含まない固定URLを与える。サブドメインで複数サービスを並べられる(api.myapp.localhost
何を解決するのか:ポート衝突(EADDRINUSE)、どのアプリが何番かを覚える負担、ポート使い回しによる「リロードしたら別アプリが出る」事故、cookieやlocalStorageがlocalhost上で混ざる問題
何を代替するのかmkcert+nginx/Caddyでローカル用のドメインとHTTPSを自前構築していた手順一式を、CLI 1本に畳む。Docker Composeを使っているならTraefikのラベル駆動ルーティングが近い立ち位置にある

モノレポでは命名規則が自動で決まる

モノレポでの挙動も押さえておきたい。portlessはリポジトリ直下のportless.jsonを1つ置くだけで、pnpm-workspace.yamlまたはpackage.jsonworkspacesフィールドからワークスペースを検出する。リポジトリのルートでportlessを実行すると、devスクリプトを持つ全パッケージが一斉に起動する。

名前を明示しない場合のホスト名は<パッケージ名>.<プロジェクト名>.localhostという規則に従う。プロジェクト名はワークスペース全体で最も多く使われているnpmスコープから決まる仕組みで、@myorg/web@myorg/apiがあればmyorgが採用され、スコープが無ければワークスペースのルートディレクトリ名にフォールバックする。パッケージの短縮名がプロジェクト名と一致する場合は重複を避けて<プロジェクト名>.localhostになる。個別に名前を変えたいときだけappsマップで上書きすればよく、列挙しなかったパッケージも自動検出の対象から外れるわけではない。

turborepoと併用する場合は少し書き方が変わる。devスクリプトにportlessを置き、本来のコマンドを別スクリプト(例dev:app)へ逃がす形にする。turboが各パッケージのdevを叩き、それがportlessを呼び、portlessが設定を読んでパッケージマネージャ経由でdev:appをプロキシ配下で実行する。turbo.jsonの変更は不要で、portlessを入れていないメンバーはpnpm run dev:appを直接叩けば従来どおり動く。

リポジトリはVercel Labs配下だが、Next.js専用ツールではない。PORT環境変数を尊重するフレームワーク(Next.js・Express・Nuxtなど)はそのまま動き、無視するフレームワーク(Vite・Astroなど)にはフラグを注入して対応する。Vercelというプラットフォーム自体の全体像はVercelとは?Next.js最適化サーバーレスからAI SDK・BotID・AI Gatewayまで2026年最新ガイドにまとめてある。

インストールと基本コマンド——Node.js 24要件を実測で確かめる

READMEには Requirements: Node.js 24+ と書かれている。手元の常用環境はNode.js 22.13.1だったので、まず「24未満だと本当に動かないのか」を測った。結論から言うと、止まる場所は1か所だけだった。

インストールはグローバルが推奨だが、検証では環境を汚さないようローカルのprefixへ入れた。

# インストール(推奨はグローバル)
npm install -g portless

# 動作確認
portless --version
portless doctor

Node.js 22.13.1でのインストール結果は次のとおり。警告は出るが、終了コードは0で成功している

npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   package: '[email protected]',
npm warn EBADENGINE   required: { node: '>=24' },
npm warn EBADENGINE   current: { node: 'v22.13.1', npm: '11.1.0' }
npm warn EBADENGINE }

added 1 package in 644ms

これはnpmの仕様どおりの挙動だ。enginesフィールドはengine-strictを設定していない限り助言的で、インストールを止めない。つまり「Node 24が必要」と書かれていても、入れる段階では弾かれない。

続けて段階ごとに測ると、次のように分かれた。

実行内容 Node.js 22.13.1での結果
npm install [email protected] 成功(EBADENGINE警告のみ、終了コード0)
portless --version 0.15.5 を出力。正常
portless --help ヘルプ全文を出力。正常
portless doctor 実行できるが fail Node.js 22.13.1 is unsupported. を報告
portless run(HTTPモード) 正常動作。プロキシ起動・ポート割当・子プロセス起動まで完走
portless run(HTTPSモード) CA生成後、信頼ストア登録の段階で停止

--version--helpが通るのは当然で、これらは引数を見て即座に返すだけだから実装の大半に触れない。バージョン要件の検証で「--helpが動いたから大丈夫」と判断すると必ず間違う。重要なのは実際にプロキシを起動して子プロセスを走らせるところまで通ったという点だ。

portless doctorはv0.15.0で追加された読み取り専用の診断コマンドで、状態を変更せずに環境を点検する。Node 22での出力は以下のとおりで、不合格の理由と対処が明示される。

portless doctor

Version: 0.15.5
Node.js: 22.13.1
Platform: darwin arm64
State dir: /Users/<user>/.portless
Proxy target: https://127.0.0.1
Mode: HTTPS, .localhost

fail  Node.js 22.13.1 is unsupported.
      Install Node.js 24 or newer.
info  State directory has not been created yet: /Users/<user>/.portless
warn  Proxy is not running on port 443.
      Run: portless proxy start
ok    OpenSSL is available for certificate generation.
info  Local CA has not been generated yet.
info  No routes are registered.

Summary: 1 failure, 1 warning.

「動いた」と「サポートされている」は別物

Node.js 22で完走したのは、あくまでHTTPモードの単純なケースだ。doctorが明示的に不合格を出している以上、サポート対象外の構成であることに変わりはなく、TLSや証明書まわりで24以降のAPIに依存する経路が今後増える可能性もある。常用するなら24以上を入れるべきで、ここで示したのは「要件表の数字が、どの段階で、どう効くか」を正確に把握するための実測である。

主要なコマンドは次のように整理できる。portless listで現在のルート、portless pruneでクラッシュしたセッションの残骸を掃除する、という運用系が一通り揃っている。

コマンド 役割
portless package.jsonのdevスクリプトをプロキシ経由で実行
portless <name> <cmd> https://<name>.localhost でアプリを起動
portless run [cmd] 名前をプロジェクトから推測して実行
portless alias <name> <port> 既存のポートに静的ルートを割り当て(Docker等)
portless list 有効なルートの一覧
portless doctor プロキシ・ルート・DNS・CA信頼状態の診断
portless trust ローカルCAをOSの信頼ストアへ登録
portless clean 状態・CA登録・hostsブロックをまとめて撤去
portless prune 落ちたセッションが残した開発サーバーを終了
PORTLESS=0 <cmd> portlessを迂回して素の状態で実行

rungetaliashostslistdoctortrustcleanpruneproxyservice はサブコマンドとして予約されており、アプリ名には使えない。この名前を使いたい場合は portless --name <name> <cmd> で明示する。

portlessがマシンに入れるもの——ローカルCA・信頼ストア・/etc/hosts

portlessは既定でHTTPSを有効にする。ブラウザ警告を出さずにHTTPSを成立させるには、マシンが信頼する認証局から証明書を発行するしかない。つまりportlessは自前のCAを作り、それをOSの信頼ストアに入れる。これはmkcertと同じ発想だが、導入前に把握しておくべき部分なので実物を確認した。

HTTPSモードで起動すると、~/.portless/ に次のファイルが生成された。

-rw-------  ca-key.pem      # CAの秘密鍵(パーミッション600)
-rw-r--r--  ca.pem          # CA証明書
-rw-------  server-key.pem  # サーバー証明書の秘密鍵(600)
-rw-r--r--  server.pem      # サーバー証明書
-rw-r--r--  routes.json     # 登録済みルート

秘密鍵はいずれも 600(所有者のみ読み書き)で作られていた。生成されたCA証明書の中身をopensslで確認すると次のようになる。

Subject / IssuerCN=portless Local CA(自己署名)
有効期間:2026-08-20 から 2036-08-17 まで(10年
Basic ConstraintsCA:TRUE(critical)
Key UsageCertificate Sign, CRL Sign(critical)
:256bitの楕円曲線鍵

サーバー証明書のほうは CN=localhost、SANに DNS:localhost, DNS:*.localhost, DNS:*.local を持ち、有効期間は1年だった。*.local が入っているのはLANモード(mDNSで実機に配信する機能)のためだ。

ここで実測して分かった重要な挙動がある。Node 22でHTTPSモードを起動しようとして「Failed to start proxy.」で失敗したにもかかわらず、CA証明書はすでにログインキーチェーンに登録されていた

# 失敗した実行のあとで確認
security find-certificate -a -c "portless" ~/Library/Keychains/login.keychain-db
#   "alis"<blob>="portless Local CA"   ← 登録されている

ただし、登録されただけで信頼はされていなかった。macOSの信頼設定を確認すると設定は空で、証明書の検証も失敗する。

security dump-trust-settings
# SecTrustSettingsCopyCertificates: No Trust Settings were found.

security verify-cert -c portless_ca.der
# Cert Verify Result: CSSMERR_TP_NOT_TRUSTED

つまり「キーチェーンに入っている」と「信頼されている」は別段階で、信頼設定の付与には対話的な認証が要る。非対話環境ではそこまで到達せず、証明書だけが宙ぶらりんで残る。これがportless trustという独立したコマンドが用意されている理由でもある(初回に信頼のプロンプトをスキップした場合、後から実行して信頼を付与する)。

撤去も実測した:portless clean は入れたものを戻せる

導入を検討するうえで「元に戻せるか」は判断材料になる。実測では portless clean が期待どおりに機能した。実行後、ログインキーチェーンから portless Local CA は消え(検索ヒット数0)、~/.portless/ の中身も空になった。出力は「Removed local CA from the system trust store.」「Removed portless state files…」「Removed portless entries from /etc/hosts.」と、撤去した対象を明示する。なお--cert/--keyで自前の証明書を渡していた場合、そのファイルは削除されない。

/etc/hosts については、.localhost だけを使っている限り書き込まれなかった。今回の一連の検証を通して/etc/hostsにportlessのブロックは現れていない。.localhostはブラウザ側が解決するので書く必要がないためで、書き込みが発生するのはカスタムTLD(.testなど)やLANモードの.local、あるいはSafari対策で明示的にportless hosts syncを実行したときだ。

ネットワーク的な露出範囲も確認しておく価値がある。CHANGELOGによればv0.15.4でプロキシのバインド先が明示的にループバックのみ(127.0.0.1::1)に限定された。LANモード以外では、LAN・VPN・その他のインターフェース経由では到達できない。ローカル開発ツールをネットワークに晒さないという意味で妥当な既定値だ。プロキシ型のツールが通信経路に入る構成という点では、sql-tapとは|アプリを無改修でSQLトラフィックをリアルタイムに覗くGo製プロキシ型TUI/Web監視ツールと同じ設計思想の系譜にある。

portlessの--port自動注入はどこまで効くか——8ケースの実測マトリクス

portlessは割り当てたポートをPORT環境変数で子プロセスに渡す。Next.jsやExpressはこれを読むので何もしなくていい。問題はPORTを無視するフレームワーク(Vite・Astro・React Router・Angular・Expoなど)で、これらにはportlessが--portフラグを注入する。

この注入がどこまで効くのかを、引数をそのまま報告する偽のフレームワークバイナリをPATHに置いて実測した。実際のViteを入れる必要がなく、注入されたフラグを正確に観測できる。まず陽性対照として素のviteで注入が起きることを確認し、そのうえで各ケースを比較している(対照が発火しなければ、他の結果は「注入されなかった」のか「そもそも認識されていない」のか区別できない)。

結果は次のとおり。v0.15.5でコマンドを直接渡した場合の挙動である。

実行したコマンド 子プロセスが受け取った引数 注入
portless run vite --port 4704 --strictPort --host 127.0.0.1 ○(陽性対照)
portless run vite build build --port 4104 --strictPort --host 127.0.0.1
portless run vite preview preview --port 4905 --strictPort --host 127.0.0.1
portless run vite optimize optimize --port 4747 --strictPort --host 127.0.0.1
portless run vite --port 9999 --port 9999 --host 127.0.0.1 △(既存の--portを尊重)
portless run astro dev dev --port 4788 --host 127.0.0.1 ○(--strictPortなし)
portless run next dev dev ×(PORT環境変数で足りる)
portless run next build build ×
portless v0.15.5 の --port 自動注入 実測比較:注入されなかったのはpackage.jsonスクリプト経由・next dev・既存--port指定済み。注入されたのはvite・vite build/preview/optimize・astro dev
偽のフレームワークバイナリをPATHに置き、子プロセスが実際に受け取った引数を観測した

読み取れることが3つある。

1つ目:既に--port 9999が指定されている場合、portlessは上書きしない。--hostだけを足す。ユーザー指定を尊重する設計になっている。

2つ目:Next.jsには何も注入されない。PORT環境変数を読むフレームワークとして扱われているためで、next buildのようなサーバーを立てないコマンドにも余計なフラグが付かない。

3つ目——ここが実務上いちばん効く——v0.15.5はvite buildvite optimizeにも--portを注入する。これらはサーバーを起動しないコマンドで、--portを受け取ると通常はエラーになる。実際にmainブランチのREADMEは、この点について次のように明記している。

a command that does not serve, such as vite build, vite optimize, vp test or astro check, rejects them and is left alone.

つまり「サーバーを起動しないコマンドにはフラグを付けない」と書かれている。しかしnpmで配布されている0.15.5では、その判定が存在しない。ソースを確認すると、v0.15.5のinjectFrameworkFlagsはサブコマンドを一切見ずに--portを追加するだけの実装だった。この差の正体は次のセクションで扱う。

さらにもう1つ、注入がまったく効かない経路がある。package.jsonのdevスクリプト経由だ。"dev": "vite" を書いてportlessを単体実行すると、v0.15.5では次のようになった。

Running: PORT=4652 HOST=127.0.0.1 PORTLESS_URL=http://control.localhost:1355 npm run dev
PROBE_ARGV=[]

npm run dev がそのまま実行され、--portは一切付かないPORT環境変数は設定されているが、Viteはそれを読まない。結果として、Viteは自分の既定ポート(5173)で起動し、portlessは別のポート(4652)へ転送しようとする——画面は開かない

0.15.5でViteを使うなら、コマンドを直接渡す

package.jsonに "dev": "vite" と書いて portless を単体実行する形は、0.15.5では機能しない。portless run vite のようにコマンドを直接渡すか、スクリプト側で自分でポートを指定する必要がある。mainブランチにはスクリプト経由の注入機構(injectPackageScriptFrameworkFlags)が実装済みなので、次のリリースで解消される見込みだ。

READMEとnpm公開版のズレ——GitHubで読める説明は先行している

前のセクションで観測した2つのズレ——vite buildへの注入と、スクリプト経由の注入なし——は、どちらも同じ原因に行き着く。GitHubのREADMEが説明しているのはmainブランチの挙動で、npmで配布されている最新版はそれより後ろにいる

実際に両者のソースを取得して比較した結果が次の表だ。

比較対象 v0.15.5タグ(npm公開版) mainブランチ(READMEが説明)
cli-utils.ts の行数 1,188行 1,595行
injectPackageScriptFrameworkFlags 存在しない(0件) 実装済み
isSafeToInjectIntoScript 存在しない(0件) 実装済み
フレームワーク定義 { strictPort: boolean } のみ サブコマンド分類を含む構造体
サーバー系サブコマンドの判定 なし serverSubcommands/nonServerSubcommands
README のバイト数 24,986 26,048

公平を期すために付け加えると、これはドキュメントの誤りではない。v0.15.5タグのREADMEを確認したところ、「vite buildは対象外」といった記述はそもそも含まれていなかった(該当表現の出現数0)。つまりリリースごとにコードとREADMEは整合している。問題は、利用者が読むのはほぼ常にmainブランチのREADME(GitHubのトップページに表示されるもの)であり、npm installで手に入るのは最新リリースだという構造にある。

npmの公開履歴を見ると、0.15.5の公開は2026-07-30。一方リポジトリへの最終push は2026-08-19で、3週間分の差分が未リリースのまま蓄積している。プレ1.0のプロジェクトでは珍しくない状態だが、READMEの記述が手元の挙動と食い違ったときは、まずこのズレを疑うのが早い。

flowchart TD M["mainブランチ
cli-utils.ts 1,595行"] -->|"GitHubトップに表示"| R["README
スクリプト注入を説明"] T["v0.15.5タグ
cli-utils.ts 1,188行"] -->|"npm publish 2026-07-30"| N["npm install portless
利用者の手元"] R -.->|"利用者はこれを読む"| U["期待する挙動"] N -.->|"実際に動くのはこれ"| A["実際の挙動"] U -->|"ズレ"| A

なお、リポジトリ自身は開発が活発だ。npmには40バージョンが公開されており、CHANGELOGの直近を見ると 0.15.5 で多段TLD対応とHTTP/2上のWebSocket(RFC 8441の拡張CONNECT)対応、0.15.4 でループバック限定バインド、0.15.3 でsudo実行時の状態ディレクトリ解決の修正、と実務的な修正が続いている。バージョン間の差分を確認する習慣は、プレ1.0のツールを扱ううえではGit 2.54の新機能まとめ:git historyコマンドと設定ベースフックで開発ワークフローが変わるで触れたようなバージョン追跡の作法と地続きだ。

OAuth・カスタムTLDとエージェント向けスキルの同梱

portlessのリポジトリで見落とされがちなのが、skills/ ディレクトリにエージェント向けのスキルファイルが2本入っている点だ。README冒頭の「For humans and agents」はキャッチコピーではなく、実体を伴っている。

skills/portless/SKILL.md(28,921バイト)——portlessの導入・設定・トラブルシュート手順
skills/oauth/SKILL.md(7,465バイト)——OAuthプロバイダをportlessのローカルURLで動かす手順

前者には、人間向けREADMEには書かれていない運用指示が含まれる。たとえば実行方法について「npxpnpm dlx でのワンショット実行は使うな」と明示している。プロキシが常駐する性質のツールなので、都度ダウンロードして実行する形と相性が悪いという判断だろう。

後者のOAuthスキルは、実務上の価値が高い情報を表にまとめている。.localhost はOAuthのリダイレクトURIとして多くのプロバイダに拒否されるという問題への対処だ。

プロバイダ localhost .localhost のサブドメイン 理由
Google 許可 拒否 同梱のPublic Suffix Listに含まれない
Apple 拒否 拒否 localhost自体を許可しない
Microsoft 許可 許可 localhostの扱いが寛容
Facebook 許可 場合による URIを個別に登録する必要がある
GitHub 許可 許可 寛容

GoogleとAppleが厳しい。この対策としてportlessが用意しているのが --tld で、任意のDNS名をTLDとして使える。自分が所有するドメインを指定すれば、ローカル開発URLが本番と同じ構造になる。

# 所有しているドメインをローカル開発のTLDとして使う
portless proxy start --tld dev.example.com
portless myapp next dev
# -> https://myapp.dev.example.com

こうすると https://myapp.dev.example.com/api/auth/callback/google をリダイレクトURIとして登録でき、GoogleやAppleの検証を通せる。cookieのサブドメイン共有やHostベースのルーティングも本番と同じ挙動になるため、環境差に起因するバグを減らせる。プロキシは登録済みホスト名を/etc/hostsへ自動同期するので、名前解決も手当てされる。ただしこの構成はループバック限定で、他の端末からは到達できない(LANモードは.local固定でカスタムTLDと併用できない)。

同梱ドキュメント間で推奨が食い違っている箇所がある

READMEは「.dev は避けろ(Googleが所有し、HSTSでHTTPSを強制する)」と明記し、推奨として.test(IANA予約済み)を挙げている。一方でOAuthスキルの最初の例は portless proxy start --tld dev と、まさにその.devを使っている。スキル側も直後の段落で「myapp.devは実在ドメインと衝突しうるので、自分が所有するドメインを多段TLDとして使え」と軌道修正しているため実害は小さいが、最初のコード例だけを模倣すると推奨から外れる。自分が所有するドメインを使う形が最も安全だ。

エージェントとの相性という文脈では、そもそもの動機が「AIエージェントがポートを推測して間違える」問題にある。固定URLならエージェントに渡す情報が決定的になる。加えてgit worktreeの自動検出——リンクされたworktreeではブランチ名がサブドメインとして前置され、fix-ui.myapp.localhost になる——により、ブランチごとに並行してエージェントを走らせても、それぞれが自分のURLを持つ。設定変更は不要で、portless run をpackage.jsonに一度書けば全worktreeで機能する。

類似ツールとの比較——portlessを選ぶ基準

ローカル開発でHTTPSと名前付きドメインを得る手段は以前から存在する。portlessが埋めているのは「セットアップの手数」の部分だ。

手段 名前付きURL HTTPS ポート自動割当 前提
portless ○(.localhost/任意TLD) ○(CA自動生成・自動信頼) ○(4000-4999) Node.js 24+
mkcert + nginx/Caddy ○(要hosts編集) ○(CAはmkcertが生成) ×(自分で管理) 各コンポーネントの設定
Traefik(Docker) ○(ラベル駆動) ○(設定次第) ○(コンテナ前提) Docker Compose
ngrok / Tailscale ○(外部ドメイン) アカウント・CLI
素のlocalhost:3000 × ×(要手動設定) × なし

選択の目安は次のように整理できる。

Node.jsのフロントエンド開発でworktreeやモノレポを並行で回すなら、portlessが最も手数が少ない。設定ファイルなしで動き、worktree対応が組み込みである点は他にない
Docker Composeで全サービスを動かしているなら、Traefikのほうが素直だ。コンテナのラベルでルーティングが完結し、Node.jsへの依存も増えない。portlessもportless alias <name> <port>で既存ポートに静的ルートを張れるので併用は可能
外部に見せたい(モバイル実機確認・Webhook受信・チーム共有)なら、ngrokやTailscaleが本命。portlessは--ngrok--tailscale--funnelフラグでこれらを内蔵しており、ローカルURLと公開URLを同時に持てる
Node.js 24を入れられない環境なら、mkcert+リバースプロキシの従来構成のほうが安全

導入時のつまずきどころも押さえておきたい。フロントエンドの開発サーバーが別のportlessアプリへAPIをプロキシする構成では、Hostヘッダを書き換えないと無限ループになる。ViteならchangeOrigin: trueを指定する。portless側はこの誤設定を検出して508 Loop Detectedを返し、対処法を示すようになっている。

もう1点、プロジェクトのdevDependencyとして入れると、コントリビュータごとにバージョンが分かれる。portlessはプレ1.0で状態ディレクトリの形式が変わることがあり、その場合portless trustのやり直しが必要になる。README自身が注意しているとおり、グローバルインストールのほうが運用は安定する。

実測のまとめ

・Node.js 24要件はインストールを止めない。22.13.1でも警告のみで入り、HTTPモードでの実行まで通った。明示的に止まるのはdoctorの診断のみ
・HTTPS既定のため、10年有効の自己署名CAがOSの信頼ストアに入る。撤去はportless cleanで可能(実測でキーチェーンから消えることを確認)
--portの自動注入は渡し方で挙動が変わる。0.15.5ではportless run viteは注入されるが、package.jsonスクリプト経由では注入されない
GitHubのREADMEはnpm最新版より先行している(cli-utils.tsで407行差)。挙動が説明と違うときはまずここを疑う

参照ソース