Ошибка EADDRINUSE (она же address already in use, port already in use) означает простую вещь: процесс пытается сделать bind() на IP:порт, которые ядро уже считает занятыми. На сервере это чаще всего всплывает при рестарте веб-сервиса, деплое, смене конфигурации, поднятии второго экземпляра приложения или при конфликте с systemd socket-юнитом.
Ниже — практический разбор причин и пошаговый алгоритм диагностики на Linux: без «магии», но с теми командами, которые реально помогают на проде.
Что именно «занято»: порт, адрес или сочетание?
Конфликт бывает не только «порт занят», а именно «занята конкретная комбинация адреса и порта». Поэтому первым делом зафиксируйте, на какой адрес пыталось забиндиться приложение (это обычно видно в логе или конфиге).
0.0.0.0:PORT — слушаем все IPv4-адреса. Такой биндинг конфликтует почти с любым слушателем на этом же порту в IPv4.
127.0.0.1:PORT — слушаем только loopback. Может не конфликтовать с процессом на
1.2.3.4:PORT, но будет конфликтовать с0.0.0.0:PORT.[::]:PORT — IPv6. На многих системах IPv6-сокет может принимать и IPv4 (v4-mapped), и тогда «внезапно» мешает именно IPv6-листенер.
Самый быстрый путь к решению: сравнить точную пару IP:порт из ошибки/конфига с тем, что реально слушает система.
Быстрый чеклист: кто слушает порт прямо сейчас
Начинайте с ss: он быстрый и почти всегда показывает нужную связку «порт → процесс».
1) ss: найти LISTEN и PID
ss -lntp
ss -lntp '( sport = :80 )'
ss -lntup '( sport = :53 )'
Коротко по ключам:
-l— только слушающие сокеты,-n— без DNS-резолва,-t/-u— TCP/UDP,-p— показать процесс.Если PID не виден, обычно не хватает прав (запускайте от root) или сокет находится в другом network namespace (контейнеры).
2) lsof: альтернативный способ (иногда лучше показывает детали)
lsof -nP -iTCP:80 -sTCP:LISTEN
lsof -nP -iUDP:53
lsof удобен, когда нужно увидеть пользователя, аргументы процесса, рабочий каталог. Но на нагруженных хостах он может быть ощутимо тяжелее, чем ss.
3) fuser: «кто держит порт» и быстрый килл (осторожно)
fuser -v 80/tcp
fuser -v 53/udp
Если вы точно понимаете, что это «лишний» процесс, его можно завершить. Но на продакшене чаще правильнее сначала остановить сервис штатно (systemd/supervisor/оркестратор), и только затем добивать остатки.
fuser -k 80/tcp

