Docker «bind: address already in use»: знайдіть процес і виправте акуратно

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

Ви запускаєте docker run -p 8080:80 ... або піднімаєте стек Compose, і Docker відповідає операційним еквівалентом «ні»: bind: address already in use.
Раптом ви опинилися в інцидент-респонсі у вівторок, бо порт зайнятий і ніхто не пам’ятає чому.

Це одна з тих помилок, що здаються простими, поки не стають складними. Іноді це Nginx. Іноді — зомбі-контейнер, systemd socket,
обмеження rootless Docker або привид процесу, який помер, але залишив слухача. Давайте знайдемо справжнього власника, оберемо найменш шкідливе рішення
і залишимо хост чистішим, ніж знайшли.

Що насправді означає помилка (і чого вона не означає)

Коли Docker «публікує» порт (наприклад -p 8080:80), він просить ядро хоста про прослуховуючий сокет на хост-порті
(тут 8080). Ядро повертає або «гаразд», або «ні». «Ні», яке ви бачите, зазвичай — EADDRINUSE: інший сокет вже володіє тією
самою точною 4‑кою (протокол, локальна адреса, локальний порт).

Підступ у тому, що «вже використовується» ширше, ніж «якийсь інший додаток працює». Це може бути:

  • Інший процес, що слухає на цьому порту (nginx, apache, node, sshd тощо).
  • Інший контейнер, який вже опублікував цей хост-порт.
  • Системна служба, яка блокує порт через systemd socket activation.
  • Власний помічник Docker (історично docker-proxy), що тримає порт, навіть якщо ви вважаєте, що контейнер зник.
  • Прив’язка до конкретного IP, яка блокує пізнішу спробу прив’язатися до 0.0.0.0 (або навпаки).
  • Обмеження rootless Docker: прив’язка до привілейованих портів потребує додаткових кроків.

Чого це не означає: проблема з фаєрволом. Фаєрволи блокують трафік; вони не перешкоджають створенню прослуховуючих сокетів.
Якщо ви бачите «address already in use», ви майже завжди шукаєте слушача, залишковий проксі або конфігураційний конфлікт.

Хороша ментальна модель: публікація порту — це претензія. Docker хоче претендувати на порт на хості. Ядро каже, що хтось інший вже має
право власності. Потрібно знайти, хто його тримає, і вирішити: евікціювати, узгодити нову адресу або перемістити вашу службу.

Швидкий план діагностики

Ви під тиском часу. Добре. Виконайте мінімальні перевірки в правильному порядку. Не починайте «перезапускати Docker», як обряд вигнання.

По-перше: підтвердіть точний порт, протокол і адресу прив’язки

  • Подивіться на рядок помилки Docker. Це 0.0.0.0:80, 127.0.0.1:8080 чи специфічна IP-адреса інтерфейсу?
  • TCP vs UDP має значення. Два різні протоколи можуть використовувати той самий номер порту.

По-друге: ідентифікуйте фактичного власника слушача

  • ss -ltnp для TCP, ss -lunp для UDP. Це зазвичай закриває загадку.
  • Якщо бачите docker-proxy, простежте контейнер, який його породив.
  • Якщо бачите systemd або unit .socket, ви в зоні socket activation.

По-третє: оберіть чисте виправлення

  • Якщо це старий контейнер: зупиніть/видаліть контейнер, потім перезапустіть з потрібною картою портів.
  • Якщо це сервіс на хості: або перемістіть хост-сервіс, або змініть опублікований порт Docker, або поставте перед цим зворотний проксі.
  • Якщо це systemd socket activation: зупиняйте/відключайте сам unit .socket, а не тільки .service.
  • Якщо це rootless і низькі порти: використовуйте setcap, підходи на кшталт authbind або публікуйте вищі порти і фронтуйте їх проксі.

По-четверте: перевірте зовні

  • curl локально, потім з іншого хоста, якщо можливо.
  • Переконайтеся, що ви не «виправили» прив’язку, зламавши маршрутизацію (NAT, iptables/nftables) або доступність через IPv6.

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

Факти та історичний контекст (щоб ви не сперечалися з ядром)

