Ничто так не тормозит выкат 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 за корпоративным firewall | LAN IP + allowlist | Согласовать DHCP reservation |
| Публичный край с reverse proxy | loopback + 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 до/после для состояния running → not 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-тестами.