[{"data":1,"prerenderedAt":407},["ShallowReactive",2],{"article:stable-named-localhost-urls-with-portless":3,"\u002Fblog\u002Fstable-named-localhost-urls-with-portless-surround":396},{"id":4,"title":5,"author":6,"body":7,"date":385,"description":386,"extension":387,"image":388,"meta":389,"minRead":251,"navigation":247,"path":390,"published":247,"seo":391,"stem":392,"tags":393,"__hash__":395},"blog\u002Fblog\u002Fstable-named-localhost-urls-with-portless.md","Stable .localhost URLs for Every Dev Server (and Every Worktree) with Portless","Baljeet Singh",{"type":8,"value":9,"toc":378},"minimark",[10,19,28,41,46,57,92,95,117,124,132,143,147,150,207,211,214,273,282,286,289,311,330,333,337,371,374],[11,12,13,14,18],"p",{},"I run several pnpm monorepos on my machine, and each one has multiple apps. The Kirtan Player repo has a public player and an admin workbench. The Scoreboard repo has its app. My notation projects have an app, a web frontend, and an admin. On any given day, three or four dev servers are running at once — and until recently, they all lived at ",[15,16,17],"code",{},"localhost:some-number",".",[11,20,21,22,27],{},"You know how this goes. Was the admin on 3001 or 3002? Did the player grab 3000 before the other repo did? And once I started running parallel ",[23,24,26],"a",{"href":25},"\u002Fblog\u002Fautomating-claude-code-worktree-setup-env-node-modules-and-ports","Claude Code worktrees",", the problem multiplied — every worktree needs its own set of ports, and I was maintaining a port-offset hack just to keep them from colliding.",[11,29,30,36,37],{},[23,31,35],{"href":32,"rel":33},"https:\u002F\u002Fportless.sh",[34],"nofollow","Portless",", a small tool from Vercel Labs, made all of that go away. Its tagline says it well: ",[38,39,40],"em",{},"\"Replace port numbers with stable, named .localhost URLs. For humans and agents.\"",[42,43,45],"h2",{"id":44},"one-line-change","One-line change",[11,47,48,49,52,53,56],{},"Portless wraps your dev command. In each app's ",[15,50,51],{},"package.json",", the ",[15,54,55],{},"dev"," script goes from this:",[58,59,64],"pre",{"className":60,"code":61,"language":62,"meta":63,"style":63},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\"dev\": \"nuxt dev\"\n","json","",[15,65,66],{"__ignoreMap":63},[67,68,71,75,78,80,84,86,89],"span",{"class":69,"line":70},"line",1,[67,72,74],{"class":73},"sMK4o","\"",[67,76,55],{"class":77},"sfazB",[67,79,74],{"class":73},[67,81,83],{"class":82},"sTEyZ",": ",[67,85,74],{"class":73},[67,87,88],{"class":77},"nuxt dev",[67,90,91],{"class":73},"\"\n",[11,93,94],{},"to this:",[58,96,98],{"className":60,"code":97,"language":62,"meta":63,"style":63},"\"dev\": \"portless run --name kirtan-player nuxt dev\"\n",[15,99,100],{"__ignoreMap":63},[67,101,102,104,106,108,110,112,115],{"class":69,"line":70},[67,103,74],{"class":73},[67,105,55],{"class":77},[67,107,74],{"class":73},[67,109,83],{"class":82},[67,111,74],{"class":73},[67,113,114],{"class":77},"portless run --name kirtan-player nuxt dev",[67,116,91],{"class":73},[11,118,119,120,123],{},"Now ",[15,121,122],{},"pnpm dev"," prints:",[58,125,130],{"className":126,"code":128,"language":129},[127],"language-text","portless\n\n-- Proxy is running\n-- kirtan-player.localhost (auto-resolves to 127.0.0.1)\n-- Name \"kirtan-player\" (from --name flag)\n-- Using port 4343\n\n  -> https:\u002F\u002Fkirtan-player.localhost\n","text",[15,131,128],{"__ignoreMap":63},[11,133,134,135,138,139,142],{},"The app is at ",[15,136,137],{},"https:\u002F\u002Fkirtan-player.localhost"," — every time, on every machine, no matter what else is running. Install it as a dev dependency (",[15,140,141],{},"pnpm add -D portless",") and you're done.",[42,144,146],{"id":145},"what-you-get-for-free","What you get for free",[11,148,149],{},"A few things happen behind that one line:",[151,152,153,165,183,201],"ul",{},[154,155,156,160,161,164],"li",{},[157,158,159],"strong",{},"No more port management."," Portless picks a free internal port and passes it to your dev server via the ",[15,162,163],{},"PORT"," env variable (Nuxt, Vite, and Next all respect it). Two repos can both think they own \"the dev server\" without ever colliding.",[154,166,167,174,175,178,179,182],{},[157,168,169,170,173],{},"No ",[15,171,172],{},"\u002Fetc\u002Fhosts"," editing."," ",[15,176,177],{},".localhost"," names resolve to loopback by design, and browsers honour that for subdomains too. ",[15,180,181],{},"kirtan-player.localhost"," just works.",[154,184,185,188,189,192,193,196,197,200],{},[157,186,187],{},"Local HTTPS with a trusted certificate."," Portless runs a local proxy with its own CA, so you get ",[15,190,191],{},"https:\u002F\u002F"," URLs in dev. That matters more than it used to — secure cookies, clipboard APIs, and service workers all behave differently on plain ",[15,194,195],{},"http:\u002F\u002Flocalhost",". If your server-side code fetches its own HTTPS URL, portless already sets ",[15,198,199],{},"NODE_EXTRA_CA_CERTS"," so Node trusts the certificate.",[154,202,203,206],{},[157,204,205],{},"A stable name you can share."," The URL is the same in your terminal, your browser bookmarks, your Playwright scripts, and your teammate's machine.",[42,208,210],{"id":209},"the-monorepo-pattern","The monorepo pattern",[11,212,213],{},"With multiple apps per repo, give each a name that reads like what it is:",[58,215,217],{"className":60,"code":216,"language":62,"meta":63,"style":63},"\u002F\u002F apps\u002Fplayer\u002Fpackage.json\n\"dev\": \"portless run --name kirtan-player nuxt dev\"\n\n\u002F\u002F apps\u002Fadmin\u002Fpackage.json\n\"dev\": \"portless run --name kirtan-admin nuxt dev\"\n",[15,218,219,225,242,249,255],{"__ignoreMap":63},[67,220,221],{"class":69,"line":70},[67,222,224],{"class":223},"sHwdD","\u002F\u002F apps\u002Fplayer\u002Fpackage.json\n",[67,226,228,230,232,234,236,238,240],{"class":69,"line":227},2,[67,229,74],{"class":73},[67,231,55],{"class":77},[67,233,74],{"class":73},[67,235,83],{"class":82},[67,237,74],{"class":73},[67,239,114],{"class":77},[67,241,91],{"class":73},[67,243,245],{"class":69,"line":244},3,[67,246,248],{"emptyLinePlaceholder":247},true,"\n",[67,250,252],{"class":69,"line":251},4,[67,253,254],{"class":223},"\u002F\u002F apps\u002Fadmin\u002Fpackage.json\n",[67,256,258,260,262,264,266,268,271],{"class":69,"line":257},5,[67,259,74],{"class":73},[67,261,55],{"class":77},[67,263,74],{"class":73},[67,265,83],{"class":82},[67,267,74],{"class":73},[67,269,270],{"class":77},"portless run --name kirtan-admin nuxt dev",[67,272,91],{"class":73},[11,274,275,277,278,281],{},[15,276,137],{}," and ",[15,279,280],{},"https:\u002F\u002Fkirtan-admin.localhost",". No cheat sheet required.",[42,283,285],{"id":284},"the-worktree-pattern","The worktree pattern",[11,287,288],{},"This is my favourite part. In my notation monorepo, the names are parameterized by an environment variable:",[58,290,292],{"className":60,"code":291,"language":62,"meta":63,"style":63},"\"dev\": \"portless run --name ${NUXT_ENV_NAME:-dev}-app nuxt dev\"\n",[15,293,294],{"__ignoreMap":63},[67,295,296,298,300,302,304,306,309],{"class":69,"line":70},[67,297,74],{"class":73},[67,299,55],{"class":77},[67,301,74],{"class":73},[67,303,83],{"class":82},[67,305,74],{"class":73},[67,307,308],{"class":77},"portless run --name ${NUXT_ENV_NAME:-dev}-app nuxt dev",[67,310,91],{"class":73},[11,312,313,314,317,318,321,322,325,326,329],{},"My ",[23,315,316],{"href":25},"worktree setup automation"," sets ",[15,319,320],{},"NUXT_ENV_NAME"," per worktree. So the main checkout runs at ",[15,323,324],{},"dev-app.localhost",", while a Claude Code worktree working on a feature gets its own ",[15,327,328],{},"feature-x-app.localhost"," — automatically, with zero port juggling.",[11,331,332],{},"That post originally described assigning unique port offsets per worktree. Portless makes that whole section obsolete: names never collide, because the proxy hands out internal ports on its own. If you're running two or three Claude Code agents in parallel worktrees, each one can spin up its own dev server and verify its own work in a browser without stepping on the others — which is exactly the \"for agents\" half of the tagline.",[42,334,336],{"id":335},"small-print","Small print",[151,338,339,350,364],{},[154,340,341,342,345,346,349],{},"The proxy keeps a registry of active routes in ",[15,343,344],{},"~\u002F.portless\u002Froutes.json",", and the first ",[15,347,348],{},"portless run"," starts it on demand. There's no daemon to manage.",[154,351,352,353,355,356,359,360,363],{},"Your dev command runs unchanged — portless only sets ",[15,354,163],{},", ",[15,357,358],{},"HOST",", and a ",[15,361,362],{},"PORTLESS_URL"," env variable, then proxies the named hostname to it.",[154,365,366,367,370],{},"The raw ",[15,368,369],{},"http:\u002F\u002F127.0.0.1:\u003Cport>"," URL still works if a tool insists on it.",[11,372,373],{},"I've rolled this out across every active repo — the notation monorepo, Kirtan Player, and Scoreboard — and dev-server ports are simply not a thing I think about anymore. Highly recommended, especially if AI agents are part of your daily workflow.",[375,376,377],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":63,"searchDepth":227,"depth":227,"links":379},[380,381,382,383,384],{"id":44,"depth":227,"text":45},{"id":145,"depth":227,"text":146},{"id":209,"depth":227,"text":210},{"id":284,"depth":227,"text":285},{"id":335,"depth":227,"text":336},"2026-08-19","How I replaced port numbers with stable, named .localhost URLs across all my monorepos using Portless — including a pattern that gives every Claude Code worktree its own dev URL automatically.","md","\u002Fimg\u002Fportless-localhost-urls.jpg",{},"\u002Fblog\u002Fstable-named-localhost-urls-with-portless",{"title":5,"description":386},"blog\u002Fstable-named-localhost-urls-with-portless",[394],"Web Development","Zo-z7eF3fK3BF6f-77RVYeQ9wxTMPJFLb9w6AjopXbU",[397,402],{"title":398,"path":399,"stem":400,"description":401,"children":-1},"Build an app with Laravel5 (backend) and Angularjs (frontend) – Part 2","\u002Fblog\u002Fseries-build-an-app-with-laravel5-backend-and-angularjs-frontend-part-2-555","blog\u002Fseries-build-an-app-with-laravel5-backend-and-angularjs-frontend-part-2-555","In this part 2 of the series we will take a look at how we can consume the api that we built in part 1",{"title":403,"path":404,"stem":405,"description":406,"children":-1},"Sublime Text color scheme for Intellij Jetbrains (phpstorm\u002F webstorm)","\u002Fblog\u002Fsublime-text-color-scheme-for-intellij-jetbrains-phpstorm-webstorm-235","blog\u002Fsublime-text-color-scheme-for-intellij-jetbrains-phpstorm-webstorm-235","In this article we will take a look at how we can add Sublime Text color scheme inside Jetbrains (phpstorm\u002F webstorm)",1787133605101]