Це здається довільним, поки ви не знаєте трохи історії й поведінки ядра. Ось конкретні факти, які мають значення під час дебагу:

  1. Порти належать по протоколу. TCP 443 і UDP 443 — окремі претензії. Зайнятий TCP-порт не блокує UDP з тим самим номером.
  2. Має значення адреса прив’язки. Процес може прив’язати 127.0.0.1:8080, не блокуючи інший процес, що прив’язався до 192.168.1.10:8080 — але прив’язка до 0.0.0.0:8080 блокує всі IPv4-адреси.
  3. Linux по-різному трактує SO_REUSEADDR і SO_REUSEPORT. Вони не означають «ігнорувати конфлікти»; вони керують семантикою повторного використання адрес і балансування навантаження.
  4. TIME_WAIT — це не прослуховуючий сокет. Це стан з’єднання; зазвичай він не заважає новому слухачу на цьому порті.
  5. Docker історично використовував userland proxy (docker-proxy) для опублікованих портів. Залежно від версії Docker і налаштувань, ви можете все ще його бачити, і він може тримати порти навіть коли мережа поводиться дивно.
  6. Перехід від iptables до nftables змінив режими відмов. Сучасні дистрибутиви запускають nftables під капотом; змішування інструментів може ускладнити розуміння доступності портів, хоча само по собі це не викликає EADDRINUSE.
  7. systemd socket activation може прив’язувати порт до старту служби. Слухач належить socket-unit, а не демон, який ви думаєте, що зупинили.
  8. Rootless Docker за замовчуванням не може прив’язувати привілейовані порти. Ядро диктує привілеї для низьких портів; є обхідні шляхи, але їх треба застосовувати свідомо.
  9. IPv6 додає паралельну поведінку прив’язки. Служба може прив’язати [::]:80 і також покривати IPv4 через v6-mapped адреси в залежності від net.ipv6.bindv6only.

Один парафраз надійності (бо я не буду вдавати, що точно цитую): парафраз: сподівання — не стратегія — приписується Edsger W. Dijkstra в операційних колах.
Суть: доведіть, хто володіє портом, перш ніж щось змінювати.

Практичні завдання: команди, виводи та рішення

Це практичні завдання, які можна виконати на Linux-хості. Кожне містить: команду, правдоподібний фрагмент виводу, що це означає, і рішення.
Виконуйте їх послідовно, коли не впевнені; вибирайте вибірково, коли впевнені.

Завдання 1: Відтворіть точну ціль прив’язки, яку Docker намагається захопити

cr0x@server:~$ docker run --rm -p 8080:80 nginx:alpine
docker: Error response from daemon: driver failed programming external connectivity on endpoint inspiring_morse (8f4c9b5b1b6a): Bind for 0.0.0.0:8080 failed: port is already allocated.

Значення: Docker намагався прив’язати TCP порт 8080 на всіх IPv4-інтерфейсах (0.0.0.0) і зазнав невдачі, бо порт вже зайнятий.

Рішення: Перестаньте гадати і знайдіть поточного власника TCP/8080.

Завдання 2: Ідентифікуйте процес-слухача за допомогою ss (швидко, сучасно)

cr0x@server:~$ sudo ss -ltnp 'sport = :8080'
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:8080      0.0.0.0:*    users:(("node",pid=2147,fd=23))

Значення: PID 2147 (node) володіє слухачем на TCP/8080 на всіх IPv4-інтерфейсах.

Рішення: Вирішіть, чи ця Node-служба має бути перенесена, зупинена або проксована через reverse proxy, поки Docker використовує інший порт.

Завдання 3: Показати повну таблицю слухачів, щоб впіймати припущення про «не той порт»

cr0x@server:~$ sudo ss -ltnp
State  Recv-Q Send-Q Local Address:Port   Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:22          0.0.0.0:*     users:(("sshd",pid=892,fd=3))
LISTEN 0      4096   127.0.0.1:5432      0.0.0.0:*     users:(("postgres",pid=1011,fd=6))
LISTEN 0      4096   0.0.0.0:8080        0.0.0.0:*     users:(("node",pid=2147,fd=23))
LISTEN 0      4096   [::]:80             [::]:*        users:(("nginx",pid=1320,fd=7))

