Замена номеров портов именованными URL при разработке
Замените порты localhost на именованные URL с .localhost, обратным прокси или portless, чтобы избежать конфликтов портов, утечек cookies и неверных вкладок.
Локальное доменное имя — это удобочитаемое имя хоста, например app.localhost, которое разрешается в 127.0.0.1, позволяя каждому локальному сервису сохранять стабильный адрес вместо постоянно меняющегося номера порта.
Вам наверняка знакома ситуация: запущены три dev-сервера, вы переключаетесь на вкладку localhost:3000, чтобы проверить исправление, а на вас смотрит вчерашний проект. Замена localhost:3000 на app.localhost разом устраняет целый ворох повседневных неудобств (конфликты портов, «плавающие» URL, утечку cookie и проблему «не той вкладки»), потому что каждое приложение получает собственное имя хоста, а вместе с ним — собственную изолированную область в браузере. В этой статье рассматриваются три способа этого добиться: встроенный TLD .localhost, самодельный обратный прокси и portless — специализированный локальный прокси от Vercel Labs.
Ключевые выводы
- TLD
.localhostзарезервирован для loopback-использования стандартом RFC 6761, поэтому любое имя внутри него разрешается в127.0.0.1в Chrome, Firefox и Edge без записи в hosts-файле. Safari, который полагается на системный резолвер, всё ещё может потребовать такой записи. - Поскольку браузеры ограничивают область cookie по хосту и игнорируют порт,
app.localhostиapi.localhostостаются раздельными, тогда какlocalhost:3000иlocalhost:3001используют общее хранилище cookie. - Сам по себе TLD
.localhostне убирает порт: ваше приложение по-прежнему слушает какой-то порт, поэтому нужен обратный прокси, сопоставляющий имя хоста с этим портом. - portless (Vercel Labs, всё ещё в предрелизной версии до 1.0) назначает каждому приложению временный порт из диапазона 4000–4999 через переменную окружения
PORTи маршрутизирует на него стабильный URLname.localhost, с HTTPS и HTTP/2, включёнными по умолчанию. - Стабильный именованный URL, зафиксированный в файле для агентов, позволяет ИИ-инструментам для программирования обращаться к нужному сервису, а не угадывать между портами 3001 и 8080.
Почему именованные URL лучше номеров портов?
Локальная разработка на основе портов ломается вполне предсказуемо, как только вы запускаете больше одного сервиса. Запустите второе приложение на занятом порту — и Node выдаст EADDRINUSE. Фреймворки, автоматически увеличивающие номер порта, избегают падения, но вносят «дрейф»: сегодня ваш блог на localhost:3001, а завтра на localhost:3002, из-за чего закладки становятся бесполезными, а история браузера для localhost:3000 превращается в неразбираемую свалку не связанных между собой проектов. Остановите один сервер, запустите другой на освободившемся порту — и оставленная открытой вкладка молча начнёт отдавать другой проект: это и есть проблема «не той вкладки».
Более тонкий сбой — утечка состояния. Браузеры ограничивают область cookie по хосту и не учитывают порт, поэтому localhost:3000 и localhost:3001 пишут в одно и то же хранилище cookie. Состояние сессии одного приложения просачивается в другое. Именованные поддомены решают это на уровне origin: app.localhost и api.localhost — разные имена хостов, поэтому они чётко разделяют cookie, а поскольку политика одного источника учитывает схему, хост и порт, они также разделяют localStorage и sessionStorage. Рекомендации Microsoft по этому TLD говорят о том же: присвоение каждому локальному приложению собственного имени изолирует привязанные к имени данные, такие как cookie, а имя в адресной строке сразу показывает, какое приложение вы смотрите.
Что такое TLD .localhost?
Discover how at OpenReplay.com.
Простейший механизм именованных URL уже встроен в ваш браузер. RFC 6761 резервирует TLD .localhost и все имена внутри него за loopback-адресом — именно поэтому app.localhost отвечает на 127.0.0.1 вообще без настройки. Chrome, Firefox и Edge выполняют это разрешение внутренне, сопоставляя любое имя *.localhost с 127.0.0.1 или ::1, так что такое имя работает как псевдоним для того, что уже обслуживается на localhost. За Safari стоит следить особо: он вместо этого передаёт имя системному DNS-резолверу, а не всякая конфигурация резолвера отвечает на поддомены .localhost, так что там может понадобиться запись в /etc/hosts.
Есть один нюанс: сам TLD не убирает порт. Ваше приложение по-прежнему слушает :3000, а app.localhost без порта просто обращается к app.localhost:80, где ничего не слушает. Чтобы действительно избавиться от номера, нужен обратный прокси на порту 80 или 443, который читает заголовок Host и перенаправляет запрос на реальный порт приложения.
Своими руками: hosts-файл плюс обратный прокси
Именованные URL можно собрать из уже знакомых вам компонентов. Добавьте имя хоста в /etc/hosts (либо положитесь на автоматическое разрешение .localhost), а затем запустите обратный прокси, который сопоставит имя с портом вашего dev-сервера. Конфигурация Caddy лаконична настолько, насколько это вообще возможно:
app.localhost {
reverse_proxy localhost:3000
}
api.localhost {
reverse_proxy localhost:8080
}
Caddy автоматически выпускает локальные TLS-сертификаты; nginx и Traefik делают то же самое, но требуют больше конфигурации. Для локальных домменов с подстановочным знаком dnsmasq может разрешать всё пространство *.test в 127.0.0.1, чтобы вы обошлись без отдельных записей в hosts для каждого имени. И каждому dev-серверу всё равно нужно зафиксировать хост и порт (в Vite через server.host и server.port, в webpack через devServer), чтобы у прокси была стабильная цель.
Плата за это — обслуживание. Вы поддерживаете конфигурацию прокси, доверие к сертификатам, записи в hosts и назначения портов по проектам, и вручную синхронизируете все четыре вещи по мере появления и исчезновения сервисов. Для одного-двух долгоживущих приложений это нормально. В масштабах монорепозитория это превращается в отдельную рутину.
portless: именованные URL, которые просто работают
portless — это локальный прокси, автоматизирующий всю цепочку. Вы добавляете префикс к своей команде разработки: next dev становится portless run next dev, — либо запускаете просто portless и позволяете ему вывести имя приложения из package.json, корня git-репозитория или имени каталога. Прокси запускается автоматически, назначает свободный порт из диапазона 4000–4999, передаёт его через переменную окружения PORT и маршрутизирует на него https://name.localhost. Фреймворкам, которые игнорируют PORT — таким как Vite, Astro, Angular и Expo, — за них подставляется нужный флаг --port, а также соответствующий флаг --host, если он требуется.
В релизах 0.15.x portless по умолчанию включает HTTPS с HTTP/2 на порту 443, генерируя и добавляя в доверенные локальный удостоверяющий центр при первом запуске. На macOS и Linux он автоматически повышает права через sudo, поскольку привязка к порту 443 требует root, а команда portless trust заново добавит CA, если вы пропустили запрос. Более ранние обзоры, где упоминается опциональный флаг --https и порт :1355 по умолчанию, описывают устаревшую версию. HTTP/2 помогает локально по конкретной причине: браузер держит открытыми не более шести HTTP/1.1-соединений к одному хосту, поэтому dev-сервер, отдающий сотни отдельных небандлированных файлов, в итоге ставит их в очередь, тогда как одно HTTP/2-соединение передаёт их все одновременно. portless требует Node.js версии 24 или новее.
Несколько функций особенно оправдывают себя на крупных конфигурациях. Поддомены вида api.myapp.localhost упорядочивают микросервисы; единственный файл portless.json в корне монорепозитория автоматически обнаруживает пакеты рабочего пространства. Для сервиса с фиксированным портом, который вы не можете изменить (например, Docker-контейнера), команда portless alias <name> <port> сопоставит с ним именованный URL, а PORTLESS=0 полностью обходит прокси — для CI или быстрой проверки. Если вам нужен свой TLD, portless рекомендует .test, который RFC 6761 также резервирует, и предостерегает от двух других: .local конфликтует с mDNS и Bonjour, а .dev принадлежит Google, которая принудительно переводит его на HTTPS через HSTS.
Почему стабильные локальные URL важны для ИИ-агентов программирования
ИИ-агенты программирования спотыкаются на портах так же, как люди, только молча: они жёстко прописывают номер, встретившийся ранее в контексте, или угадывают неверно. Агент, считывающий фиксированный https://api.myapp.localhost из файла AGENTS.md, всегда обращается к нужному сервису, вместо того чтобы от сессии к сессии переключаться между 3001 и 8080 и отвлекать вас вопросами. Это часть общего сдвига в инструментарии разработки: стабильные эндпоинты — это инфраструктура для автоматизации. portless поставляется с skill-файлами, а релизы 0.15.x добавляют страницы документации в Markdown и индекс llms.txt, чтобы его URL были обнаруживаемы агентами «из коробки».
Как выбрать подход
Только TLD .localhost | TLD + обратный прокси | portless | |
|---|---|---|---|
| Убирает порт? | Нет | Да | Да |
| Дополнительные инструменты | Нет | Caddy/nginx/Traefik | Одна глобальная установка |
| HTTPS | Вручную | Обеспечивается прокси | Включён по умолчанию |
| Автообнаружение в монорепозитории | Нет | Нет | Да |
| Дружественность к агентам | Частичная | Частичная | Да (skill-файлы, llms.txt) |
| Трудоёмкость настройки | Минимальная | Средняя (ручная синхронизация) | Низкая |
Решение в одну строку: берите встроенный TLD плюс обратный прокси, если хотите обойтись без новых инструментов и не против поддерживать конфигурацию; берите portless, если хотите, чтобы именованные URL просто работали для множества сервисов, монорепозитория или в связке с ИИ-агентами.
Именованные, стабильные, удобочитаемые локальные URL однозначно лучше номеров портов, и внедрить их можно за считаные минуты: добавьте сегодня двухстрочный Caddyfile или допишите префикс portless к одному dev-скрипту — и больше никогда не думайте про EADDRINUSE.
Часто задаваемые вопросы
Нужно ли добавлять поддомены .localhost в файл /etc/hosts?
Нет, в Chrome, Firefox и Edge это не требуется. Все три браузера сами разрешают любое имя внутри TLD .localhost в 127.0.0.1, поскольку RFC 6761 резервирует этот TLD для loopback-использования, поэтому app.localhost и api.localhost работают вообще без настройки. Исключение — Safari: он передаёт разрешение имени системному DNS-резолверу, а не всякая конфигурация резолвера отвечает на поддомены .localhost. Если имя не загружается, добавьте запись в /etc/hosts.
Убирает ли использование домена .localhost номер порта у моего dev-сервера?
Нет. TLD .localhost лишь разрешает имя хоста в 127.0.0.1; ваше приложение по-прежнему слушает свой исходный порт, поэтому app.localhost без порта обращается к app.localhost:80, где ничего не запущено. Чтобы действительно избавиться от номера, нужен обратный прокси на порту 80 или 443, который читает заголовок Host и перенаправляет запрос на реальный порт приложения. Именно это и автоматизируют инструменты вроде Caddy или portless.
Почему cookie утекают между localhost:3000 и localhost:3001, но не между app.localhost и api.localhost?
Браузеры ограничивают область cookie по хосту и игнорируют порт, поэтому localhost:3000 и localhost:3001 используют один и тот же хост localhost, а значит и одно хранилище cookie. У именованных поддоменов хосты разные, поэтому app.localhost и api.localhost хранят cookie раздельно. Поскольку политика одного источника учитывает схему, хост и порт, разные имена хостов также чётко разделяют localStorage и sessionStorage, чего origin'ы на основе портов не делают.
Какая версия Node.js требуется для portless и работает ли он без sudo?
portless требует Node.js версии 24 или новее. На macOS и Linux при первом запуске он автоматически повышает права через sudo, поскольку привязка к порту 443 для HTTPS требует привилегий root. HTTPS работает с HTTP/2 из коробки, и при первом запуске portless создаёт локальный удостоверяющий центр и добавляет его в доверенные; используйте portless trust, чтобы добавить CA позже, если вы пропустили первоначальный запрос.