12k
All articles

Замена номеров портов именованными URL при разработке

Замените порты localhost на именованные URL с .localhost, обратным прокси или portless, чтобы избежать конфликтов портов, утечек cookies и неверных вкладок.

OpenReplay Team
OpenReplay Team
Замена номеров портов именованными URL при разработке

Локальное доменное имя — это удобочитаемое имя хоста, например 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 и маршрутизирует на него стабильный URL name.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?

Простейший механизм именованных 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 .localhostTLD + обратный прокси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 позже, если вы пропустили первоначальный запрос.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.