Значення: Маєте суміш локальних і публічних слухачів; також зверніть увагу на IPv6 [::]:80.

Рішення: Якщо Docker намагається опублікувати 80, перевірте також IPv6-слухачів. Якщо це 8080 — конфлікт з Node.

Завдання 4: Використайте lsof, коли потрібен контекст файлової системи та рядка запуску

cr0x@server:~$ sudo lsof -nP -iTCP:8080 -sTCP:LISTEN
COMMAND PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
node    2147 app   23u  IPv4  56123      0t0  TCP *:8080 (LISTEN)

Значення: Підтверджує слухача та обліковий запис користувача, корисно для з’ясування, як його запускають (systemd unit? cron? PM2?).

Рішення: Якщо це «таємнича служба», тепер у вас є PID і користувач для трасування.

Завдання 5: Прослідкуйте PID до systemd unit (питання «хто це запустив?»)

cr0x@server:~$ ps -p 2147 -o pid,ppid,user,cmd
  PID  PPID USER     CMD
 2147     1 app      node /opt/api/server.js

cr0x@server:~$ systemctl status 2147
● api.service - Internal API
     Loaded: loaded (/etc/systemd/system/api.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2026-01-01 09:13:22 UTC; 1d 2h ago
   Main PID: 2147 (node)

Значення: Слухач — керована служба, а не випадковий термінал.

Рішення: Скоординуйте зміни: або оновіть api.service, щоб поміняти порти, або публікуйте Docker на іншому порту.

Завдання 6: Перевірте, чи Docker вже опублікував цей порт (конфлікт контейнерів)

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES            IMAGE            PORTS
web              nginx:alpine     0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
postgres         postgres:16      127.0.0.1:5432->5432/tcp

Значення: Інший контейнер вже володіє хост-портом 8080 і пробросив його на порт 80 контейнера.

Рішення: Не запускайте другу службу на тому самому хост-порті. Оберіть новий хост-порт або зупиніть/замініть існуючий контейнер.

Завдання 7: Знайдіть, який проєкт Compose володіє портом (коли контейнери мають ввічливі імена)

cr0x@server:~$ docker inspect -f '{{.Name}} {{range $p, $conf := .NetworkSettings.Ports}}{{$p}}={{(index $conf 0).HostIp}}:{{(index $conf 0).HostPort}} {{end}}' web
/web 80/tcp=0.0.0.0:8080

Значення: Підтверджує точно, який контейнер і відображення забрали хост-порт.

Рішення: Якщо ви планували заміну, виконайте контрольовану зміну (blue/green або stop-then-start), а не сперечайтеся з ядром.

Завдання 8: Якщо слухає docker-proxy, зіставте його з контейнером

cr0x@server:~$ sudo ss -ltnp 'sport = :8080'
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:8080      0.0.0.0:*    users:(("docker-proxy",pid=3012,fd=4))

cr0x@server:~$ ps -p 3012 -o pid,cmd
  PID CMD
 3012 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.3 -container-port 80

Значення: Проксі Docker утримує хост-порт і пересилає на IP контейнера. Контейнер може й існувати, а може й ні.

Рішення: Перевірте стан Docker; якщо контейнера немає, а проксі лишився, розглядайте це як баг Docker/мережі і обережно приберіть.

Завдання 9: Корелюйте IP контейнера з контейнером (коли у вас лише 172.17.0.3)

cr0x@server:~$ docker network inspect bridge -f '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'
web 172.17.0.3/16
postgres 172.17.0.4/16

Значення: Проксі пересилає на web.

Рішення: Якщо ви намагаєтесь запустити ще один «web», вам потрібен план rollout або інший хост-порт.

Завдання 10: Якщо це systemd socket activation, зупиніть unit сокета (не тільки сервіс)

cr0x@server:~$ sudo ss -ltnp 'sport = :80'
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:80        0.0.0.0:*    users:(("systemd",pid=1,fd=49))

cr0x@server:~$ systemctl list-sockets | grep -E '(:80|http)'
web.socket   0      4096   0.0.0.0:80   0.0.0.0:*    LISTENING

cr0x@server:~$ sudo systemctl stop web.socket
cr0x@server:~$ sudo systemctl disable web.socket
Removed "/etc/systemd/system/sockets.target.wants/web.socket".

Значення: PID 1 (systemd) володіє портом 80 через socket unit. Зупинка сервісу не звільнить порт; зупинка сокета звільнить.

Рішення: Відключіть сокет, якщо Docker потребує порту. Якщо сокет потрібен для хост-демона, не боріться — публікуйте Docker у іншому місці.

Завдання 11: Підтвердіть, чи IPv6 причетний до конфлікту

cr0x@server:~$ sudo ss -ltnp | grep ':80 '
LISTEN 0      4096   [::]:80      [::]:*    users:(("nginx",pid=1320,fd=7))

Значення: Хтось слухає на IPv6 порті 80. Залежно від sysctl і застосунку, це може також покривати IPv4.

Рішення: Якщо Docker намагається прив’язати 0.0.0.0:80 і зазнає невдачі, зупиніть/перенесіть IPv6-слухача або явно прив’яжіть служби до різних адрес.

Завдання 12: Використайте curl, щоб підтвердити, що саме обслуговує порт

cr0x@server:~$ curl -sS -D- http://127.0.0.1:8080/ -o /dev/null
HTTP/1.1 200 OK
Server: Express
Content-Type: text/html; charset=utf-8
Connection: keep-alive

Значення: Порт 8080 не «таємниче виділений»; він активно обслуговує Express-застосунок.

Рішення: Ви не звільните цей порт без впливу на когось. Плануйте контрольований перенос або встановіть reverse proxy.

Завдання 13: Коли підозрюєте застарілий контейнер, перелічіть зупинені контейнери й видаліть потрібний

cr0x@server:~$ docker ps -a --filter "status=exited" --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES          STATUS                    PORTS
web_old        Exited (137) 2 hours ago  0.0.0.0:8080->80/tcp

cr0x@server:~$ docker rm web_old
web_old

Значення: Навіть зупинені контейнери можуть залишати плутанину в головах людей. Відображення портів має зникнути після видалення контейнера,
але переконайтеся за допомогою ss.

Рішення: Якщо порт все ще зайнятий після видалення, ви маєте справу з проксі або демоном — продовжуйте розслідування, а не перезавантажуйте випадково.

Завдання 14: Переконайтеся, що Docker не завис із сиротою-проксі (рідко, але реально)

cr0x@server:~$ docker ps --filter "publish=8080"
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

cr0x@server:~$ sudo ss -ltnp 'sport = :8080'
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:8080      0.0.0.0:*    users:(("docker-proxy",pid=3012,fd=4))

Значення: Жоден працюючий контейнер не заявляє 8080, але docker-proxy усе ще його тримає. Це неконсистентний стан.

Рішення: Віддавайте перевагу таргетованому виправленню: перезапустіть Docker, щоб узгодити проброси портів, але робіть це свідомо (дренуйте трафік, попередьте зацікавлених, знайте зону ураження).

Завдання 15: Безпечний перезапуск Docker (коли без нього не обійтися)

cr0x@server:~$ sudo systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
     Active: active (running) since Thu 2026-01-01 08:11:09 UTC; 1d 3h ago

cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ sudo ss -ltnp 'sport = :8080'
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process

Значення: Слухач зник, що означає: проксі був сиротою, і рестарт демона прибрав його.

Рішення: Негайно знову розгорніть контейнер з потрібним відображенням портів, а потім перевірте шляхи трафіку. Також: відкрийте тикет, щоб з’ясувати, чому Docker відійшов від синхрону.

Завдання 16: Діагностика помилок прив’язки в rootless Docker (виглядає схоже, але причина інша)

cr0x@server:~$ docker context show
rootless

cr0x@server:~$ docker run --rm -p 80:8080 nginx:alpine
docker: Error response from daemon: driver failed programming external connectivity on endpoint hopeful_bardeen: Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: permission denied.

Значення: Це не «вже використовується»; це «permission denied», але оператори часто читають це як проблему з портом.
Rootless режим не може прив’язувати привілейовані порти (<1024) без допомоги.

Рішення: Або публікуйте високий порт (наприклад 8080) і фронтуйте його привілейованим проксі, або налаштуйте свідоме рішення на базі capability.

Другий короткий жарт, і повертаємося до роботи: Перезапуск Docker, щоб виправити конфлікт порту, — це як перезавантажити офіс, щоб знайти степлер. Працює, але ви наживете ворогів.

Вибір чистого виправлення: дерево рішень

«Чисте виправлення» — це не «kill -9 і далі по життю». Чисте — коли ви розумієте власність, вплив і повторність. Використайте це дерево рішень.

1) Чи належить порт хост-сервісу, який вам справді потрібен?

  • Так, і він має залишатися на цьому порту: Не публікуйте Docker на тому ж порту. Публікуйте на іншому хост-порту й використайте reverse proxy (або балансувальник) для маршрутизації.
  • Так, але його можна перемістити: Змініть конфігурацію хост-сервісу і розгорніть у контрольоване вікно. Оновіть моніторинг і правила фаєрволу.
  • Ні, це несподівано: Прослідкуйте, хто це запустив (PID → батько → unit файл або контейнер) і коректно видаліть.

