Ви розгортаєте сервіс, він намагається прив’язатися, а ядро відповідає найменш корисною правдою в інформатиці: bind: Address already in use. Pager спрацьовує. Хтось каже «просто перезавантажте». Інший каже «це, мабуть, DNS». Ніхто з них вам зараз не друг.
На Debian 13 у вас є всі інструменти, щоб точно ідентифікувати, хто займає порт — процес, systemd unit, контейнер, socket-активація або застарілий неймспейс — і виправити конфлікт без зайвих побічних ефектів. Трюк у тому, щоб знати порядок перевірок і що саме кожен інструмент вам каже.
Швидкий план діагностики
Це послідовність «припиніть кровотечу». Вона оптимізована для швидкого знаходження власника, а не для навчання. Навчання — пізніше.
По-перше: підтвердіть, що саме не вдається (логи сервісу)
Не починайте з порт-сканерів. Почніть із менеджера сервісів. Якщо systemd причетний, він часто скаже вам точну адресу та порт, на яких bind зазнав невдачі.
По-друге: ідентифікуйте слухача (ss)
Використовуйте ss перш за все. Він сучасний, швидкий і показує вигляд сокетів із погляду ядра з асоціацією до процесу.
По-третє: зіставте PID з реальним власником (systemd unit / контейнер)
Власність PID — не те саме, що «хто спричинив це в продакшені». Вам потрібен unit, пакет або образ контейнера за цим процесом.
По-четверте: перевірте socket-активацію systemd і переспрямувальники портів
Порт може утримуватися socket-юнітом, а не тим демоном, про який ви думаєте. Або проксі (Envoy, HAProxy, nginx), про який хтось забув згадати.
По-п’яте: оберіть чисте виправлення
Віддавайте перевагу відключенню невірного слухача, виправленню конфігурації або перенесенню сервісу на виділений порт. Уникайте «kill -9», якщо не любите постмортеми.
Цікаві факти та контекст (чому це повторюється)
- Факт 1: Рядок помилки походить від
EADDRINUSE, errno POSIX, що повертаєтьсяbind(2), коли локальна кортеж адреса/порт вже зайнята. - Факт 2: Раніше, до поширення
ss, адміністратори Linux користувалисяnetstat(з пакету net-tools). Debian вже роками підштовхує людей від net-tools, і Debian 13 остаточно у ері «use iproute2». - Факт 3: Socket-активація systemd може прив’язувати порти до того, як сервіс стартує. Тобто власником порту може бути systemd, а не ваш демон.
- Факт 4: IPv6 може «накрити» IPv4 через v6-mapped сокети залежно від
net.ipv6.bindv6only. Ви думаєте, що прив’язуєте IPv4, а слухач IPv6 займає порт. - Факт 5: Привілейовані порти (нижче 1024) історично вимагали root. Сучасний Linux може надати цій програмі можливість через file capabilities, що змінює, хто може bind-порт — і хто може конфліктувати.
- Факт 6: Конфлікт порту може спричинити зовсім інший мережевий неймспейс (контейнер, systemd-nspawn). Інструменти на хості можуть не бачити його, якщо ви не перевірите потрібний неймспейс.
- Факт 7:
SO_REUSEADDRшироко неправильно розуміють. Воно не означає «дві різні програми можуть слухати той самий TCP-порт». ЦеSO_REUSEPORT, і воно має складні нюанси. - Факт 8: UDP поводиться інакше. Декілька UDP-сокетів можуть іноді прив’язуватись до того самого порту залежно від опцій і адрес, що ускладнює визначення «хто власник».
- Факт 9: «Нічого не слухає, але bind не вдається» іноді означає, що ваш додаток намагається прив’язатися до IP, який є на машині, але не на інтерфейсі, який ви очікуєте (або керується VRRP, keepalived чи cloud-агентом).
Що насправді означає «Address already in use» на Linux
Коли сервер стартує, він зазвичай виконує приблизно таке: створює сокет, встановлює опції, потім виконує bind() до локальної адреси/порту, потім listen(). Якщо ядро не може зарезервувати цю комбінацію адреса/порт, воно повертає EADDRINUSE. Ваша програма виводить помилку й завершується (або повторює спробу, якщо вона ввічлива).
Ось важлива частина: «Адреса» включає більше ніж номер порту. Це може означати:
- TCP vs UDP: TCP-порт 53 і UDP-порт 53 — це різні сокети. Один може бути зайнятий, а інший — вільний.
- IP-специфічний vs wildcard: Прив’язка до
127.0.0.1:8080відрізняється від прив’язки до0.0.0.0:8080(усі IPv4). Але wildcard-прив’язка може блокувати специфічну прив’язку, залежно від того, як вже було зроблено binding. - IPv6 wildcard:
[::]:8080на деяких системах може також приймати IPv4-з’єднання, якщо ядро не налаштоване як v6-only. - Мережеві неймспейси: Якщо ви всередині неймспейсу контейнера, «порт 8080» не обов’язково означає порт 8080 хоста — якщо тільки ви його не опублікували.
Якщо візьмете лише один урок з цього розділу: завжди фіксуйте повний target прив’язки з логів — протокол, IP і порт — перш ніж ганятися за примарами.
Одна перефразована ідея від Richard Cook (дослідник надійності): Збої відбуваються у розриві між тим, як планувалося робити роботу, і тим, як вона фактично виконується.
Визначення власності порту — саме один із таких розривів.
Практичні завдання: команди, виводи, рішення (12+)
Це завдання, які ви виконуєте на хості Debian 13, коли сервіс не стартує через «Address already in use». Кожне містить, що означає вивід і яке рішення прийняти далі.
Завдання 1: Прочитайте логи юніта, що зазнає невдачі (systemd)
cr0x@server:~$ sudo systemctl status myapp.service
× myapp.service - MyApp API
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Mon 2025-12-29 09:12:01 UTC; 8s ago
Process: 18422 ExecStart=/usr/local/bin/myapp --listen 0.0.0.0:8080 (code=exited, status=1/FAILURE)
Main PID: 18422 (code=exited, status=1/FAILURE)
CPU: 48ms
Dec 29 09:12:01 server myapp[18422]: bind: Address already in use
Dec 29 09:12:01 server systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
Dec 29 09:12:01 server systemd[1]: myapp.service: Failed with result 'exit-code'.
Значення: Ви отримали target прив’язки: 0.0.0.0:8080. Це повний IPv4 wildcard-порт. Тепер можна шукати точно.
Рішення: Знайдіть, хто вже слухає TCP/8080 на IPv4 або IPv6 wildcard.
Завдання 2: Знайдіть слухачів за допомогою ss (швидко, точно)
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("nginx",pid=1312,fd=8))
Значення: TCP-порт 8080 вже прив’язаний на всіх IPv4-адресах процесом nginx PID 1312.
Рішення: Визначте, чи nginx повинен володіти 8080. Якщо ні — відредагуйте конфіг nginx або зупиніть/відключіть юніт.
Завдання 3: Підтвердьте, що IPv6 також не причетний
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080' -6
LISTEN 0 4096 [::]:8080 [::]:* users:(("nginx",pid=1312,fd=9))
Значення: nginx також слухає IPv6 wildcard. Навіть якщо ви виправите IPv4, IPv6 може все ще створити колізію залежно від поведінки прив’язки вашого додатка.
Рішення: Вирішіть, чи ваш новий сервіс має слухати IPv6 теж, і переконайтеся, що nginx повністю звільнений з порту, а не «наполовину виправлений».
Завдання 4: Перекладіть PID у systemd unit (реальний власник)
cr0x@server:~$ ps -o pid,comm,args -p 1312
PID COMMAND COMMAND
1312 nginx nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
cr0x@server:~$ systemctl status nginx.service
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-12-29 08:55:10 UTC; 17min ago
Main PID: 1312 (nginx)
Tasks: 2 (limit: 18920)
Memory: 4.9M
CPU: 1.205s
Значення: Це керований сервіс, а не випадковий процес. Чудово: ви можете виправити це чисто.
Рішення: Перевірте конфіг nginx на слухачі 8080, потім змініть його або видаліть і перезавантажте nginx.
Завдання 5: Знайдіть директиву прив’язки в nginx
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE 'listen\s+8080'
47: listen 8080 default_server;
98: listen [::]:8080 default_server;
Значення: nginx явно налаштований на використання 8080 обома стековими адресами.
Рішення: Або змініть nginx на інший порт, або перенесіть ваш додаток на інший порт, або помістіть додаток за nginx і залиште 8080 за nginx. Виберіть одне — не «діліться».
Завдання 6: Якщо це не керований сервіс — визначте пакет або батька
cr0x@server:~$ sudo ss -H -ltnp 'sport = :5432'
LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=2221,fd=6))
cr0x@server:~$ ps -fp 2221
UID PID PPID C STIME TTY TIME CMD
postgres 2221 1 0 08:41 ? 00:00:01 /usr/lib/postgresql/17/bin/postgres -D /var/lib/postgresql/17/main
Значення: Postgres слухає тільки на localhost, але все одно займає 5432.
Рішення: Якщо ви намагалися стартувати інший Postgres або додаток, що хоче 5432, зупиніть існуючий екземпляр або перемістіть один із них. Запуск «ще одного Postgres на 5432» — це не план.
Завдання 7: Використовуйте lsof, коли ss не показує очікуване
cr0x@server:~$ sudo lsof -nP -iTCP:8080 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 1312 root 8u IPv4 24562 0t0 TCP *:8080 (LISTEN)
nginx 1312 root 9u IPv6 24563 0t0 TCP *:8080 (LISTEN)
Значення: Та сама історія, інструмент інший. lsof повільніший, але іноді зрозуміліший у стресі.
Рішення: Якщо інструменти не сходяться — довіряйте погляду ядра, але перевірте неймспейс (див. нижче). Більшість розбіжностей означає, що ви дивитесь у різні неймспейси або неправильно відфільтрували.
Завдання 8: Перевірте, чи порт утримує socket-активація systemd
cr0x@server:~$ systemctl list-sockets --all | grep -E ':(80|443|8080)\b'
nginx.socket 0 128 0.0.0.0:8080 0.0.0.0:*
nginx.socket 0 128 [::]:8080 [::]:*
Значення: Порт може бути власністю socket-юніта, який продовжує слухати навіть якщо сервіс зупинено. Це класичний випадок «я його зупинив, чому він досі зайнятий?».
Рішення: Керуйте .socket юнітом, а не тільки .service. Відключіть/mask-уйте сокет, якщо не хочете, щоб systemd його утримував.
Завдання 9: Зупиніть і відключіть правильний юніт (service vs socket)
cr0x@server:~$ sudo systemctl stop nginx.service
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("systemd",pid=1,fd=67))
cr0x@server:~$ sudo systemctl stop nginx.socket
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
cr0x@server:~$ sudo systemctl disable --now nginx.socket
Removed "/etc/systemd/system/sockets.target.wants/nginx.socket".
Значення: Зупинка сервісу нічого не дала, бо systemd утримував сокет. Зупинка сокета звільнила порт.
Рішення: Якщо nginx не має управляти тим портом більше, відключіть сокет (і переконайтеся, що сервіс теж не увімкнений випадково).
Жарт 1: Якщо ви «виправили» конфлікт порту перезавантаженням, ви його не вирішили — ви просто перезавантажили загадку.
Завдання 10: Підтвердьте, до якої IP-адреси прив’язується ваш додаток
cr0x@server:~$ ip -br addr show
lo UNKNOWN 127.0.0.1/8 ::1/128
ens3 UP 10.10.5.21/24 2001:db8:10:10::21/64
Значення: Ви можете прив’язуватися тільки до адрес, які належать машині (або які будуть налаштовані через менеджер, як-от keepalived). Якщо ваш додаток намагається bind до 10.10.5.99, а ви його не маєте, ви отримаєте іншу помилку (зазвичай Cannot assign requested address), але люди часто неправильно читають логи і ганяються за хибним питанням.
Рішення: Переконайтеся, що bind-адреса відповідає реальності. Якщо це VIP, підтвердьте, що VIP присутній на цьому вузлі.
Завдання 11: Впіймайте «прихованого» слухача в іншому неймспейсі (контейнери)
cr0x@server:~$ sudo ss -H -ltnp 'sport = :9090'
LISTEN 0 4096 0.0.0.0:9090 0.0.0.0:* users:(("docker-proxy",pid=5144,fd=4))
Значення: Хост-порт утримує docker-proxy (або іноді nftables без docker-proxy, залежно від налаштувань). Ваш реальний додаток всередині контейнера, але хостова прив’язка блокує ваш сервіс.
Рішення: Знайдіть, який контейнер опублікував порт. Не вбивайте docker-proxy; виправте мапінг контейнера.
Завдання 12: Визначте контейнер, що опублікував порт (Docker)
cr0x@server:~$ sudo docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Ports}}'
CONTAINER ID NAMES PORTS
c2f1d3a7b9c1 prometheus 0.0.0.0:9090->9090/tcp, [::]:9090->9090/tcp
8bca0e23ab77 node-exporter 9100/tcp
cr0x@server:~$ sudo docker inspect -f '{{.Name}} {{json .HostConfig.PortBindings}}' c2f1d3a7b9c1
/prometheus {"9090/tcp":[{"HostIp":"","HostPort":"9090"}]}
Значення: Контейнер prometheus володіє хостовим 9090.
Рішення: Якщо ваш новий сервіс потребує 9090, перемістіть Prometheus (рекомендується: залишити 9090 для Prometheus і перемістити ваш сервіс), або змініть опублікований порт і оновіть scrape-конфігурації.
Завдання 13: Визначте слухачів, створених сесією користувача (не root, не systemd)
cr0x@server:~$ sudo ss -H -ltnp 'sport = :3000'
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=27533,fd=21))
cr0x@server:~$ ps -o pid,user,cmd -p 27533
PID USER CMD
27533 alice node /home/alice/dev/server.js
cr0x@server:~$ sudo loginctl session-status
2 - alice (1000)
Since: Mon 2025-12-29 07:58:02 UTC; 1h 17min ago
Leader: 27102 (sshd)
Seat: n/a
TTY: pts/1
Service: sshd
State: active
Unit: session-2.scope
└─27533 node /home/alice/dev/server.js
Значення: Людина запускає dev-сервер у продукції. Це трапляється частіше, ніж хтось зізнається.
Рішення: Узгодьте дії. Або перенесіть dev-процес, або зарезервуйте порт для продакшену і попросіть людей використовувати непересічний порт, прив’язаний до localhost (або краще: dev-хост).
Завдання 14: Опрацюйте конфлікти UDP (DNS, метрики, ігрові сервери)
cr0x@server:~$ sudo ss -H -lunp 'sport = :53'
UNCONN 0 0 127.0.0.1:53 0.0.0.0:* users:(("systemd-resolve",pid=812,fd=13))
Значення: UDP/53 вже прив’язаний на localhost резолвером. Якщо ви намагаєтеся запустити DNS-сервер, буде конфлікт.
Рішення: Вирішіть, чи цей хост повинен запускати DNS-сервер. Якщо так — вам, можливо, доведеться переконфігурувати локальний резолвер, щоб він не прив’язував порт, або прив’язати ваш DNS до конкретного інтерфейсу/IP, що не конфліктує (обережно).
Завдання 15: Підтвердіть налаштовані Listen* та порти сервісу через systemd
cr0x@server:~$ systemctl cat nginx.socket
# /lib/systemd/system/nginx.socket
[Unit]
Description=nginx Socket
[Socket]
ListenStream=8080
ListenStream=[::]:8080
Accept=no
[Install]
WantedBy=sockets.target
Значення: Socket-юніт — джерело прив’язки. Це курильний слід, коли порт «повертається» після змін.
Рішення: Змініть/перевизначте цей юніт або відключіть/mask-уйте його.
Завдання 16: Переконайтеся, що порт справді вільний після зміни
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
cr0x@server:~$ sudo systemctl start myapp.service
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("myapp",pid=19201,fd=7))
Значення: Власність чиста: очікуваний процес тепер слухає.
Рішення: Перейдіть до перевірок здоров’я, маршрутизації та інших задач, які чергають наступні виклики богів аутейджів.
Підводні камені systemd: сокети, юніти, залишкові слухачі
На Debian 13 systemd — центр тяжіння. Навіть якщо ви вважаєте, що «просто запускаєте бінарник», швидше за все ви робите це через unit-файл, таймер, сокет або сервіс контейнера.
Socket-активація — чудово, поки ви не забудете, що вона існує
Socket-активація означає, що systemd прив’язує порт, потім запускає сервіс за потребою і передає йому файловий дескриптор. Це покращує латентність старту для сервісів, що реагують на запити, і може згладжувати рестарти. Але це також створює режим відмов: ви зупиняєте сервіс, а порт залишається прив’язаний PID 1.
Якщо бачите users:(("systemd",pid=1,...)) у виводі ss для вашого порту — не сперечайтеся. Знайдіть socket-юніт:
cr0x@server:~$ systemctl list-sockets --all | grep ':8080'
nginx.socket 0 128 0.0.0.0:8080 0.0.0.0:*
nginx.socket 0 128 [::]:8080 [::]:*
Masking: ядерний варіант, який іноді правильний
Disable запобігає запуску під час завантаження, але щось інше може все одно запустити юніт вручну або як залежність. Mask блокує його повністю, замінюючи посиланням на /dev/null. У інцидентному реагуванні masking може бути правильним рішенням, коли небажаний слухач постійно воскресає.
cr0x@server:~$ sudo systemctl mask --now nginx.socket
Created symlink /etc/systemd/system/nginx.socket → /dev/null.
Правило прийняття рішення: Disable — коли ви змінюєте очікувану поведінку і маєте час зробити це правильно. Mask — коли потрібно гарантувати, що воно не повернеться (і ви потім приберете маску вручну).
Drop-ins краще за редагування vendor-юнитів
У Debian vendor-юнити живуть під /lib/systemd/system. Їх редагування працює, поки їх не перезапише оновлення пакета.
Створюйте override замість цього:
cr0x@server:~$ sudo systemctl edit nginx.socket
[Socket]
ListenStream=
ListenStream=127.0.0.1:8080
Значення: Порожній рядок ListenStream= скидає список, потім ви задаєте новий. Це спосіб systemd переконатися, що ви дійсно хочете «замінити», а не «додати».
Рішення: Використовуйте drop-ins для стабільної поведінки при оновленнях. Редагування vendor-файлів — тимчасовий хак, який варто розглядати як тимчасовий.
Контейнери та оркестрація: Docker, Podman, Kubernetes
Конфлікти портів у контейнеризованих середовищах рідко бувають «два демони хочуть 8080». Зазвичай це «хтось опублікував 8080 на хості місяцями тому й усі забули». Контейнери полегшують доставку ПО; вони також полегшують заняття портів назавжди.
Docker та публікація хост-портів
Коли ви запускаєте -p 8080:8080, ви явно захоплюєте хост-порт. У багатьох налаштуваннях ви побачите docker-proxy, що володіє сокетом. В інших — nftables керує переспрямуванням, але хост-порт все одно може виглядати зайнятим залежно від режиму.
Чистий робочий процес:
- Визначте, що проксі — слухач (
ssпоказує docker-proxy). - Зіставте його з контейнером (
docker ps,docker inspect). - Змініть опублікований порт або видаліть контейнер.
Podman: та сама ідея, інше під капотом
Podman може запускати rootless-контейнери. Rootless прив’язка портів додає особливість: процеси можуть жити в сесії користувача, а переспрямування портів здійснюють slirp4netns або pasta. Симптом той самий: хост-порт зайнятий.
Якщо підозрюєте Podman:
cr0x@server:~$ podman ps --format 'table {{.ID}}\t{{.Names}}\t{{.Ports}}'
CONTAINER ID NAMES PORTS
4fdb08b1b3a8 grafana 0.0.0.0:3000->3000/tcp
Рішення: Якщо це rootless і прив’язане до користувача, може знадобитися узгодження з тим користувачем або відключення lingering для облікового запису.
Kubernetes: NodePort і hostNetwork
Kubernetes додає особливого хаосу:
- NodePort займає порти в діапазоні, і ви можете зіштовхнутись, якщо щось інше слухає там.
- hostNetwork: true змушує поди прив’язуватися безпосередньо до мережевого стеку вузла.
- DaemonSets означають «воно всюди», що зручно, якщо це задумано, і дуже дратує, якщо ні.
На вузлі з kubelet ss покаже процес-власник (іноді сам додаток, іноді проксі). Ваше завдання — зіставити його з подом і робочим навантаженням. Точні команди залежать від доступу до кластера, але діагностика на боці хоста однакова: спочатку ідентифікуйте слухача.
IPv4/IPv6 і «слухає, але не там, де ви шукали»
Класична плутанина: ви запускаєте ss -ltnp, не бачите свого порту, але додаток все одно падає з «Address already in use». Потім ви запускаєте ще раз і бачите щось на [::]. Ви кажете «ми не використовуємо IPv6». Ядро на ваші переконання плюватися.
Розберіться з wildcard-слухачами
Ось патерни, які варто розпізнавати:
0.0.0.0:PORTозначає всі IPv4-адреси.[::]:PORTозначає всі IPv6-адреси. Залежно від sysctl, воно також може приймати IPv4-меппінг-з’єднання.127.0.0.1:PORTозначає тільки localhost (зазвичай безпечніше для внутрішніх адміністративних кінцевих точок).
Перевірте bindv6only, коли поведінка дивна
cr0x@server:~$ sysctl net.ipv6.bindv6only
net.ipv6.bindv6only = 0
Значення: При 0 IPv6-wildcard сокет може також приймати IPv4-з’єднання через v4-mapped адреси на багатьох системах. Це може робити [::]:8080 блокуючим для очікувань 0.0.0.0:8080 залежно від того, як програми встановлюють опції.
Рішення: Не «виправляйте» це миттєво зміною sysctl під час інциденту, якщо ви не розумієте радіус ураження. Виправляйте це в конфігурації додатка/сервісу: явно прив’язуйте до IPv4 або IPv6, як задумано.
Гонки, перезапуски та міфи про TIME_WAIT
Люди звинувачують TIME_WAIT у проблемах прив’язки порту так само часто, як звинувачують «мережу» за повільні запити. Іноді воно причетне, але рідко є причиною того, що сервер не може прив’язати порт для прослуховування.
TIME_WAIT зазвичай про вихідні з’єднання
TIME_WAIT сокети зазвичай — це клієнтські ефермерні порти, а не серверні listening-порти. Ваш сервер зазвичай може прив’язати свій listening-порт без проблем. Якщо не може — майже напевно у вас є реальний слухач, socket-юніт або завислий процес.
Коли перезапуски все ж дають конфлікти
Справжня проблема при перезапусках — це перекриття двох інстансів:
- systemd запускає новий інстанс до того, як старий повністю вийшов (некоректний
Type=forking, неправильний трекінг PID). - Додаток робить daemonize, але unit написаний як для процесу без daemonize, тож systemd думає, що він помер і перезапускає — створюючи множинні спроби прив’язки.
- Обгортковий скрипт порожньо створює реальний сервер і відходить.
Щоб виявити це, дивіться на наявність кількох процесів з тим самим бінарником або на швидкі цикли перезапусків у журналі.
cr0x@server:~$ sudo journalctl -u myapp.service -n 50 --no-pager
Dec 29 09:11:58 server systemd[1]: Started MyApp API.
Dec 29 09:12:01 server myapp[18422]: bind: Address already in use
Dec 29 09:12:01 server systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
Dec 29 09:12:01 server systemd[1]: myapp.service: Failed with result 'exit-code'.
Dec 29 09:12:01 server systemd[1]: myapp.service: Scheduled restart job, restart counter is at 5.
Dec 29 09:12:01 server systemd[1]: Stopped MyApp API.
Dec 29 09:12:01 server systemd[1]: Started MyApp API.
Рішення: Якщо є цикл перезапусків — зупиніть юніт, щоб стабілізувати хост, потім вирішіть конфлікт. Інакше ви будете ганятися за рухомою ціллю, поки systemd багаторазово створює невдачі.
Чисті виправлення, які можна захистити в рев’ю змін
Порт можна «звільнити» багатьма способами. Більшість із них — ліниві. Мета — виправити модель власності так, щоб конфлікт не повернувся після наступного перезавантаження, redeploy або доброзичливого інженера у п’ятницю ввечері.
Варіант A: Перенесіть сервіс на правильний порт (і задокументуйте)
Якщо два сервіси хочуть 8080, бо ніхто не обрав план портів — оберіть зараз. Покладіть це в конфіг менеджменту. Додайте в мітки моніторингу. Зробіть це нудним.
Варіант B: Помістіть один сервіс за зворотним проксі
Якщо nginx вже на 80/443, а ваш додаток хоче 8080, чиста архітектура зазвичай така: nginx володіє публічними портами; додатки живуть на приватних високих портах або unix-сокетах; nginx маршрутизує.
Будьте послідовними. «Іноді ми прив’язуємо додатки прямо до 0.0.0.0» — от як з’являються війни за порти.
Варіант C: Зупиніть/відключіть невірний сервіс (service + socket, якщо потрібно)
Якщо порт належить чомусь, чого ви не хочете, приберіть його з графа завантаження:
cr0x@server:~$ sudo systemctl disable --now nginx.service
Removed "/etc/systemd/system/multi-user.target.wants/nginx.service".
cr0x@server:~$ sudo systemctl disable --now nginx.socket
Removed "/etc/systemd/system/sockets.target.wants/nginx.socket".
Рішення: Відключення обох запобігає танцю «я його зупинив, але він повернувся».
Варіант D: Робіть прив’язки явними (уникайте wildcard, коли не потрібно)
Прив’язка до 0.0.0.0 зручна і часто неправильна. Для адмін-прикінців прив’язуйте до localhost. Для внутрішніх сервісів — до IP внутрішнього інтерфейсу або виділеного VRF. Для публічних сервісів — прив’язуйте до VIP балансувальника або дозвольте проксі володіти портом.
Варіант E: Використовуйте file capabilities для привілейованих портів замість запуску від root
Якщо сервіс потребує 80/443, але не повинен працювати від root, встановіть capabilities:
cr0x@server:~$ sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
cr0x@server:~$ getcap /usr/local/bin/myapp
/usr/local/bin/myapp cap_net_bind_service=ep
Значення: Бінар може прив’язувати привілейовані порти без повних прав root.
Рішення: Використовуйте це, коли потрібно, але відстежуйте — capabilities легко забути і вони змінюють ваш профіль безпеки. Також: capabilities не запобігають конфліктам; вони лише змінюють, хто може їх створювати.
Варіант F: Резервуйте порти і забезпечуйте політику
У серйозних середовищах ви ведете реєстр портів для кожної ролі хоста або кластера. Його застосовують у CI, у helm-чарті, у systemd-шаблонах. Без цього конфлікти портів — не «інциденти», це «неминучість».
Жарт 2: Роздача портів без реєстру — як розсадка на конференції: всі б’ються за один стіл поруч із розеткою.
Поширені помилки: симптом → корінь → виправлення
1) «Я зупинив сервіс, але порт досі зайнятий»
Симптом: systemctl stop foo.service пройшов, але ss все ще показує порт у LISTEN.
Корінь: foo.socket юніт прив’язує порт (socket-активація), або інший залежний сервіс його втримує.
Виправлення: Перевірте systemctl list-sockets. Зупиніть/відключіть/mask-уйте socket-юніт. Перевірте ss, щоб PID 1 більше не був слухачем.
2) «ss нічого не показує, але мій додаток каже Address already in use»
Симптом: Ви шукали через ss і не бачили слухача на тому порті.
Корінь: Ви шукали не той протокол (UDP vs TCP), не ту сім’ю адрес (IPv4 vs IPv6), або фільтр помилковий. Менш часто: ви в іншому мережевому неймспейсі, а слухач у іншому.
Виправлення: Шукайте в обох стеках і протоколах: ss -ltnp, ss -lunp, додавайте -6. Якщо є контейнери — перевірте docker-proxy і інспектуйте mapping контейнерів.
3) «Порт належить ‘systemd’ і я не знаю чому»
Симптом: ss показує users:(("systemd",pid=1,...)).
Корінь: Конфігурований socket-юніт прив’язав його (іноді під ім’ям, якого ви не очікували, або підключений пакетом).
Виправлення: Ідентифікуйте сокет через systemctl list-sockets --all, потім systemctl cat NAME.socket. Відключіть або перевизначте ListenStream/ListenDatagram.
4) «Працює на IPv4, але не на IPv6» (або навпаки)
Симптом: Сервіс стартує при прив’язці до 127.0.0.1, але падає на 0.0.0.0, або падає лише при конфігурації для [::].
Корінь: Інший сервіс прив’язаний на одній із сімей; або dual-stack IPv6 блокує IPv4.
Виправлення: Перевірте слухачів для обох сімей. Зробіть прив’язки явними у конфігах. Не покладайтеся на значення за замовчуванням.
5) «Ми змінили порт, але він постійно повертається»
Симптом: Після редагування юніта або конфігу старий порт повертається після оновлень/перезавантажень.
Корінь: Ви редагували vendor-конфіг під /lib, або інструмент управління конфігурацією перезаписує зміни.
Виправлення: Використовуйте systemd drop-ins під /etc/systemd/system. Якщо є CM, зробіть його джерелом істини.
6) «Вбив PID — і це допомогло, але тепер інші речі зламались»
Симптом: Ви застосували kill -9 до слухача; порт звільнився; пізніше виявили побічні ефекти.
Корінь: Ви вбили спільний компонент (проксі, інгрес, агент метрик) або супервізований сервіс, який автоматично перезапустився на той самий порт.
Виправлення: Зіставляйте PID → unit/container перед вбивством. Зупиніть через systemd/Docker, щоб воно залишилось зупиненим (або змініть конфіг). Використовуйте kill тільки коли ви вирішили, що випадковий вплив допустимий.
Контрольні списки / покроковий план
Чекліст: Ідентифікуйте власника менше ніж за 2 хвилини
- Прочитайте точний target прив’язки з логів (
systemctl statusабо логи додатка). Зафіксуйте протокол, IP і порт. - Запустіть
ss -ltnp 'sport = :PORT'для TCP;ss -lunp 'sport = :PORT'для UDP. - Якщо нічого немає — перевірте IPv6 явно з
-6і переконайтеся, що не помилилися в номері порту. - Коли маєте PID/команду, зіставте з systemd unit через
systemctl statusіps. - Якщо слухач — systemd PID 1, перераховуйте сокети і знайдіть
.socketюніт. - Якщо слухач — docker-proxy/помістка контейнера, ідентифікуйте контейнер, що публікує порт.
Чекліст: Зробіть виправлення довготривалим
- Прийміть рішення, який сервіс має володіти портом (архітектурне рішення, не монетка).
- Впровадьте зміни конфігурації через drop-ins або керований конфіг, а не правкою vendor-файлів.
- Зупиніть/відключіть старого власника (service і socket, якщо релевантно).
- Запустіть очікуваного власника і перевірте через
ss. - Зробіть локальний тест з’єднання (curl, nc) і перевірте зовнішню маршрутизацію, якщо потрібно.
- Додайте моніторинг, що виявляє несподіваних слухачів на критичних портах.
Чекліст: Безпечна процедура «терміново звільнити порт»
- Зупиніть сервіс, що падає, щоб уникнути циклів перезапусків.
- Ідентифікуйте поточного власника через
ss/lsof. - Зупиніть його акуратно через супервізор (systemd/Docker), а не вбивайте.
- Тільки якщо чиста зупинка не допомогла: надішліть SIGTERM, почекайте, потім розгляньте SIGKILL.
- Документуйте зроблене і причину. Майбутній ви о 3:00 не згадає деталей.
Три міні-історії з корпоративного світу
Міні-історія 1: Інцидент через неправильне припущення
Команда мала простий rollout: розгорнути нове внутрішнє API на порті 8080 за існуючим зворотним проксі. Усі «знали», що 8080 — внутрішній порт додатків. Це було у чиємусь мозку і в дворічній вікі, якій ніхто не довіряв оновлювати.
Розгортання провалилося з «Address already in use». Онколінійний інженер зробив стандартну перевірку ss і побачив nginx, що слухає 8080. Це здавалося неможливим — nginx «володів» 80 і 443. Тож вони припустили, що ss показує застарілий артефакт, і перезавантажили вузол. (Все повернулося точно так само.)
Коли перестали перезавантажувати як спосіб розслідування, корінь був банально простим: попередня міграція тимчасово перемістила старий legacy-додаток з 80 на 8080, і nginx залишився слухати 8080 як редиректор. Це «тимчасове» тривало три квартали.
Виправлення не було «вбити nginx». Виправлення — архітектурне рішення: nginx — публічні порти; внутрішні додатки — виділені високі порти; 8080 не «за замовчуванням», а «виділено». Вони повернули редиректор під 80/443 і звільнили 8080 (точніше: перестали використовувати 8080 як магічну константу).
Насправді важлива пост-інцидент дія: реєстр портів, прив’язаний до власності сервісів. Не просто табличка — щось, що застосовується в рев’ю конфігів. Помилка перестала бути загадкою, бо власність перестала бути племінним знанням.
Міні-історія 2: Оптимізація, що обернулася проти
Інженер, що прагнув швидших рестартів при деплої, ввімкнув socket-активацію systemd для невеликого HTTP-сервісу, щоб systemd утримував сокет і передавав його новому процесу з меншим даунтаймом. Гарна ідея, в потрібних місцях.
Потім він забув, що зробив це. Через місяці інша команда спробувала розгорнути інший сервіс на тому ж порту під час консолідації. Вони зупинили старий сервіс, побачили, що він неактивний, і спробували запустити новий. Він упав з «Address already in use». ss показав PID 1, що утримує порт. Плутанина швидко зросла.
У корпоративній реальності інцидент був не лише технічним. Було запропоновано «вбити systemd, що утримує 8443». Коли хтось пропонує вбити init через конфлікт порту — ви в біді.
Справжнє виправлення було простим: зупинити і відключити .socket юніт, потім запустити новий сервіс. Урок: socket-активація — це функція деплою, а не тумблер «увімкнути і забути». Якщо її вмикаєте, ви несете відповідальність за експлуатаційну складність, яку вона додає, включно з тим, як «зупинено» сервіс тепер означає інше.
Потім вони зафіксували правило: socket-activated сервіси мають бути задокументовані у описі юніта і моніторитись алертом «сокет прив’язаний, хоча сервіс зупинено». Це нудна запобіжна міра, яка рятує від «оптимізацій», що стають таємницями.
Міні-історія 3: Нудна, але правильна практика, що врятувала
Сервіс поруч зі сховищем працював на флоті Debian, що також хостив метрики, лог-шіпери і пару legacy-демонів, яких ніхто не наважувався видаляти. Порти були мінним полем. Але команда мала одну нудну практику: перед будь-яким rollout вони запускали preflight-скрипт, що перевіряв невеликий список зарезервованих портів і верифікував очікуваного власника.
Під час планового вікна патчів новий контейнер метрик був деплоєний з дефолтним -p 9100:9100, бо автор чарта припустив, що Node Exporter завжди «всередині кластера». На цих хостах Node Exporter вже працював як systemd-сервіс. Контейнер забрав порт першим на підмножині вузлів через порядок планування. Rollout виглядав «майже гаразд», що ідеально для болючого удару.
Preflight зупинив це на першому вузлі в батчі: скрипт побачив, що 9100 більше не належить очікуваному юніту. Deploy призупинили, ще до того, як він зламав половину флоту — після того, як загрожував одному вузлу.
Виправлення зайняло хвилини: прибрати публікацію хост-порту з контейнера, використати існуючий Node Exporter і не дозволяти чарту претендувати на порт знову. Без драм, без перезавантажень, без war room. Просто маленька нудна перевірка, що запобігла великому нудному інциденту.
Ось як на практиці виглядає «операційна досконалість»: це не героїзм, це відмова пускати сюрпризи в продакшн.
FAQ
1) Чому «Address already in use» буває, коли нібито нічого не працює?
Зазвичай тому, що щось таки працює, просто не те, що ви очікуєте: socket-юніт systemd, проксі контейнера або слухач, прив’язаний по IPv6, а ви шукали IPv4. Перевірте ss по обох сім’ях і systemctl list-sockets.
2) Яка найкраща команда, щоб знайти, хто використовує порт на Debian 13?
ss -ltnp (TCP) і ss -lunp (UDP). Використовуйте точний фільтр типу 'sport = :8080', щоб не потонути у виводі.
3) Мені встановити net-tools для netstat?
Ні, якщо тільки вам не треба для звички під час інциденту і ви приймаєте компроміс. Debian 13 розрахований на iproute2-інструменти. Вивчіть ss; воно того варте.
4) Чи можуть дві програми слухати один TCP-порт?
Не так, як люди сподіваються. Є просунуті випадки з SO_REUSEPORT, де кілька воркерів ділять порт, але це зазвичай у межах одного дизайну сервісу. Для двох несумісних демонів — вважайте, що ні.
5) Чи перешкоджає TIME_WAIT моєму серверу прив’язати порт?
Майже ніколи для серверного listening-порту. TIME_WAIT частіше стосується закритих вихідних з’єднань. Якщо bind зазнає невдачі — знайдіть реального слухача або socket-юніт.
6) Чому ss показує, що порт належить systemd?
Socket-активація. systemd навмисно прив’язує порт через .socket юніт. Зупиніть/відключіть/mask-уйте socket-юніт, якщо хочете звільнити порт назавжди.
7) Як виправити конфлікт порту без перезавантаження?
Ідентифікуйте власника, зупиніть його через супервізор (systemd/Docker), відключіть шлях автозапуску (service/socket/container), потім запустіть потрібний сервіс і перевірте через ss.
8) Як визначити, чи конфлікт по IPv4 чи IPv6?
Перевірте обидві: ss -ltnp 'sport = :PORT' і ss -ltnp -6 'sport = :PORT'. Дивіться на 0.0.0.0 vs [::] і на специфічні адреси типу 127.0.0.1.
9) Якщо порт належить процесу користувача і мені потрібен назад?
Не починайте з вбивства. Ідентифікуйте користувача і сесію, узгодьте дії та встановіть політику: продакшен-порти — зарезервовані. Якщо потрібно швидко забрати — зупиніть процес SIGTERM, потім зробіть довготривале виправлення.
Висновок: кроки, що не розбудять вас вночі
«Address already in use» — не загадка. Це спір за власність. Ядро каже, що вже є орендар для тієї комбінації адреса/порт, і ваше завдання — з’ясувати, чи цей орендар легітимний.
Зробіть наступне:
- Стандартизуйте
ssдля перевірок власності портів і внесіть його в runbook. - Коли знаходите PID, завжди зіставляйте його з unit або контейнером. Виправляйте систему, що його запускає, а не тільки процес.
- Аудитуйте socket-activated сервіси і документуйте їх. Якщо PID 1 володіє портом — це, ймовірно, навмисно.
- Створіть реєстр виділення портів для вашого середовища (навіть невеликий). Застосовуйте його в рев’ю.
- Додайте легку перевірку, що верифікує критичні порти належними сервісам перед розгортанням.
Перезавантаження — для оновлень ядра та випадкових проблем з драйверами. Конфлікти портів потребують кращого підходу: доказів, чистого виправлення і майбутнього, коли вам не доведеться відкривати те саме о 2:00 ночі.