AI Frontier

Привязка порта шлюза OpenClaw на macOS в 2026: диагностика EADDRINUSE через lsof, проверка plist LaunchAgent и согласование health-проб

MacHTML Lab2026.05.11~30 мин чтения

Ничто так не тормозит выкат OpenClaw, как Error: listen EADDRINUSE: address already in use. В 2026 году команды по-прежнему путают «порт занят» с «упал API модели», крутят ключи часами и не замечают, что вчерашний smoke оставил node на TCP 8787. На macOS launchd может поднять новый job до полного закрытия старого сокета; на одном арендованном Mac mini крутятся несколько профилей; синтетический health часто бьёт в 127.0.0.1, тогда как шлюз слушает LAN. Здесь — повторяемая триаж-цепочка: сначала lsof, затем паритет plist, затем выравнивание проб — со связкой doctor и диагностика шлюза, первый запуск и LaunchAgent smoke и мониторинг здоровья шлюза, чтобы не гадать.

Коллизии портов — инцидент ёмкости: фиксируйте интерфейс, PID и label LaunchAgent, иначе постмортем растянется.

Без таблицы диапазонов портов blue/green в пятницу часто заканчивается двумя экземплярами на одном номере — заранее опишите диапазоны в вики.

Симптомы как у апстрима

Клиенты видят отказ в соединении, пустой curl или «шлюз офлайн» при почти нулевой загрузке CPU. Прежде чем винить вендоров моделей, убедитесь, что HTTP-сервер дошёл до LISTEN. Если процесс падает на старте, stderr может оборваться до баннера — останется errno. Соберите StandardErrorPath и сопоставьте с переходами launchctl print.

На общих Mac mini чаще всего виноваты забытые npm run dev; они не попадают в Grafana, потому что не регистрировали метрики.

Ошибки DNS и TLS дают похожие облака симптомов — сначала сокет, затем TLS, затем апстрим.

lsof за минуту

lsof -nP -iTCP:8787 -sTCP:LISTEN

-n убирает медленный DNS; -P показывает числовые порты. Неизвестный node PID — сначала ps -p PID -o args=. Если никто не слушает, а клиенты падают, проверьте HTTPS клиента против HTTP на том же порту.

Для IPv6-only добавьте lsof -nP -i6TCP:8787; dual-stack может дублировать строки.

ProgramArguments LaunchAgent

Двойной --port из wrapper и шаблона тихо перекрывается. Одна истина: либо EnvironmentVariables, либо явные аргументы. После правок launchctl bootout и bootstrap, чтобы старый сокет закрылся.

WorkingDirectory должен указывать на checkout с ожидаемой package.json; неверный cwd + npx легко поднимает второй слушатель на порту по умолчанию.

127.0.0.1, 0.0.0.0 и LAN

Loopback защищает от простого LAN-сканирования, но ломает health с других хостов. 0.0.0.0 требует дисциплины firewall. Фиксированный офисный IP ломается при DHCP renew. Зафиксируйте контракт в runbook и в ожиданиях openclaw doctor.

Обманчивые health-пробы

Монитор, который curl http://127.0.0.1:8787/readyz, остаётся зелёным, пока удалённые пользователи не достучатся до 10.0.40.12:8787 из-за split VPN или маршрутов. Выровняйте источник проб с реальным путём или туннелируйте через тот же bastion. Экспортируйте gateway_bind_interface при старте для Grafana.

Health L4 может быть зелёным при провале L7-auth — комбинируйте проверку сокета с минимальным аутентифицированным HTTP.

Матрица решений

СценарийПредпочтительный bindРиск
Один ноутбук127.0.0.1Переключить перед удалёнными демо
Общий Mac mini за корпоративным firewallLAN IP + allowlistСогласовать DHCP reservation
Публичный край с reverse proxyloopback + nginxНе выставлять шлюз на публичный интерфейс

TIME_WAIT и частые рестарты

Скрипты, перезапускающиеся каждые 30 секунд, оставляют соединения в TIME_WAIT до ~60 с или выжигают эфемерные порты. Вставьте ~5 с между bootout и bootstrap или временно сдвиньте админ-порт на +1.

Прикладной firewall

macOS спрашивает входящие для каждого нового пути к node. Отказ даёт локальный bind и удалённые SYN-timeout, похожие на конфликт порта. Стандартизируйте пути/подписи, чтобы апгрейды не спамили диалогами.

Несколько шлюзов на одном Mac

Blue/green требует разных портов (8787/8788) и уникальных label. Резервируйте диапазоны: 8700–8799 под OpenClaw, 8800–8899 под моки — без таблицы подрядчики выбирают случайно.

Staging и production на одном железе — разделяйте пользователей или логи, иначе lsof нечитаем.

launchctl kickstart

После освобождения порта используйте launchctl kickstart -k gui/$UID/com.example.openclaw, чтобы launchd убил упрямых детей SIGKILL. Без -k middleware-поток может держать FD после «shutdown complete». Сохраните вывод launchctl print до/после для состояния runningnot running.

Эфемерные клиентские порты

Fan-out инструментов открывает тысячи исходящих соединений; macOS может исчерпать эфемерный диапазон при живом LISTEN. Не путайте клиентский EADDRINUSE с конфликтом слушателя. Следите за sysctl net.inet.ip.portrange.hifirst у CI-runner на том же хосте.

Структурированные логи bind

JSON с event="bind_failed", errno, host, port, хэш argv. Постмортем не должен требовать SSH, чтобы вспомнить базу. errno 48 (EADDRINUSE) — в одной строке с командой lsof.

Почему Linux CI пропускает гонки macOS

Контейнеры и GUI-launchd ведут себя по-разному. Linux CI — контроль компиляции; для plist меняйте — smoke bind на Apple-железе. Суточная аренда Mac mini (~16,9 USD) сопоставима с часом инженера и закрывает разрыв.

FAQ

macOS сразу освобождает порт?

После частых рестартов возможен TIME_WAIT; подождите или смените порт.

Почему health зелёный, а пользователи нет?

Разные интерфейсы и сетевые пути.

0.0.0.0 безопаснее loopback?

Нет — шире поверхность, нужен firewall.

Когда арендовать Mac mini?

Когда нужна продакшен-подобная семантика сокетов macOS вне ноутбука.

Битвы за порты скучны, но дороги. Аренда Mac mini на Apple Silicon у MacHTML — около 16,9 USD в сутки — даёт тот же цикл launchd, те же сокеты и те же диалоги firewall, что и прод, без отправки железа. Поднимите на релизную неделю, соберите lsof-доказательства, затем выключите.

Низкий шум вентилятора помогает при долгих SSH-сессиях с повторными bind-тестами.

Воспроизведение bind-проблем OpenClaw на настоящем macOS

Арендуйте облачный Mac mini для проверки портов, plist LaunchAgent и health-проб с корректным поведением сокетов macOS.

QA портов шлюза
от ~16,9 $/сутки