2) Чи належить порт іншому контейнеру?

  • Це той самий сервіс (деплой заміни): Зробіть контрольовану підміну. У Compose це може означати зупинити старий сервіс перед запуском нового або використовувати різні публічні порти для blue/green.
  • Це інший сервіс: Узгодьте порти. Не створюйте систему «хто перший — той і переможе» без документування.

3) Чи власник — systemd через socket unit?

  • Так: Зупиніть/відключіть .socket unit або змініть його, щоб слухати на іншій адресі/порті. Зупинка .service сама по собі — лише театральний номер.

4) Це артефакт мережі Docker?

  • Ймовірно: Порівняйте уявлення Docker (docker ps, docker network inspect) з уявленням ядра (ss). Якщо вони не співпадають, перезапустіть Docker контрольовано.

5) Вам справді потрібно прив’язуватися до 0.0.0.0?

Публікація на всіх інтерфейсах зручна, але часто неправильна. Якщо служба призначена лише для локального доступу (метрики, admin UI),
прив’яжіть її до 127.0.0.1 або конкретного інтерфейсу керування і тримайте її поза публічним інтернетом.

Три корпоративні міні-історії (біль, уроки, паперова робота)

Міні-історія 1: Інцидент через неправильне припущення

Команда успадкувала «прості» хост: одну VM, встановлений Docker, кілька контейнерів і reverse proxy. Runbook казав, що застосунок живе на порту 8080.
Отже on-call припустив, що 8080 — це порт застосунку. Розумно. Неправильно.

