«Connection refused» — це прекрасно пряме повідомлення. Воно не означає, що ваш застосунок «трохи повільний сьогодні».
Воно означає, що хтось намагався відкрити TCP-з’єднання, і інша сторона відразу відповіла «ні».
Ніякого рукопотискання. Ніякого очікування. Просто зачинені двері.
У Docker-стеках ці двері зачиняються з передбачуваних причин: ви звертаєтеся не за тією адресою, ви в неправильній мережі,
ви потрапляєте на не той порт, сервер не слухає там, де ви думаєте, або щось посеред перешкоджає трафіку.
Ця стаття про те, як швидко довести, яка саме проблема — і виправити модель мережі замість того, щоб посипати ретраї, як святу воду.
Що насправді означає «connection refused» (і чого це не означає)
«Connection refused» — це спосіб TCP сказати: IP-адреса призначення доступна, але ніхто не слухає на цьому порту
(або щось активно відкинуло з’єднання TCP RST). Це зовсім інший режим помилки порівняно з:
- Таймаут: пакети зникають, маршрутивання зламане, фаєрвол відкидає, або сервіс завис і не відповідає.
- Помилка резолюції імен: ви навіть не отримуєте IP-адреси для цільового імені.
- Connection reset by peer: ви з’єдналися, а потім застосунок закрив з’єднання посеред сесії.
У Docker «refused» часто означає, що ви успішно підключилися не туди. Це звучить суперечливо, поки ви не усвідомите,
як часто розробники випадково цілеспрямовано влучають у localhost, або в публікований порт хоста зсередини тієї ж мережі,
або в IP контейнера, який змінився з минулого вівторка.
Ось правило, яке можна приклеїти до монітора: всередині контейнера «localhost» означає контейнер.
Якщо ви підключаєтеся з одного контейнера до іншого, «localhost» майже завжди невірний, якщо лише ви навмисно не запустили обидва процеси
в одному контейнері (а це окрема життєва філософія).
Ментальна модель мереж Docker, якою можна оперувати під тиском
Мережі Docker не «складні». Вони просто багаторівневі. Проблеми виникають, коли люди вгадують, який рівень винен.
Ми не будемо вгадувати. Ми доведемо це.
Рівень 1: Процес і сокет
Хтось повинен слухати на порті. Якщо ваш сервіс слухає на 127.0.0.1 всередині свого контейнера, інші контейнери не зможуть дістатися до нього.
Йому потрібно прив’язуватися до 0.0.0.0 (або до адреси інтерфейсу контейнера).
Рівень 2: Простір імен мережі контейнера
Кожен контейнер має власний простір імен мережі: свої інтерфейси, маршрути та loopback. Контейнери можуть бути приєднані до однієї або кількох мереж.
Docker створює пару veth, щоб з’єднати простір імен контейнера з бриджем (для bridge-мереж) або з оверлеєм (для Swarm).
Рівень 3: Docker-мережі (bridge/overlay/macvlan)
Мережа за замовчуванням «bridge» не те саме, що користувацька bridge-мережа. Користувацькі мережі забезпечують DNS-базоване виявлення сервісів.
Compose покладається на це. Якщо ви повертаєтеся до default bridge і починаєте хардкодити IP, ви пишете майбутні інциденти.
Рівень 4: Виявлення сервісів (Docker DNS)
У користувацьких мережах Docker запускає вбудований DNS-сервер. Контейнери зазвичай бачать його як 127.0.0.11 у /etc/resolv.conf.
Імена сервісів Compose резолвляться в IP-контейнерів у цій мережі. Якщо резолюція імен невірна, все вниз по ланцюжку перетворюється на хаос.
Рівень 5: Публікація портів хоста та NAT
ports: у Compose публікує порти контейнера на хості. Це для трафіку, що приходить ззовні Docker (ваш ноутбук, хост, інші машини).
Всередині Docker-мережі контейнери звичайно повинні спілкуватися між собою на порт контейнера через ім’я сервісу.
Якщо ви опиняєтеся в ситуації, коли контейнер A звертається до host.docker.internal:5432, щоби дістатися до контейнера B, зупиніться і запитайте:
«Навіщо я виходжу з Docker-мережі й знову заходжу через NAT?» Іноді це потрібно. Частіше — ні.
Один цитат, бо у ops‑світу є квитанції
Перефразована думка від Werner Vogels (надійність/архітектура): «Усе ламається; проектуйте, припускаючи відмову, і відновлюйте через автоматизацію.»
Це застосовується тут: припиніть сподіватися, що мережа поводитиметься правильно; проектуйте так, щоб її спостерігати та перевіряти.
План швидкої діагностики (перший/другий/третій)
Коли продукція горить, вам не потрібен ступінь філософії. Потрібна послідовність, яка звужує область пошуку.
Цей план створено для випадку «сервіс A не може підключитися до сервісу B» з повідомленням «connection refused».
Перший: підтвердьте, що ви дзвоните за правильною ціллю з контейнера-дзвінка
- Зсередини контейнера A: розв’яжіть ім’я, яке ви використовуєте.
- Підтвердіть, що IP належить мережі, якою ділиться контейнер B.
- Спробуйте TCP-підключення до потрібного порту.
Якщо резолюція імен провалюється або вказує кудись несподівано — зупиніться. Спочатку виправляйте DNS/членство в мережі.
Другий: підтвердьте, що серверний контейнер дійсно слухає на потрібному інтерфейсі і порту
- Всередині контейнера B: перелікуйте прослуховувані сокети.
- Перевірте, що сервіс прив’язаний до
0.0.0.0, а не до127.0.0.1. - Перевірте логи застосунку на предмет «запущено» проти «завалився і перезапускається».
Третій: інспектуйте мережеву проводку Docker і фільтрування на хості
- Перевірте прив’язки до мережі та IP-контейнерів.
- Перевірте правила iptables/nftables, якщо в шляхі задіяний хост (публіковані порти або трафік між просторами імен).
- Перевірте випадкову ізоляцію мережі (кілька проектів Compose, кілька мереж, неправильні псевдоніми).
Жарт №1: Якщо ваше рішення — «додати sleep 10», ви не вирішили мережу — ви просто домовилися з часом, а час завжди виставляє рахунки.
Практичні завдання: команди, очікуваний вивід, та рішення
Нижче — практичні завдання, які ви можете виконати на Linux-хості з Docker і Compose. Кожне завдання містить команду, реалістичний вивід,
що означає цей вивід, і рішення, яке потрібно прийняти. Це та частина, яку ви копіюєте в канал інциденту.
Завдання 1: Ідентифікуйте деталі невдалого підключення (з логів)
cr0x@server:~$ docker logs --tail=50 api
2026-01-03T09:12:41Z ERROR db: dial tcp 127.0.0.1:5432: connect: connection refused
2026-01-03T09:12:41Z INFO retrying in 1s
Значення: API-контейнер намагається дістатися Postgres за 127.0.0.1 в межах власного простору імен.
Якщо Postgres не працює в тому ж контейнері — це невірно.
Рішення: Змініть хост БД на ім’я сервісу Compose (наприклад db) і використайте порт контейнера (5432).
Завдання 2: Підтвердіть, що стан контейнера не бреше
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES STATUS PORTS
api Up 2 minutes 0.0.0.0:8080->8080/tcp
db Restarting (1) 5 seconds ago 5432/tcp
Значення: БД перезапускається. «Connection refused» може бути реальним (ніхто не слухає), а не проблемою маршруту.
Рішення: Припиніть налаштовувати клієнта. Спочатку виправте crash-loop серверного контейнера: перевірте логи DB і конфіг.
Завдання 3: Перевірте логи сервера на проблеми bind/listen
cr0x@server:~$ docker logs --tail=80 db
2026-01-03 09:12:32.177 UTC [1] LOG: listening on IPv4 address "127.0.0.1", port 5432
2026-01-03 09:12:32.177 UTC [1] LOG: listening on IPv6 address "::1", port 5432
2026-01-03 09:12:32.180 UTC [1] LOG: database system is ready to accept connections
Значення: Postgres слухає лише loopback всередині контейнера. Інші контейнери отримуватимуть «refused».
Рішення: Налаштуйте Postgres слухати на 0.0.0.0 (або інтерфейсі контейнера) і відповідно налаштуйте pg_hba.conf.
У контейнерах «безпечно прив’язувати до localhost» — це театральна безпека з побічними ефектами.
Завдання 4: Доведіть резолюцію імен зсередини контейнера-дзвінка
cr0x@server:~$ docker exec -it api getent hosts db
172.21.0.3 db
Значення: Docker DNS може резолвити db в IP. Це передумова для виявлення сервісів.
Рішення: Якщо це не вдається, ви в неправильній мережі або використовуєте неправильне ім’я. Виправляйте мережі/псевдоніми в Compose, а не застосунок.
Завдання 5: Протестуйте TCP-порт з контейнера-дзвінка (швидкий сигнал)
cr0x@server:~$ docker exec -it api bash -lc 'nc -vz -w2 db 5432; echo exit=$?'
nc: connect to db (172.21.0.3) port 5432 (tcp) failed: Connection refused
exit=1
Значення: Ви дісталися до IP контейнера, але порт відкинув з’єднання. Це майже завжди «ніхто не слухає» або «слухає тільки на loopback».
Рішення: Дослідіть прослуховувані сокети й адресу прив’язки серверного контейнера (наступне завдання).
Завдання 6: Перевірте, на що сервер слухає (всередині серверного контейнера)
cr0x@server:~$ docker exec -it db bash -lc 'ss -lntp | head -n 20'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=1,fd=6))
Значення: Postgres прив’язаний лише до 127.0.0.1:5432. Контейнери в одній мережі не зможуть підключитися.
Рішення: Переналаштуйте сервіс на прив’язку до 0.0.0.0 (або IP контейнера) і перезапустіть.
Завдання 7: Підтвердіть, що обидва контейнери на одній Docker-мережі
cr0x@server:~$ docker inspect -f '{{json .NetworkSettings.Networks}}' api
{"appnet":{"IPAMConfig":null,"Links":null,"Aliases":["api","api-1"],"NetworkID":"c0b3f6f2e2c1","EndpointID":"8d6d...","Gateway":"172.21.0.1","IPAddress":"172.21.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:15:00:02","DriverOpts":null}}
cr0x@server:~$ docker inspect -f '{{json .NetworkSettings.Networks}}' db
{"appnet":{"IPAMConfig":null,"Links":null,"Aliases":["db","db-1"],"NetworkID":"c0b3f6f2e2c1","EndpointID":"a21c...","Gateway":"172.21.0.1","IPAddress":"172.21.0.3","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:15:00:03","DriverOpts":null}}
Значення: Обидва в appnet з однаковим NetworkID. Отже маршрутизація між ними має бути коректною.
Рішення: Зосередьтеся на конфігуруванні прослуховування сервера і готовності застосунку, а не на дивностях мульти-мереж.
Завдання 8: Інспект мережевий об’єкт на предмет сюрпризів (підмережа, контейнери, опції)
cr0x@server:~$ docker network inspect appnet --format '{{json .IPAM.Config}} {{json .Containers}}'
[{"Subnet":"172.21.0.0/16","Gateway":"172.21.0.1"}] {"1c2f...":{"Name":"api","IPv4Address":"172.21.0.2/16"},"7aa9...":{"Name":"db","IPv4Address":"172.21.0.3/16"}}
Значення: Підтверджено підмережу та членство. Якщо DB не в списку — вона не на тій мережі, яку ви думаєте.
Рішення: Якщо членство невірне: виправте Compose, щоб приєднати сервіси до однієї користувацької мережі, і розгорніть заново.
Завдання 9: Виявлення проблеми «плутанини з публікованими портами»
cr0x@server:~$ docker port db
5432/tcp -> 0.0.0.0:15432
Значення: Контейнер DB експонує 5432 внутрішньо, опублікований як 15432 на хості.
Інші контейнери все одно мають використовувати db:5432, а не db:15432 і не localhost:15432.
Рішення: Якщо конфіг застосунку вказує на 15432 зсередини Docker — виправте це. Публіковані порти — для зовнішніх клієнтів.
Завдання 10: Перевірте маршрутизацію зсередини контейнера-дзвінка
cr0x@server:~$ docker exec -it api ip route
default via 172.21.0.1 dev eth0
172.21.0.0/16 dev eth0 proto kernel scope link src 172.21.0.2
Значення: Контейнер має маршрут до підмережі, де живе db. Якщо маршрут відсутній — ви приєднали неправильну мережу.
Рішення: Відсутній маршрут означає неправильне приєднання до мережі. Виправляйте мережі в Compose, не підправляйте /etc/hosts вручну.
Завдання 11: Перевірте, чи DNS-клієнт викликає Docker DNS
cr0x@server:~$ docker exec -it api cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
Значення: Використовується вбудований Docker DNS. Якщо ви бачите лише корпоративні DNS-сервери, резолюція імен сервісів може не працювати.
Рішення: Якщо Docker DNS не використовується, перевірте примусові налаштування DNS у конфігурації демона або в Compose.
Завдання 12: Доведіть, що сервіс доступний після виправлення прив’язки
cr0x@server:~$ docker exec -it db bash -lc 'grep -E "^(listen_addresses|port)" -n /var/lib/postgresql/data/postgresql.conf | head'
60:listen_addresses = '*'
64:port = 5432
cr0x@server:~$ docker restart db
db
cr0x@server:~$ docker exec -it api bash -lc 'nc -vz -w2 db 5432; echo exit=$?'
Connection to db (172.21.0.3) 5432 port [tcp/postgresql] succeeded!
exit=0
Значення: Ми перетворили «refused» у «succeeded» виправивши прослуховувач.
Рішення: Зафіксуйте зміну конфігурації, додайте readiness-перевірку і приберіть клієнтські «ретраї до кінця світу».
Завдання 13: Викрити міф «depends_on означає готовність»
cr0x@server:~$ docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
stack-api-1 api:latest "/app/api" api Up 20 seconds 0.0.0.0:8080->8080/tcp
stack-db-1 postgres:16 "docker-entrypoint..." db Up 22 seconds 5432/tcp
Значення: «Up» не означає «ready». Postgres може все ще виконувати міграції або відтворювати WAL. Клієнти бачать refused під час раннього завантаження.
Рішення: Додайте healthcheck для DB і затримайте старт API до готовності DB (або реалізуйте надійні ретраї з обмеженням).
Завдання 14: Перевірте статус здоров’я (коли додасте healthchecks)
cr0x@server:~$ docker inspect -f '{{.State.Health.Status}}' stack-db-1
healthy
Значення: Healthchecks дають надійний сигнал готовності. Ви можете використовувати це для оркестраційних рішень і алертингу.
Рішення: Якщо unhealthy: перестаньте звинувачувати мережу і виправляйте ініціалізацію DB, облікові дані, диск або конфіг.
Завдання 15: Помітити проблеми фаєрволу/NAT для публікованих портів (якщо задіяний хост)
cr0x@server:~$ sudo iptables -S DOCKER | head
-N DOCKER
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.21.0.2:8080
Значення: Docker вставляє правила для переадресації хост-портів на IP-контейнерів. Якщо цих правил немає — публіковані порти не працюватимуть.
Рішення: Якщо правила відсутні або ваше середовище використовує nftables, що перекриває Docker: узгодьте управління фаєрволом з Docker,
або свідомо оберіть інший режим мережі. Не «просто скиньте iptables» в продакшні, якщо вам подобаються несподівані аудити.
Завдання 16: Виявити випадкові множинні проекти Compose на окремих мережах
cr0x@server:~$ docker network ls --format 'table {{.Name}}\t{{.Driver}}\t{{.Scope}}' | grep -E 'stack|appnet'
NAME DRIVER SCOPE
stack_default bridge local
billing_default bridge local
Значення: Два проекти — дві default-мережі. Контейнер у stack_default не може дістатися до сервісів у billing_default за іменем.
Рішення: Приєднайте обидва стекі до загальної користувацької мережі (явно), або запустіть їх як один проект Compose, якщо вони пов’язані.
Три міні-історії з реальних інцидентів
Міні-історія 1: Інцидент через хибне припущення («localhost — це база даних»)
Команда продукту мігрувала моноліт у «мікросервіси» протягом кварталу. Це не була релігійна конверсія; це була стаття витрат.
Перший крок — запуск API і Postgres у Docker Compose локально, потім підняття цієї конфігурації в спільне середовище розробки.
У часи моноліту база даних жила на тій самій ВМ. Тому в конфігах скрізь було DB_HOST=localhost.
Під час міграції хтось помістив Postgres у контейнер, але залишив стару змінну, думаючи, що Docker «замапить» її.
Docker і замапив — прямо в неправильне місце.
Симптом був миттєвим: API-контейнери видавали connect: connection refused. Першою реакцією стало збільшення ретраїв,
бо команда недавно пережила cold starts у Kubernetes. Ретраї зросли з 3 до 30, і логи стали дорогою книгою.
Це все одно не допомогло, бо ви не зробите існування сокета eventual consistency.
Розв’язка настала, коли хтось запустив getent hosts всередині контейнера і помітив, що ім’я сервісу db резолвиться нормально.
Просто API його не використовував. Один зміна конфігу — DB_HOST=db — і інцидент закрито.
Урок не в «використовуйте імена сервісів». Урок у тому, що припущення — це технічний борг із датою погашення.
Міні-історія 2: Оптимізація, яка обернулась проти (публіковані порти для «продуктивності»)
Інша організація мала інтеграційне середовище на Compose. Старший інженер (компетентний, щиро) вирішив «спростити мережі»
через те, що сервіси спілкуються через хост-публікацію портів. Логіка звучала просто:
«Усе дивиться на IP хоста, ми маємо одну політику фаєрволу, і так буде легше дебажити».
Це працювало, доки не перестало. Під навантаженням вони почали бачити періодичні «connection refused» від сервісу A до B.
Відмови кластувалися під час деплоїв, але також випадали рандомно в години пікового навантаження. Це той тип поведінки, що змушує людей звинувачувати провайдера хмари,
ядро і іноді астрологію.
Насправді проблема була самостійно створеною складністю. Вхідний трафік робив петлю: контейнер A → NAT хоста → docker-proxy/iptables → контейнер B.
Під час перезапусків і churn IP часові вікна розширювалися. Деякі підключення потрапляли на відображення портів, яке тимчасово вказувало нікуди.
Крім того, внутрішні виклики тепер були прив’язані до адрес хоста, ускладнюючи горизонтальне масштабування і failover.
Вони повернулися до прямого трафіку сервіс‑до‑сервісу через користувацьку Docker-мережу, використовуючи імена сервісів і порти контейнерів.
Для дебагу й ingress вони залишили публіковані порти, але внутрішній трафік залишився внутрішнім. Їхня «оптимізація» виявилася відвідною дорогою через зайві складові.
Міні-історія 3: Нудна практика, що врятувала день (healthchecks і явні мережі)
Платформна команда тримала скромний стек Compose для внутрішніх інструментів: API, черга, Postgres і воркер. Нічого фантастичного.
А фантастичним було те, що вони поводилися з ним як із продакшном: явні користувацькі мережі, healthchecks і передбачувані імена сервісів.
Ніяких магічних дефолтів. Ніякого «на моєму ноуті працює».
Одної п’ятниці хост перезавантажився після рутинного оновлення ядра. Сервіси повернулися, але API почав одразу помилково відповідати.
Он-кол побачив «connection refused» і готувався до довгого вечора. Потім перевірив стан DB: starting.
Postgres відтворював WAL після некоректного вимкнення — нормально, але займає час.
Оскільки healthchecks були налаштовані, API контейнер не навалився на DB хвилею підключень.
Він зачекав. Логи були нудними. Алерти — змістовні. Через десять хвилин усе стало healthy і ніхто не писав панічного статусу.
Нудне — це досягнення. «Працює після ребута» — це не дефолт; це те, що ви інженеруєте.
Типові помилки: симптом → корінна причина → виправлення
Це повторювані патерни, що стоять за «connection refused» у Docker-системах. Кожен містить конкретну коригувальну дію.
Якщо ваша команда постійно повторює одну й ту ж помилку — винесіть її в lint‑правило або до чекліста рев’ю.
1) «API не може дістатися DB, але DB працює» → DB прив’язаний до loopback → прив’язка до 0.0.0.0
- Симптом:
connect: connection refusedвід інших контейнерів;ssпоказує127.0.0.1:5432. - Корінна причина: Сервіс слухає лише loopback всередині контейнера.
- Виправлення: Налаштуйте адресу bind/listen на
0.0.0.0(або IP контейнера) і додайте відповідні правила автентифікації (наприклад,pg_hba.confдля Postgres).
2) «Працює на хості, падає в контейнері» → використання localhost → використовуйте ім’я сервісу
- Симптом: Конфіг застосунку використовує
localhostабо127.0.0.1для іншого сервісу. - Корінна причина: Неправильне розуміння просторів імен мережі.
- Виправлення: Використовуйте ім’я сервісу Compose (наприклад
redis,db) і порт контейнера.
3) «Connection refused тільки під час старту» → готовність не гарантована → додайте healthchecks/затримки
- Симптом: Перші секунди/хвилини після деплою: refused; пізніше: OK.
- Корінна причина: Клієнт стартує раніше, ніж сервер починає слухати (або раніше, ніж готовий приймати з’єднання).
- Виправлення: Healthchecks та управління залежностями; або клієнтські ретраї з обмеженим експоненційним backoff і jitter.
4) «Можна допінгувати, але TCP відкидає» → мережа в порядку, порт ні → припиніть дебажити L3
- Симптом: IP досяжний, ARP/маршрути в порядку, але
ncповертає refused. - Корінна причина: Сервіс вимкнений, неправильний порт, неправильна прив’язка або падіння процесу.
- Виправлення: Перевірте
ss -lntpі логи сервісу; підтвердіть відображення портів і конфіг.
5) «Ім’я сервісу не резолвиться» → неправильна мережа/проект → приєднати до спільної користувацької мережі
- Симптом:
getent hosts dbне знаходить всередині контейнера. - Корінна причина: Контейнери знаходяться в різних мережах або в різних проектах Compose без спільної мережі.
- Виправлення: Оголосіть явну спільну мережу в Compose і приєднайте до неї обидва сервіси.
6) «Підключення до публікованого порту з іншого контейнера» → NAT-обхід → використовуйте внутрішній порт у мережі
- Симптом: Контейнер викликає
host:15432абоservice:15432, бо 15432 опублікований. - Корінна причина: Плутанина між ingress портами і внутрішніми портами.
- Виправлення: Для внутрішнього трафіку:
db:5432. Публікуйте порти лише для зовнішніх клієнтів.
7) «Переривчасті refused після redeploy» → застарілі припущення про IP → припиніть використовувати IP контейнерів
- Симптом: Закодований IP працює до перезапуску, потім refused/timeout.
- Корінна причина: IP контейнера міняються; конфіг застосунку цього не враховує.
- Виправлення: Використовуйте виявлення сервісів (імена), а не IP адреси контейнерів.
8) «Публікований порт недоступний зовні» → конфлікт фаєрволу/nftables → узгодьте фільтрацію хоста з Docker
- Симптом: Хост-порт змеплений, контейнер слухає, але зовнішні клієнти отримують refused.
- Корінна причина: Правила фаєрволу хоста перекривають DNAT/форвардинг Docker, або правила Docker не встановлені коректно.
- Виправлення: Виправте політику фаєрволу, щоб дозволити форвардинг; забезпечте інтеграцію ланцюгів Docker; уникайте двох конкурентних систем керування правилами.
Жарт №2: Мережі Docker не привидні; вони просто такими здаються, коли ви пропускаєте перевірку, в якому всесвіті ваші пакети перебувають.
Чеклісти / покроковий план
Покроково: від «refused» до кореневої причини за 10 хвилин
- Визначте точну ціль з логів клієнта: хост, порт, протокол. Якщо це
localhost, вважайте, що це невірно, поки не доведено протилежне. - Перевірте стан контейнера: чи сервер перезапускається або unhealthy?
- З контейнера клієнта: розв’яжіть ім’я сервера і зафіксуйте IP.
- З контейнера клієнта: спробуйте TCP-підключення через
ncдо імені сервера і порту. - З серверного контейнера: підтвердіть наявність listener за допомогою
ss -lntp. - Підтвердіть адресу прив’язки: якщо це
127.0.0.1, змініть на0.0.0.0(і підтягніть автентифікацію). - Підтвердіть членство в мережі: обидва контейнери на одній користувацькій мережі; інспект мережевого об’єкта.
- Усуньте плутанину з портами: внутрішні виклики використовують порт контейнера; зовнішні — публіковані порти.
- Лише потім перевіряйте фаєрвол/NAT на хості, якщо хост у шляху.
- Після виправлення: додайте healthchecks/readiness, приберіть «сонні» ретраї і документуйте мережевий контракт.
Чекліст для деплою: запобігти «refused» заздалегідь
- Використовуйте користувацьку мережу; не покладайтесь на default bridge.
- Використовуйте імена сервісів для внутрішнього трафіку; ніколи не хардкодьте IP контейнерів.
- Прив’язуйте мережеві сервіси до
0.0.0.0всередині контейнерів, якщо немає вагомої причини не робити цього. - Публікуйте порти лише для ingress; не маршрутизуйте внутрішній трафік через хост.
- Додайте healthchecks для stateful сервісів (БД, кеш, черга); використовуйте статус здоров’я для оркестрації.
- Реалізуйте обмежені ретраї з backoff і jitter для клієнтів; розглядайте це як стійкість, не як пластир.
- Тримайте політику фаєрволу узгодженою з Docker; уникайте двовладдя в правилах.
- Оголошуйте мережі Compose явними іменованими, коли кілька проектів мають спілкуватися.
Цікаві факти та історичний контекст (корисно, не тривіально)
- Факт 1: Ранні налаштування Docker значно покладалися на Linux-бриджі та iptables NAT; публіковані порти досі часто реалізуються через DNAT-правила на багатьох системах.
- Факт 2: Мережа за замовчуванням
bridgeісторично поводилася відмінно від користувацьких bridge-мереж, особливо щодо автоматичного DNS/виявлення сервісів. - Факт 3: Вбудований Docker DNS зазвичай відображається як
127.0.0.11всередині контейнерів на користувацьких мережах — діагностична золота деталь. - Факт 4: «Connection refused» зазвичай — це миттєвий TCP RST, тобто мережевий шлях до IP спрацював; кінцева точка відхилила порт.
- Факт 5: Імена сервісів Compose стали де-факто механізмом виявлення для dev/test задовго до того, як багато команд перейшли на «реальні» системи виявлення.
- Факт 6: IP контейнерів навмисно ефермерні; стабільну адресацію забезпечує іменування і виявлення, не прив’язування IP.
- Факт 7: Деякі дистрибутиви перейшли з iptables на nftables; невідповідність інструментів фаєрвола може спричиняти дивні помилки мережевого стеку Docker, якщо ланцюги не інтегровані правильно.
- Факт 8:
depends_onу Compose ніколи не давав гарантії готовності; це просто порядок старту. Вважайте «ready» властивістю застосунку. - Факт 9: Прив’язка до
127.0.0.1всередині контейнера — класична пастка, бо вона безшумно блокує весь зовнішній контейнерний трафік, виглядаючи «безпечною».
FAQ
1) Чому я отримую «connection refused» замість таймауту?
Refused зазвичай означає, що ви дісталися до IP призначення, і ядро відповіло скиданням, бо ніхто не слухає на тому порту
(або фаєрвол активно відхилив). Таймаути більше пов’язані з втраченими пакетами та зламаними шляхами.
2) Якщо обидва сервіси в Compose, чому вони не можуть поговорити автоматично?
Вони можуть, але лише якщо вони ділять мережу і ви використовуєте імена сервісів. Проблеми виникають, коли сервіси опиняються на різних мережах,
в різних проектах Compose, або клієнт налаштований на localhost чи на публікований порт хоста.
3) Чи варто використовувати IP контейнера заради продуктивності?
Ні. IP контейнерів змінюються. Резолюція імен не буде вашим вузьким місцем; ваш наступний аутейдж — так.
Використовуйте імена сервісів і довіряйте Docker DNS.
4) У чому різниця між expose і ports у Compose?
expose документує внутрішні порти і може впливати на поведінку лінків, але не публікує на хост.
ports публікує порти на інтерфейс хоста (зазвичай через NAT). Трафік між контейнерами не потребує публікованих портів.
5) Чи вистачає depends_on, щоб уникнути помилок стартового підключення?
Ні. Він запускає контейнери в порядку; він не гарантує, що залежність готова приймати з’єднання.
Використовуйте healthchecks і/або клієнтські ретраї з адекватним backoff.
6) Чому сервер слухає на 127.0.0.1 всередині контейнера?
Багато сервісів за замовчуванням прив’язуються до loopback «з міркувань безпеки». У контейнерах це часто блокує той трафік, який вам потрібен: інші контейнери.
Прив’язуйте до 0.0.0.0 і захищайтеся автентифікацією/ACL, а не ховаючись за loopback.
7) Чи можуть фаєрволи спричинити «connection refused» у Docker?
Так. Правила reject можуть генерувати RST/ICMP відповіді, що схожі на відмову. Частіше фаєрволи спричиняють таймаути, якщо пакети відкидаються.
Якщо ваш шлях включає публіковані порти або маршрути між хостами, перевірте інтеграцію фаєрвола хоста з Docker.
8) Чи мають контейнери дзвонити один до одного через публікований порт хоста?
Зазвичай ні. Це додає NAT, додаткові режими відмов і залежність від хоста. Використовуйте мережу сервіс‑до‑сервісу через спільну Docker-мережу.
Публікуйте порти для клієнтів поза Docker.
9) Чому на моєму локалі все працює, а в CI або на спільному сервері — ні?
Локальні налаштування часто мають менше мереж, менше проектів Compose і слабший фаєрвол. У CI/спільних середовищах імена мереж конфліктують,
сервіси стартують в іншому порядку, і базові правила фаєрволу можуть відрізнятися. Робіть мережі явними і додавайте healthchecks.
10) Що робити, якщо DNS резолвиться правильно, але я все одно отримую refused?
Тоді DNS — не ваша проблема. Refused вказує на прослуховувані сокети, адресу прив’язки, неправильний порт або падіння процесу.
Запустіть ss -lntp всередині серверного контейнера і перевірте, що порт слухає на 0.0.0.0.
Висновок: практичні наступні кроки
«Connection refused» — не загадка; це діагноз, який чекає, щоб його доповнили. Ваша робота — припинити ставитися до цього як до погодної події.
Доведіть ціль, доведіть резолюцію імен, доведіть членство в мережі, доведіть прослуховувач, і лише потім сперечайтеся про фаєрволи.
Наступні кроки, які окуплять роботу:
- Аудит всіх конфігів між сервісами на предмет
localhost, IP хоста і публікованих портів, які використовують всередині. Замініть на імена сервісів і порти контейнерів. - Додайте healthchecks для stateful сервісів і змусьте клієнтів коректно обробляти старт з обмеженим backoff.
- Зробіть мережі Compose явними, особливо коли кілька проектів мають спілкуватися.
- Стандартизуйте короткий runbook:
getent,nc,ss,docker network inspect. Прокачайте це в пам’ять м’язів.