Debian 13 «Address already in use»: знайдіть, хто займає порт (і виправте акуратно)

Було корисно?

Ви розгортаєте сервіс, він намагається прив’язатися, а ядро відповідає найменш корисною правдою в інформатиці: 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 керує переспрямуванням, але хост-порт все одно може виглядати зайнятим залежно від режиму.

Чистий робочий процес:

  1. Визначте, що проксі — слухач (ss показує docker-proxy).
  2. Зіставте його з контейнером (docker ps, docker inspect).
  3. Змініть опублікований порт або видаліть контейнер.

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 хвилини

  1. Прочитайте точний target прив’язки з логів (systemctl status або логи додатка). Зафіксуйте протокол, IP і порт.
  2. Запустіть ss -ltnp 'sport = :PORT' для TCP; ss -lunp 'sport = :PORT' для UDP.
  3. Якщо нічого немає — перевірте IPv6 явно з -6 і переконайтеся, що не помилилися в номері порту.
  4. Коли маєте PID/команду, зіставте з systemd unit через systemctl status і ps.
  5. Якщо слухач — systemd PID 1, перераховуйте сокети і знайдіть .socket юніт.
  6. Якщо слухач — docker-proxy/помістка контейнера, ідентифікуйте контейнер, що публікує порт.

Чекліст: Зробіть виправлення довготривалим

  1. Прийміть рішення, який сервіс має володіти портом (архітектурне рішення, не монетка).
  2. Впровадьте зміни конфігурації через drop-ins або керований конфіг, а не правкою vendor-файлів.
  3. Зупиніть/відключіть старого власника (service і socket, якщо релевантно).
  4. Запустіть очікуваного власника і перевірте через ss.
  5. Зробіть локальний тест з’єднання (curl, nc) і перевірте зовнішню маршрутизацію, якщо потрібно.
  6. Додайте моніторинг, що виявляє несподіваних слухачів на критичних портах.

Чекліст: Безпечна процедура «терміново звільнити порт»

  1. Зупиніть сервіс, що падає, щоб уникнути циклів перезапусків.
  2. Ідентифікуйте поточного власника через ss/lsof.
  3. Зупиніть його акуратно через супервізор (systemd/Docker), а не вбивайте.
  4. Тільки якщо чиста зупинка не допомогла: надішліть SIGTERM, почекайте, потім розгляньте SIGKILL.
  5. Документуйте зроблене і причину. Майбутній ви о 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» — не загадка. Це спір за власність. Ядро каже, що вже є орендар для тієї комбінації адреса/порт, і ваше завдання — з’ясувати, чи цей орендар легітимний.

Зробіть наступне:

  1. Стандартизуйте ss для перевірок власності портів і внесіть його в runbook.
  2. Коли знаходите PID, завжди зіставляйте його з unit або контейнером. Виправляйте систему, що його запускає, а не тільки процес.
  3. Аудитуйте socket-activated сервіси і документуйте їх. Якщо PID 1 володіє портом — це, ймовірно, навмисно.
  4. Створіть реєстр виділення портів для вашого середовища (навіть невеликий). Застосовуйте його в рев’ю.
  5. Додайте легку перевірку, що верифікує критичні порти належними сервісам перед розгортанням.

Перезавантаження — для оновлень ядра та випадкових проблем з драйверами. Конфлікти портів потребують кращого підходу: доказів, чистого виправлення і майбутнього, коли вам не доведеться відкривати те саме о 2:00 ночі.

← Попередня
Помилки wp-config.php у WordPress: поширені некоректні налаштування й виправлення
Наступна →
Історія Radeon: бренд, який пережив кілька епох

Залишити коментар