Під час рутинного оновлення вони розгорнули новий контейнер з -p 8080:80 і отримали «port already allocated». Вони зробили те, що роблять люди під тиском:
обрали інший порт (8081), оновили reverse proxy і порахували справу закритою. Трафік відновився.

Через дві години спрацював інший алерт: внутрішні callback-и почали падати. Деяка апстрім-система мала хардкодний залежник на http://host:8080.
Ця залежність з’явилася, бо місяці тому хтось тимчасово обійшов reverse proxy, а «тимчасово» стало інфраструктурою.

Неправильне припущення було не про Docker. Воно було про власність і контракти інтерфейсів. Порт 8080 не був «застосунком». Це був «контракт, проти якого писали інші команди».
Коли вони це зрозуміли, рішення було нудним: повернути reverse proxy на 8080, перемістити внутрішню службу на інший порт і документувати контракт.

Насправді корисним було підтвердження: вони додали перевірку інвентаризації слухачів до деплоїв. Якщо власність порту змінюється, деплой фейлиться голосно до production.

Міні-історія 2: Оптимізація, що повернулася бумерангом

Інша організація вирішила «оптимізувати» хост-мережу. Вони хотіли менше рухомих частин, тож переключили деякі latency-sensitive сервіси на --network host.
Це усуває NAT і може зменшити оверхед. Але це також прибирає захисні рамки.

Перший місяць пройшов нормально. Потім розробник додав sidecar з дефолтним портом 9090, також у host networking.
На одній ноді 9090 вже використовувався експортером метрик. На іншій — ні. Тож rollout успішно пройшов «іноді».