Если LISTEN нет, а EADDRINUSE есть: TIME_WAIT, namespace и другие «невидимые» причины
Сценарий, который часто сбивает с толку: сервис падает с EADDRINUSE, а ss -lntp не показывает слушателя. На практике причины обычно такие:
порт занят не в вашем network namespace (контейнер, отдельный namespace, chroot сам по себе не влияет);
конфликт возникает не на уровне LISTEN, а из‑за переиспользования конкретных локальных адресов/портов (включая ситуации с
TIME_WAIT);порт держит
systemdчерез socket-activation.
TIME_WAIT: что это и почему иногда «мешает»
TIME_WAIT — нормальное состояние TCP после закрытия соединения: оно защищает от путаницы со «старыми» пакетами. В типичном веб-сценарии большое число TIME_WAIT — это чаще симптом паттерна соединений (много коротких коннектов), а не «поломка».
Важно: TIME_WAIT сам по себе обычно не мешает поднять LISTEN на том же порту. Если мешает, значит приложение делает что-то нестандартное: пытается повторно использовать конкретные локальные порты/адреса в исходящих соединениях, использует фиксированный source-port, поднимает много краткоживущих сокетов в одном процессе и т. п.
Быстро посмотреть картину по состояниям:
ss -tan state time-wait | head
ss -tan | awk '{print $1}' | sort | uniq -c | sort -nr | head
systemd socket-activation: порт слушает systemd, а не ваш сервис
На современных дистрибутивах частая ловушка: включен socket-юнит, и именно он держит порт, а ваш демон при старте пытается слушать этот порт сам. В результате — EADDRINUSE, и «виновник» выглядит как что-то постороннее.
Как проверить, не слушает ли порт socket-юнит
systemctl list-sockets
systemctl list-sockets | grep -E '(:80|:443|:3000|:8080)'
Если нашли подходящий socket, посмотрите его привязку и юнит:
systemctl status nginx.socket
systemctl cat nginx.socket
Типовые решения:
если socket-активация не нужна — отключить
.socketи включить обычный.service;если socket-активация нужна — убедиться, что сервис умеет принимать уже открытый дескриптор и не делает
bind()сам.
Пример отключения socket-юнита:
systemctl disable --now nginx.socket
systemctl enable --now nginx.service
Если вы управляете несколькими воркерами/демонами через supervisor/systemd, полезно держать под рукой разбор типичных конфликтов запусков: очереди, supervisor и systemd для воркеров.
SO_REUSEPORT и SO_REUSEADDR: когда «можно делить порт», а когда нельзя
В обсуждениях EADDRINUSE часто всплывают SO_REUSEADDR и SO_REUSEPORT. Это не «волшебные флаги», а инструменты под конкретные сценарии.
SO_REUSEADDR: быстрый рестарт и особые случаи
SO_REUSEADDR обычно помогает корректнее переживать перезапуски и переходные состояния сокетов, но не позволяет двум произвольным процессам одновременно слушать один и тот же TCP-порт на одном и том же адресе.
Для UDP поведение отличается: там совместное использование порта встречается чаще, и из-за этого диагностика «почему пакеты прилетают не туда» может быть сложнее.
SO_REUSEPORT: несколько слушателей на одном порту
SO_REUSEPORT позволяет нескольким сокетам слушать одну и ту же пару адрес:порт, а ядро распределяет входящие соединения между ними. Это полезно для высоконагруженных сервисов и некоторых моделей масштабирования.
Но учитывайте условия:
все процессы должны явно включить
SO_REUSEPORT;не все серверы и фреймворки корректно поддерживают этот режим «из коробки»;
ошибка настройки может привести к неожиданной маршрутизации трафика и гонкам в логике приложения.
Если задача — поднять один сервис на одном порту, лечить
EADDRINUSEчерезSO_REUSEPORTпочти всегда плохая идея. Сначала устраните реальный конфликт.
Типовые причины EADDRINUSE на серверах и как их быстро подтвердить
Причина 1: старый экземпляр сервиса не умер
Сервис рестартовали, но дочерний процесс остался жить; менеджер поднял второй экземпляр; сокет удерживает воркер, который не завершился.
Проверка:
ss -lntp '( sport = :8080 )'
ps -fp PID
Решение: штатная остановка через init-систему/менеджер процессов, затем контроль, что LISTEN исчез.
Причина 2: конфликт конфигурации (два сервиса слушают один порт)
Например, Nginx и Apache одновременно настроены на 0.0.0.0:80; или два инстанса приложения слушают 127.0.0.1:3000.
Подтверждение — ss/lsof. Дальше вы выбираете «кто главный»: меняете порт, адрес бинда или отключаете лишний сервис.
Причина 3: сервис слушает на IPv6 и «перехватывает» IPv4
Вы запускаете процесс на 0.0.0.0:80, а в системе уже есть LISTEN на [::]:80, и ядро настроено так, что IPv6-сокет принимает также IPv4. В логах это выглядит как «порт занят», хотя адрес «другой».
Проверка:
ss -lntp | grep ':80'
Решение зависит от стека: либо разводите сервисы по разным адресам, либо аккуратно разделяете IPv4/IPv6 поведение на уровне приложения/ОС (важно не сломать доступность).
Причина 4: порт занят в другом network namespace (контейнеры)
В Docker/Podman/Kubernetes встречаются две противоположные ситуации:
на хосте порт свободен, но внутри контейнера этот порт уже занят (конфликт внутри namespace контейнера);
в контейнере порт свободен, но публикация порта на хосте конфликтует с хостовым слушателем.
Проверяйте обе стороны: хост и конкретный контейнер (внутри него — ss/lsof).
Как действовать безопасно: «освободить порт» без лишнего даунтайма
Когда виновник найден, не начинайте с kill -9. На продакшене лучше идти по ступеням:
Остановить сервис штатно (systemd, supervisor, контейнер-оркестратор).
Проверить, что LISTEN исчез:
ss -lntpпо нужному порту.Если не исчез — выяснить процесс через
lsof/fuserи завершить корректно.Если процесс завис: сначала TERM, затем KILL.
Если сервис крутится на сервере и важно быстро масштабироваться или изолировать окружения, часто удобнее вынести проблемный компонент на отдельный VDS, чтобы порты/юниты/зависимости не конфликтовали между проектами.

Профилактика: как сделать так, чтобы EADDRINUSE не возвращался
1) Явно фиксируйте адрес бинда и порт
Не полагайтесь на «по умолчанию слушаем всё». Явно задавайте: 127.0.0.1 для внутреннего бекенда, публичный IP для фронта, отдельные порты для разных сред.
2) Проверьте, не включен ли лишний socket-юнит
Для systemd держите в голове, что .socket и .service могут жить параллельно и конфликтовать, особенно после установки пакетов, где socket-activation включается автоматически.
3) Делайте корректный graceful-restart
Многие демоны умеют «мягкую» перезагрузку без потери слушающего сокета (мастер держит порт, воркеры заменяются). Если вместо этого делать жесткий рестарт, вероятность поймать проблемы на стыке выше.
4) Не лечите симптомы sysctl-настройками без понимания
При большом числе TIME_WAIT часто хочется «подкрутить сеть». Иногда это оправдано, но обычно лучше сначала понять причину (паттерн соединений, keepalive, балансировщик, короткие запросы, тестовые нагрузки) и только потом менять параметры, чтобы не ухудшить стабильность.
Мини-шпаргалка команд
Если нужно «в один экран»:
ss -lntp '( sport = :PORT )'
ss -lntup '( sport = :PORT )'
lsof -nP -iTCP:PORT -sTCP:LISTEN
fuser -v PORT/tcp
systemctl list-sockets
ss -tan state time-wait | head
Заключение
EADDRINUSE почти всегда решается предсказуемо: выясняете точный IP:порт, находите слушателя (или socket-юнит), учитываете IPv4/IPv6 и контейнерные namespace, после чего освобождаете порт штатно. А если регулярно видите «LISTEN нет, но порт занят» — отдельно проверьте socket-activation и сценарии, где приложение переиспользует локальные порты на фоне большого числа TIME_WAIT.
Если хотите, можно быстро докопаться до конкретного кейса: пришлите строку ошибки из лога и вывод ss -lntp по проблемному порту.


