Sustituye los números de puerto por URLs con nombre en desarrollo
Sustituye los puertos de localhost por URLs con nombre usando .localhost, un proxy inverso o portless para evitar conflictos, cookies y pestañas equivocadas.
Un nombre de dominio de localhost es un hostname legible por humanos, como app.localhost, que se resuelve a 127.0.0.1, lo que permite que cada servicio local mantenga una dirección estable en lugar de un número de puerto cambiante.
Probablemente hayas vivido ese momento en el que tienes tres servidores de desarrollo en marcha, vuelves a la pestaña de localhost:3000 para comprobar una corrección y la aplicación que te devuelve la mirada es el proyecto de ayer. Cambiar localhost:3000 por app.localhost resuelve de golpe un conjunto de molestias diarias (conflictos de puertos, URLs que cambian, filtración de cookies y el problema de la «pestaña equivocada»), porque cada aplicación obtiene su propio hostname y, con él, su propio ámbito aislado en el navegador. Este artículo cubre tres formas de conseguirlo: el TLD .localhost integrado, un reverse proxy hecho por ti mismo y portless, un proxy local creado específicamente para esto por Vercel Labs.
Puntos clave
- El TLD
.localhostestá reservado para uso de loopback por RFC 6761, por lo que cualquier nombre bajo él se resuelve a127.0.0.1en Chrome, Firefox y Edge sin necesidad de una entrada en el archivo hosts. Safari, que delega en el resolver del sistema, puede seguir necesitándola. - Como los navegadores delimitan el ámbito de las cookies por host e ignoran el puerto,
app.localhostyapi.localhostpermanecen separadas, mientras quelocalhost:3000ylocalhost:3001comparten el mismo almacén de cookies. - El TLD
.localhostpor sí solo no elimina el puerto; tu aplicación sigue escuchando en uno, así que necesitas un reverse proxy que asigne el hostname a ese puerto. - portless (Vercel Labs, todavía pre-1.0) asigna a cada aplicación un puerto efímero en el rango 4000–4999 mediante la variable de entorno
PORTy enruta hacia él una URL establename.localhost, con HTTPS y HTTP/2 activados por defecto. - Una URL con nombre estable registrada en un archivo de agentes permite que las herramientas de programación con IA apunten al servicio correcto en lugar de adivinar entre el puerto 3001 y el 8080.
¿Por qué las URLs con nombre son mejores que los números de puerto?
El desarrollo local basado en puertos falla de formas predecibles en cuanto ejecutas más de un servicio. Arranca una segunda aplicación en un puerto ocupado y Node lanza EADDRINUSE. Los frameworks que autoincrementan el puerto evitan el fallo, pero introducen deriva: hoy tu blog está en localhost:3001 y mañana en localhost:3002, así que los marcadores se estropean y el historial del navegador para localhost:3000 se convierte en un montón inmanejable de proyectos sin relación. Mata un servidor, arranca otro en el puerto liberado y una pestaña que dejaste abierta servirá silenciosamente el otro proyecto: el problema de la «pestaña equivocada».
El fallo más sutil es la filtración de estado. Los navegadores delimitan el ámbito de las cookies por host y hacen caso omiso del puerto, así que localhost:3000 y localhost:3001 escriben en el mismo almacén de cookies. El estado de sesión de una aplicación se filtra a otra. Los subdominios con nombre resuelven esto a nivel de origen: app.localhost y api.localhost son hostnames distintos, por lo que separan limpiamente las cookies y, como la política del mismo origen se basa en el esquema, el host y el puerto, también separan localStorage y sessionStorage. La guía de Microsoft sobre el TLD señala lo mismo: dar a cada aplicación local su propio nombre mantiene separados los recursos delimitados por nombre, como las cookies, y el nombre en la barra de direcciones te dice de un vistazo qué aplicación estás viendo.
¿Qué es el TLD .localhost?
Discover how at OpenReplay.com.
El mecanismo de URLs con nombre más simple ya viene con tu navegador. RFC 6761 reserva el TLD .localhost, y todos los nombres bajo él, para la dirección de loopback, y por eso app.localhost responde en 127.0.0.1 sin ninguna configuración. Chrome, Firefox y Edge gestionan internamente esa resolución, asignando cualquier nombre *.localhost a 127.0.0.1 o ::1, de modo que ese nombre actúa como un alias de lo que ya esté sirviéndose en localhost. Safari es el que hay que vigilar: en su lugar pasa el nombre al resolver DNS del sistema, y no todas las configuraciones de resolver responden a los subdominios .localhost, así que ahí puede que necesites una entrada en /etc/hosts.
Hay un pero: el TLD por sí solo no elimina el puerto. Tu aplicación sigue escuchando en :3000, y app.localhost sin puerto simplemente apunta a app.localhost:80, donde no hay nada escuchando. Para eliminar realmente el número necesitas un reverse proxy en el puerto 80 o 443 que lea la cabecera Host y reenvíe al puerto real de la aplicación.
Hazlo tú mismo: archivo hosts más un reverse proxy
Puedes montar URLs con nombre a partir de piezas que ya conoces. Añade un hostname a /etc/hosts (o confía en la autorresolución de .localhost) y luego ejecuta un reverse proxy que asigne el nombre al puerto de tu servidor de desarrollo. Una configuración de Caddy es prácticamente lo más conciso posible:
app.localhost {
reverse_proxy localhost:3000
}
api.localhost {
reverse_proxy localhost:8080
}
Caddy provisiona certificados TLS locales automáticamente; nginx y Traefik hacen lo mismo con más configuración. Para dominios locales con comodín, dnsmasq puede resolver todo un espacio *.test a 127.0.0.1, de modo que te ahorras las entradas de hosts nombre por nombre. Y cada servidor de desarrollo sigue necesitando su host y puerto fijados (Vite mediante server.host y server.port, webpack mediante devServer) para que el proxy tenga un destino estable.
La contrapartida es el mantenimiento. Tú mantienes la configuración del proxy, la confianza de los certificados, las entradas de hosts y las asignaciones de puertos por proyecto, y mantienes las cuatro cosas sincronizadas a mano a medida que aparecen y desaparecen servicios. Para una o dos aplicaciones de larga vida está bien. En un monorepo se convierte en una tarea en sí misma.
portless: URLs con nombre que simplemente funcionan
portless es un proxy local que automatiza toda la cadena. Añades un prefijo a tu comando de desarrollo, así que next dev pasa a ser portless run next dev, o ejecutas portless a secas y dejas que infiera el nombre de la aplicación a partir de package.json, la raíz de git o el directorio. El proxy se autoinicia, asigna un puerto libre en el rango 4000–4999, lo inyecta mediante la variable de entorno PORT y enruta https://name.localhost hacia él. A los frameworks que ignoran PORT, como Vite, Astro, Angular y Expo, se les pasa el flag --port correcto en su lugar, además de un flag --host correspondiente cuando es necesario.
En las versiones 0.15.x, portless habilita HTTPS con HTTP/2 por defecto en el puerto 443, generando una autoridad de certificación local y estableciendo su confianza en la primera ejecución. Se autoeleva con sudo en macOS y Linux porque enlazar el 443 requiere root, y portless trust vuelve a añadir la CA si te saltaste el aviso. Los artículos anteriores que muestran un flag --https opcional y un :1355 por defecto describen una versión ya superada. HTTP/2 ayuda en local por una razón concreta: un navegador solo mantendrá abiertas seis conexiones HTTP/1.1 a un mismo host, así que un servidor de desarrollo que entrega cientos de archivos sueltos sin empaquetar acaba encolándolos, mientras que una sola conexión HTTP/2 los transporta todos a la vez. portless requiere Node.js 24 o superior.
Algunas funciones se justifican solas en configuraciones más grandes. Subdominios como api.myapp.localhost organizan microservicios; un único portless.json en la raíz de un monorepo autodescubre los paquetes del workspace. Para un servicio con puerto fijo que no puedes cambiar, como un contenedor Docker, portless alias <name> <port> le asigna una URL con nombre, y PORTLESS=0 omite el proxy por completo para CI o una prueba rápida. Si quieres un TLD personalizado, portless recomienda .test, que RFC 6761 también reserva, y advierte contra otros dos: .local choca con mDNS y Bonjour, mientras que .dev pertenece a Google, que lo fuerza a HTTPS mediante HSTS.
Por qué las URLs locales estables importan para los agentes de programación con IA
Los agentes de programación con IA fallan en lo mismo que los humanos con los puertos, solo que en silencio: codifican a fuego un número que vieron antes en el contexto o lo adivinan mal. Un agente que lee un https://api.myapp.localhost fijo desde un archivo AGENTS.md apunta siempre al servicio correcto, en lugar de alternar entre 3001 y 8080 entre sesiones e interrumpirte para preguntar. Se trata de un cambio general en el tooling de desarrollo: los endpoints estables son infraestructura para la automatización. portless incluye archivos de skills, y las versiones 0.15.x añaden páginas de documentación en Markdown y un índice llms.txt, para que sus URLs sean descubribles por los agentes desde el primer momento.
Cómo elegir un enfoque
Solo TLD .localhost | TLD + reverse proxy | portless | |
|---|---|---|---|
| ¿Elimina el puerto? | No | Sí | Sí |
| Herramientas adicionales | Ninguna | Caddy/nginx/Traefik | Una instalación global |
| HTTPS | Manual | Provisto por el proxy | Activado por defecto |
| Autodescubrimiento en monorepos | No | No | Sí |
| Compatible con agentes | Parcial | Parcial | Sí (archivos de skills, llms.txt) |
| Fricción de configuración | La más baja | Media (sincronización manual) | Baja |
La decisión en una línea: opta por el TLD integrado más un reverse proxy si no quieres herramientas nuevas y no te importa mantener configuración; opta por portless si quieres que las URLs con nombre simplemente funcionen en muchos servicios, en un monorepo o con agentes de IA.
Las URLs locales con nombre, estables y legibles por humanos son estrictamente mejores que los números de puerto, y puedes adoptarlas en minutos: añade hoy un Caddyfile de dos líneas, o pon el prefijo portless a un script de desarrollo y no vuelvas a pensar en EADDRINUSE.
Preguntas frecuentes
¿Necesito añadir los subdominios .localhost a mi archivo /etc/hosts?
No, no en Chrome, Firefox ni Edge. Esos tres resuelven por sí mismos cualquier nombre bajo el TLD .localhost a 127.0.0.1, porque RFC 6761 reserva el TLD para uso de loopback, así que app.localhost y api.localhost funcionan sin ninguna configuración. Safari es la excepción, porque delega la resolución al resolver DNS del sistema, y no todas las configuraciones de resolver responden a los subdominios .localhost. Añade ahí una entrada en /etc/hosts si algún nombre no carga.
¿Usar un dominio .localhost elimina el número de puerto de mi servidor de desarrollo?
No. El TLD .localhost solo resuelve el hostname a 127.0.0.1; tu aplicación sigue escuchando en su puerto original, así que app.localhost sin puerto apunta a app.localhost:80, donde no hay nada en ejecución. Para eliminar realmente el número necesitas un reverse proxy en el puerto 80 o 443 que lea la cabecera Host y reenvíe al puerto real de la aplicación. Eso es exactamente lo que automatizan herramientas como Caddy o portless.
¿Por qué se filtran las cookies entre localhost:3000 y localhost:3001 pero no entre app.localhost y api.localhost?
Los navegadores delimitan el ámbito de las cookies por host e ignoran el puerto, así que localhost:3000 y localhost:3001 comparten el mismo host, localhost, y por tanto el mismo almacén de cookies. Los subdominios con nombre tienen hosts distintos, así que app.localhost y api.localhost mantienen cookies separadas. Como la política del mismo origen se basa en el esquema, el host y el puerto, los hostnames distintos también separan limpiamente localStorage y sessionStorage, algo que los orígenes basados en puertos no hacen.
¿Qué versión de Node.js requiere portless y funciona sin sudo?
portless requiere Node.js 24 o superior. En macOS y Linux se autoeleva con sudo en la primera ejecución porque enlazar el puerto 443 para HTTPS requiere privilegios de root. HTTPS funciona con HTTP/2 desde el primer momento, y portless crea una autoridad de certificación local y establece su confianza la primera vez que se ejecuta; usa portless trust para añadir la CA más adelante si te saltaste el aviso inicial.