Режим відмови був підступний: контейнер стартував на вузлах, де порт був вільний, і падав на інших з «bind: address already in use».
Або ж sidecar запускався першим і відбирав порт, а експортер метрик падав. Той самий порт, два сервіси, недетермінований порядок запуску.

«Оптимізація» не була априорі поганою, але вимагала дисципліни, якої в них не було: явного виділення портів, валідації на рівні ноди і політики проти дефолтних портів.
Вони відкотили host networking для більшості сервісів і залишили його лише там, де це було виправдано — і документували дозволені порти по нодах.

Міні-історія 3: Нудна, але правильна практика, що врятувала день

Платформа, пов’язана з платежами, тримала кілька Docker-хостів зі строгим change management. Нічого цікавого. Вони також мали просту звичку:
щовечора знімати знімок прослуховуючих сокетів і зіставляти їх з іменами unit-ів і контейнерів.

Одного ранку хост почав відхиляти деплой через знайому помилку прив’язки порту. On-call витягнув нічний знімок і порівняв зі «зараз».
Порт 443 мав нового слухача, який належав systemd socket unit, що не був у базовому наборі. Це звузило пошук від «усе на хості» до «недавні системні зміни».

Винуватцем виявилося оновлення пакета, яке увімкнуло дефолтний socket unit для вбудованого web UI. Сервіс навіть не запускався — systemd тримав порт заздалегідь.
Без знімка вони б гонялися за Docker мережею та iptables годину.

Виправлення було тривіальним: відключити несподіваний socket unit, перезапустити деплой і додати пакетну пінг до тих пір, поки UI не перерозмістять.
Практика була нудною і ефективною: знати, хто слухає, і знати, коли це змінюється.

Типові помилки: симптом → корінна причина → виправлення

Це шаблони, що регулярно з’являються в продакшні. Симптоми часто ідентичні. Виправлення — ні.

1) Симптом: Docker каже «port is already allocated», але ви не знаходите процес

  • Корінна причина: Слухач лише на IPv6 ([::]:PORT) або прив’язаний до конкретного IP, який ви не перевірили, або його тримає docker-proxy.
  • Виправлення: Виконайте ss -ltnp без фільтрації спочатку, потім перевірте IPv4 і IPv6. Якщо це docker-proxy, зіставте його з контейнером через рядок запуску або network inspect.

2) Симптом: Ви зупинили сервіс, але порт усе ще зайнятий

  • Корінна причина: systemd socket activation. .socket unit володіє портом, а не демон, який ви зупинили.
  • Виправлення: Зупиніть/відключіть socket unit: systemctl stop something.socket і systemctl disable something.socket.

3) Симптом: Compose deploy працює на одній ноді, падає на іншій

  • Корінна причина: Сховані споживачі портів відрізняються між хостами (експортери метрик, локальні dev-сервіси, дефолти дистрибутиву).
  • Виправлення: Уніфікуйте базові слухачі і перевіряйте їх у CI/CD preflight. Або перестаньте публікувати фіксовані порти по хостах і помістіть сервіси за reverse proxy.

4) Симптом: Ви «виправили» прив’язку, змінивши на випадковий високий порт, а потім downstream падає

  • Корінна причина: Номер порту був частиною контракту (хардкодні клієнти, правила фаєрволу, allowlists, перевірки моніторингу).
  • Виправлення: Відновіть контрактний порт і перемістіть конфліктний сервіс. Якщо змінюєте контракт, координуйте і версіонуйте це як API.

5) Симптом: Docker публікує на IPv4, але сервіс доступний лише на localhost (або навпаки)

  • Корінна причина: Невідповідність прив’язок: хост прив’язаний до 127.0.0.1, а клієнти очікують зовнішнього доступу, або процес у контейнері слухає лише 127.0.0.1 замість 0.0.0.0.
  • Виправлення: Узгодьте рівні прив’язки. В контейнерах переконайтеся, що процес слухає 0.0.0.0. Для публікації на хості використовуйте потрібний хост-IP у -p (наприклад -p 127.0.0.1:8080:80).

6) Симптом: Після видалення контейнера порт залишається зайнятим

  • Корінна причина: Сирота docker-proxy або завислий стан демона.
  • Виправлення: Підтвердіть, що жоден контейнер не публікує порт. Якщо ні — перезапустіть Docker контрольовано і повторно перевірте слухачів. Якщо проблема лишається, дослідіть процеси та логи Docker.

7) Симптом: «bind: permission denied» при спробі публікації порту 80 в rootless Docker

  • Корінна причина: Обмеження привілейованих портів у rootless режимі.
  • Виправлення: Публікуйте високий порт і фронтуйте його привілейованим reverse proxy, або реалізуйте deliberate capability-based підхід (й задокументуйте наслідки в плані безпеки).

8) Симптом: Не вдається прив’язати до 0.0.0.0:PORT, але прив’язка до 127.0.0.1:PORT працює

  • Корінна причина: Хтось уже володіє wildcard-прив’язкою (0.0.0.0) на цьому порту, або ваша служба намагається захопити wildcard, а інша служба прив’язана до конкретної адреси з ексклюзивними прапорами.
  • Виправлення: Перевірте список слухачів з адресами. Вирішіть, чи вам потрібен порт на всіх інтерфейсах; прив’яжіть явно до потрібного IP або перемістіть конфліктну службу.

Контрольні списки / покроковий план

Чекліст A: Коли потрібно просто, щоб Docker стартував (без перекладання проблем на інших)

  1. Прочитайте рядок помилки. Занотуйте протокол, адресу та порт (0.0.0.0:PORT vs 127.0.0.1:PORT).
  2. Запустіть sudo ss -ltnp 'sport = :PORT' (або UDP-еквівалент) і знайдіть PID/процес.
  3. Якщо це контейнер: docker ps і знайдіть порт-мепінг; зупиніть/видаліть правильний контейнер.
  4. Якщо це сервіс на хості: знайдіть unit через systemctl status PID; вирішіть перемістити чи зупинити.
  5. Якщо це systemd socket: зупиніть/відключіть .socket unit.
  6. Перезапустіть Docker з явною адресою прив’язки, якщо потрібно (loopback-only для внутрішніх інструментів).
  7. Перевірте за допомогою curl локально і з пір-хоста, якщо служба призначена для зовнішнього доступу.

Чекліст B: Чисте виправлення для production (те, що ви зможете пояснити пізніше)

  1. Зафіксуйте поточні слухачі: зробіть знімок ss -ltnp перед змінами.
  2. Ідентифікуйте власність: зіставте PID з unit/контейнером і задокументуйте в change ticket.
  3. Визначте контракт-порт: які клієнти на нього залежать? Чи є він у фаєрволі, allowlists, моніторингу або DNS?
  4. Виберіть стратегію:
    • Перенести опублікований порт контейнера.
    • Перемістити порт хост-сервісу.
    • Впровадити reverse proxy і зберегти зовнішній порт стабільним.
    • Прив’язувати служби до різних інтерфейсів (публічний vs management).
  5. Розгортайте з планом відкату: «Якщо X не вдається, відновіть Y listener і відкотуйте конфіг Z.» Не опціонально.
  6. Валідація після зміни: слухач існує, застосунок відповідає, моніторинг проходить, і немає несподіваних нових слухачів.
  7. Запобігання повторенню: додайте preflight-перевірку портів у CI/CD або при підготовці хостів.

Чекліст C: План для Compose (порти — це політика, не декор)

  1. Знайдіть мапінг у docker-compose.yml під ports:.
  2. Перевірте дублікати між сервісами (поширена помилка copy/paste).
  3. Переконайтеся, що цільові порти не використовуються іншими стекми на тому ж хості.
  4. Якщо потрібні кілька інстансів одного стеку, не повторно використовуйте ті самі хост-порти. Параметризуйте їх.
  5. Розгляньте варіант повністю прибрати публікацію портів для внутрішніх сервісів і з’єднуватися через Docker network.

Поширені питання

1) Чому Docker каже «port is already allocated», а не показує процес?

Docker повідомляє помилку ядра з виклику bind. Ядро не надає «дружніх деталей власності» у відповіді syscall. Використовуйте ss або lsof, щоб ідентифікувати PID-власника.

2) Я запускали netstat і він нічого не показав, але Docker все одно падає. Чому?

Ви могли перевіряти не той протокол (UDP vs TCP), не ту адресну сім’ю (IPv4 vs IPv6) або фільтрувати неправильно.
Також деякі збірки netstat не показують власника процесу без root. Краще ss -ltnp з sudo.

3) Чи можуть два контейнери публікувати той самий хост-порт, якщо прив’язуються до різних IP?

Так, якщо ви явно публікуєте на різні хост-IP: один прив’язується до 127.0.0.1:8080, інший — до 192.168.1.10:8080.
Якщо будь-хто прив’язується до 0.0.0.0:8080, це блокує всі IPv4-адреси для цього порту.

4) Чи безпечно просто перезапустити Docker, щоб це виправити?

Іноді це очищує сирітські проксі або узгоджує стани. Але це також перериває працюючі контейнери і може розірвати з’єднання.
У production ставте це в один ряд з перезапуском сервісу в change window. Якщо можна виправити реального власника — робіть те.

5) Що робити, якщо порт тримає systemd (PID 1)?

Зазвичай це .socket unit. Зупиніть і відключіть socket unit. Зупинка пов’язаного сервісу не достатня, бо systemd тримає прослуховуючий сокет.

6) Чи може TIME_WAIT спричинити «address already in use»?

Не для типового bind-слухача. TIME_WAIT стосується нещодавно закритих з’єднань.
Звичайне «address already in use» під час публікації портів Docker — це реальний слухач, а не хірургія із TIME_WAIT.

7) Я використовую rootless Docker і не можу опублікувати порт 80. Це те саме?

Ні. Зазвичай це «permission denied» через правила привілейованих портів. Чисте виправлення — публікувати високий порт і фронтувати його reverse proxy,
або явно налаштувати capability-based підхід (та усвідомлювати наслідки безпеки).

8) Чому Compose іноді падає під час rolling updates через конфлікти портів?

Compose не робить rolling updates як оркестратори. Якщо ви намагаєтесь одночасно запустити «старі» та «нові» контейнери з тими самими опублікованими портами,
другий не зможе прив’язатися. Використовуйте різні порти для blue/green, або зупиніть старий контейнер перед стартом нового.

9) Як уникнути цього назавжди?

Ви не уникнете повністю. Але можна зменшити сюрпризи:
тримайте документ розподілу портів по хосту (або по середовищу), використовуйте reverse proxy для стабільних зовнішніх портів і додавайте preflight-перевірку, що фейлиться, коли потрібні порти вже зайняті.

10) Чи створює Kubernetes NodePort таку ж проблему «address already in use»?

Може бути, але володіння змінюється: kube-proxy і node-рівневі слухачі можуть резервувати порти. Метод діагностики той самий: перевірте слухачі за допомогою ss,
а потім зіставте власника з компонентом оркестратора.

Висновок: наступні кроки, що не розбудять вас о 3 AM

«bind: address already in use» — це ядро, яке робить свою роботу. Ваша робота — перестати ставитися до цього як до випадкового Docker-іспадку.
Ідентифікуйте слухача за допомогою ss, зіставте його з unit або контейнером і оберіть виправлення, що враховує контракти і зону ураження.

Практичні наступні кроки:

  • Додайте preflight для деплоїв, що перевіряє потрібні хост-порти за допомогою ss і фейлиться раніше.
  • Перестаньте публікувати все на 0.0.0.0 за замовчуванням. Прив’язуйте внутрішні інструменти до loopback.
  • Задокументуйте, яка служба «володіє» кожним зовнішньо релевантним портом, і ставте такі зміни як зміни API.
  • Якщо використовуються systemd socket unit-и, проведіть їх аудит — порти можуть бути зарезервовані навіть коли «сервіс зупинено».
  • Коли стан Docker і стан ядра не співпадають, перезапускайте Docker лише як контрольовану операцію, а не з магічних причин.
← Попередня
Облік місця в ZFS: чому du і zfs list не збігаються
Наступна →
Therac-25: коли збої програмного забезпечення вбивали пацієнтів

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