Ви запустили контейнер. Ви опублікували порт. docker ps з гордістю показує 0.0.0.0:8080->80/tcp.
І все одно ваш браузер таймаутить, ніби чекає автобус під дощем.
«Порт опубліковано, але недоступний» — це одна з тих помилок, що змушує розумних людей робити дурні речі: перезапускати Docker, гукати випадкові правила firewall,
перебирати образи та шепотіти погрози NAT. Припиніть це. Це детермінований механізм. Якщо опублікований порт не досяжний,
значить щось конкретне блокує пакет або додаток не слухає там, де ви думаєте.
Ментальна модель: що насправді означає «опубліковано»
Коли ви публікуєте порт через Docker (-p 8080:80), ви не «відкриваєте порт всередині контейнера».
Ви просите Docker Engine організувати трафік так, щоб пакети, що приходять на порт хоста (8080), були переспрямовані на порт контейнера (80).
Це переспрямування може реалізовуватися кількома способами залежно від платформи та режиму:
- Linux з правами root (класичний): правила NAT iptables/nftables (DNAT) плюс невеликий проксі в просторі користувача в деяких сценаріях.
- Linux rootless Docker: часто використовує
slirp4netns/ пересилання у просторі користувача, з іншими обмеженнями та характеристиками продуктивності. - Docker Desktop (Mac/Windows): там працює віртуальна машина, і пересилання портів проходить через межу хост ↔ VM, з додатковим шаром «забавності».
«Опубліковано» означає, що Docker створив намір. Воно не гарантує, що пакет виживе:
фаєрвол хоста, cloud security group, неправильне прив’язування до інтерфейсу, відсутній маршрут, невідповідність зворотного проксі або процес, який слухає лише на 127.0.0.1,
— усе це може дати той самий симптом: порт, що виглядає відкритим, але поводиться як цегляна стіна.
Діагностична хитрість — перестати сприймати це як «Docker-мережі» й почати розглядати як шлях:
клієнт → мережа → інтерфейс хоста → фаєрвол → NAT → контейнерний veth → процес у контейнері.
Знайдіть перше місце, де реальність відрізняється від очікувань.
Одна перефразована ідея від Werner Vogels (CTO Amazon): усе рано чи пізно ламається; проєктуйте так, щоб можна було швидко виявляти, ізолювати та відновлюватися
.
Опубліковані порти не виняток — інструментуйте шлях, і правда з’явиться.
Швидкий план діагностики (перший/другий/третій)
Перший: підтвердіть, що сервіс дійсно слухає (всередині контейнера)
Якщо додаток не слухає на порті, який ви зміапінгували, Docker може весь день пересилати пакети, а ви все одно отримаєте таймаути або скиди.
Не починайте з iptables. Починайте з процесу.
Другий: перевірте, що хост слухає і пересилає (на правильному інтерфейсі)
Перевірте, що порт хоста зв’язаний, на якому інтерфейсі та чи вставив Docker очікувані правила NAT.
Якщо хост не слухає, або слухає лише на 127.0.0.1, віддалені клієнти не підключаться.
Третій: усуньте «зовнішні блоки» (фаєрвол, cloud SG, маршрутизація)
Якщо localhost працює, а ззовні ні — перестаньте звинувачувати Docker. Це проблема периметра: UFW/firewalld/nftables політика, cloud security group,
маршрутизація хоста або балансувальник навантаження, що робить health check не туди.
Четвертий: перевірте спеціальні режими та крайові особливості
Rootless Docker, IPv6, host networking, macvlan, overlay мережі, зворотні проксі й hairpin NAT кожен мають гострі краї.
Якщо ви в одному з цих режимів — будьте явними та дотримуйтесь відповідної гілки чекліста нижче.
Жарт №1: NAT як офісна політика — усі кажуть, що розуміють її, а потім ви бачите, як вони звинувачують принтер.
Практичні завдання: команди, виводи, рішення (12+)
Це перевірки продакшн-рівня. Кожна містить команду, типовий вивід, що це означає, і рішення, яке ви приймаєте далі.
Виконуйте їх по порядку, доки не знайдете перше «неправильне» місце. Саме там ви зупиняєтесь і виправляєте.
Завдання 1: Підтвердіть відображення, яке Docker думає, що створив
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES PORTS
web-1 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
db-1 5432/tcp
Значення: Docker стверджує, що опублікував 8080 на всіх IPv4 інтерфейсах і також IPv6.
Якщо ви бачите лише 127.0.0.1:8080->80/tcp, це означає прив’язку до localhost і віддалені з’єднання не пройдуть.
Рішення: Якщо відображення неправильне — виправте конфігурацію запуску/compose спочатку. Якщо виглядає правильно — продовжуйте.
Завдання 2: Перевірте прив’язки портів контейнера (джерело істини)
cr0x@server:~$ docker inspect -f '{{json .NetworkSettings.Ports}}' web-1
{"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}
Значення: Конфігурація контейнера говорить: хост 0.0.0.0:8080 переспрямовується на контейнер 80/tcp.
Якщо для порту з’являється null, він не опублікований.
Рішення: Якщо прив’язка не та, що ви очікували — розгорніть заново з правильним -p або ports: у Compose.
Завдання 3: Переконайтеся, що сервіс слухає всередині контейнера
cr0x@server:~$ docker exec -it web-1 sh -lc "ss -lntp | sed -n '1,6p'"
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:(("nginx",pid=1,fd=6))
Значення: Щось (nginx) слухає на 0.0.0.0:80 всередині контейнера.
Якщо ви бачите лише 127.0.0.1:80, це зазвичай все одно працює, бо трафік прибуває локально в контейнері,
але деякі додатки прив’язуються дивно — тільки IPv6 або сокети UNIX.
Рішення: Якщо нічого не слухає — виправте додаток/контейнер (невірна команда, crash loop, конфігурація).
Якщо слухає — рухайтесь далі.
Завдання 4: Curl до сервісу зсередини контейнера
cr0x@server:~$ docker exec -it web-1 sh -lc "apk add --no-cache curl >/dev/null 2>&1; curl -sS -D- http://127.0.0.1:80/ | head"
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:12 GMT
Content-Type: text/html
Значення: Додаток відповідає локально. Якщо це не вдається — зупиніться. Публікація портів не виправить зламаний додаток.
Рішення: Якщо внутрішній curl не вдається — діагностуйте додаток. Якщо вдається — рухайтесь назовні.
Завдання 5: Curl через IP контейнера з хоста
cr0x@server:~$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web-1
172.17.0.4
cr0x@server:~$ curl -sS -D- http://172.17.0.4:80/ | head
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:20 GMT
Content-Type: text/html
Значення: Хост може дістатися контейнера через bridge-мережу. Якщо це не вдається — проблема всередині мережі хоста/контейнера
(bridge вимкнений, policy routing, ланцюг DOCKER-USER, модулі безпеки, або контейнер приєднаний до іншої мережі).
Рішення: Якщо хост → IP контейнера не працює — інспектуйте docker network, iptables та політики хоста. Якщо працює — перевіряйте шлях публікації.
Завдання 6: Curl через опублікований порт хоста з хоста
cr0x@server:~$ curl -sS -D- http://127.0.0.1:8080/ | head
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:28 GMT
Content-Type: text/html
Значення: Перенаправлення портів працює локально. Якщо localhost працює, а ззовні ні — ви в зоні фаєрвола/інтерфейсу.
Рішення: Якщо localhost не працює — перевірте слухання на хості та правила NAT наступними. Якщо localhost працює — переходьте до перевірки периметра.
Завдання 7: Подивіться, що фактично слухає на порті хоста
cr0x@server:~$ sudo ss -lntp | grep ':8080'
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=2314,fd=4))
Значення: На хості є слушач, часто docker-proxy. На новіших налаштуваннях ви можете не побачити docker-proxy,
тому що DNAT достатньо; тоді ss може нічого не показувати, хоча все працює.
Рішення: Якщо ви бачите тільки 127.0.0.1:8080, виправте прив’язку (ports у compose або явний IP хоста).
Якщо нічого не видно і це не працює — перевірте правила NAT та конфіг Docker daemon.
Завдання 8: Підтвердіть, що Docker вставив правила NAT (огляд iptables у legacy)
cr0x@server:~$ sudo iptables -t nat -S DOCKER | sed -n '1,6p'
-N DOCKER
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.4:80
Значення: Пакети, що прибувають на TCP/8080 на хості з інтерфейсів, відмінних від docker0, DNAT-яться на IP контейнера/80.
Якщо правило відсутнє — Docker не програмує NAT (поширено для rootless режиму, спеціальних прапорів демона або несумісності з nftables).
Рішення: Якщо правила відсутні або неправильні — виправте бекенд мережі Docker або перезапустіть Docker з правильною інтеграцією iptables.
Завдання 9: Перевірте ланцюг DOCKER-USER (ланцюг «ви самі себе заблокували»)
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j DROP
Значення: Цей хост відкидає пересланий трафік до того, як спрацюють власні правила Docker. Це робить опубліковані порти недоступними з мережі,
хоча localhost може все ще працювати (залежно від шляху).
Рішення: Замініть масове відкидання на явні правила дозволу або перемістіть політику в контрольований шар фаєрволу, що враховує Docker.
Завдання 10: Якщо ви використовуєте nftables, перегляньте ruleset (сучасний погляд)
cr0x@server:~$ sudo nft list ruleset | sed -n '1,40p'
table ip nat {
chain PREROUTING {
type nat hook prerouting priority dstnat; policy accept;
tcp dport 8080 dnat to 172.17.0.4:80
}
chain OUTPUT {
type nat hook output priority -100; policy accept;
tcp dport 8080 dnat to 172.17.0.4:80
}
}
Значення: DNAT існує в prerouting і output (локальні підключення).
Якщо правило є лише в OUTPUT — віддалений трафік не буде переспрямований; якщо лише в PREROUTING — поведінка localhost може відрізнятись.
Рішення: Переконайтеся, що Docker правильно інтегрується з nftables і ви не змішуєте несумісні бекенди iptables.
Завдання 11: Перевірте kernel forwarding та bridge netfilter налаштування
cr0x@server:~$ sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables 2>/dev/null
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
Значення: Пересилання увімкнене, і трафік з bridge видимий для iptables.
Деякі хардовані базові образи відключають це, а потім дивуються, чому контейнери недоступні.
Рішення: Якщо ip_forward=0 і ви очікуєте маршрутизацію/NAT — увімкніть його (і задокументуйте в базовому образі).
Завдання 12: Перевірте стан UFW і чи він мовчки блокує Docker
cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
Значення: «deny (routed)» — класичний вбивця контейнерів, якщо ви покладаєтеся на пересланий трафік.
UFW може блокувати bridge forwarding навіть якщо ви дозволили порт на хості.
Рішення: Налаштуйте UFW, щоб дозволити routed трафік для Docker-мереж, або керуйте фаєрволом явними правилами iptables/nft.
Завдання 13: Перевірте зони firewalld і masquerade (поширено на RHEL/CentOS)
cr0x@server:~$ sudo firewall-cmd --state
running
cr0x@server:~$ sudo firewall-cmd --get-active-zones
public
interfaces: eth0
docker
interfaces: docker0
Значення: firewalld може помістити docker0 у свою зону. Якщо ця зона забороняє forwarding/masquerade, опубліковані порти ламаються.
Рішення: Переконайтеся, що налаштування зони docker дозволяють пересилання за потреби, або уніфікуйте зони усвідомлено.
Завдання 14: Протестуйте з віддаленого хоста і порівняйте шлях
cr0x@server:~$ curl -sS -m 2 -D- http://$(hostname -I | awk '{print $1}'):8080/ | head
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:39 GMT
Content-Type: text/html
Значення: Це імітує «не localhost», використовуючи IP хоста. Якщо це не вдається, але 127.0.0.1 працює,
то прив’язка/фаєрвол/маршрут відрізняються між loopback і зовнішнім інтерфейсом.
Рішення: Якщо не працює — перевірте IP прив’язки та фаєрвол по інтерфейсу. Якщо вдається — проблема може бути за межами хоста (cloud SG, LB, маршрут клієнта).
Завдання 15: Захоплення пакетів на хості, щоб побачити чи приходить SYN
cr0x@server:~$ sudo tcpdump -ni eth0 tcp port 8080 -c 5
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:02:01.123456 IP 203.0.113.10.51922 > 192.0.2.20.8080: Flags [S], seq 123456789, win 64240, options [mss 1460,sackOK,TS val 1 ecr 0,nop,wscale 7], length 0
Значення: Якщо ви бачите SYN-и, мережевий шлях до хоста в порядку. Якщо нічого не видно — проблема upstream
(security group, NACL, роутер, балансувальник, DNS вказує не туди).
Рішення: Немає SYN — припиніть дебаг Docker і рухайтесь назовні. SYN є — продовжуйте діагностику фаєрвола/NAT/відповіді додатку на хості.
Завдання 16: Захоплення пакетів на docker0, щоб підтвердити пересилання
cr0x@server:~$ sudo tcpdump -ni docker0 tcp port 80 -c 5
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:02:01.124001 IP 203.0.113.10.51922 > 172.17.0.4.80: Flags [S], seq 123456789, win 64240, options [mss 1460,sackOK,TS val 1 ecr 0,nop,wscale 7], length 0
Значення: Пакет було DNAT-нуто і він досяг docker0. Якщо пакет приходить на eth0, але не на docker0,
то вузьке місце — у ваших правилах NAT/forwarding.
Рішення: Виправте iptables/nftables, налаштування пересилання або правила DOCKER-USER.
Де все ламається: реальні режими відмов
1) Додаток слухає на неправильному інтерфейсі або порті
Багато фреймворків за замовчуванням прив’язуються до 127.0.0.1 «з міркувань безпеки». Це чудово на ноутбуках. У контейнерах — часта самопідстава.
Node, Python dev-сервери та деякі Java мікрофреймворки — часті порушники.
Що ви бачите: контейнер «здоровий», порт опублікований, але з’єднання зависають або скидаються. Всередині контейнера curl може працювати тільки через localhost,
або не працювати зовсім, якщо він прив’язаний до UNIX-сокета.
Що робити: змусьте додаток прив’язуватися до 0.0.0.0 (або явного IP інтерфейсу контейнера) і підтвердіть за допомогою ss -lntp.
Якщо ви використовуєте dev-сервер — припиніть. Використовуйте справжній сервер (gunicorn/uvicorn, nginx тощо) поза лаптопом.
2) Порт опублікований лише на localhost
Compose та docker run дозволяють прив’язки типу 127.0.0.1:8080:80. Це означає саме те, що написано.
Воно працює на хості, але не працює з мережі, і знищує години через «у мене працює».
Виправлення: прив’яжіть до 0.0.0.0 або до конкретного зовнішнього IP інтерфейсу свідомо.
Робіть прив’язку явною у Compose, коли маєте на увазі зовнішню доступність.
3) Фаєрвол хоста блокує пересланий трафік (UFW/firewalld), навіть якщо порт виглядає відкритим
Тонкий момент: «опублікований порт» часто реалізується через NAT + пересилання.
Фаєрволи можуть дозволяти INPUT до TCP/8080, але блокувати FORWARD до docker0, що призводить до таймаутів.
Локально це може працювати, бо трафік localhost може йти іншою ланкою/гачком.
Якщо ви запускаєте UFW з «deny routed», припускайте, що це причетне, допоки не доведено протилежне.
Якщо ви використовуєте firewalld — припускайте, що зони та masquerade мають значення.
4) Ланцюг DOCKER-USER блокує (навмисно або випадково)
DOCKER-USER існує саме для того, щоб оператори могли вставити політику попереду власних ланцюгів Docker. Це хороша інженерія.
Це також місце, де «тимчасові» відкидання живуть роками.
Один -j DROP у DOCKER-USER може заблокувати ваші опубліковані порти. Не розкидайте глобальні відкидання, якщо не документуєте їх.
5) Ви змішуєте бекенди iptables (legacy vs nft) і Docker програмує «не ту» сторону
На деяких дистрибутивах iptables — це сумісна обгортка над nftables, і ви можете отримати правила, вставлені в один вигляд,
тоді як пакет оцінюється іншим, залежно від версії ядра/користувацького простору та конфігурації.
Симптом: Docker стверджує, що порти опубліковано; правила видно в iptables -t nat -S, але трафік не змінюється,
або правила видно в nft, а вивід iptables виглядає порожнім.
Виправлення: оберіть послідовний бекенд і налаштуйте Docker відповідно. І перестаньте ставити стек фаєрвола як гру «оберіть свою пригоду».
6) Rootless Docker: інша механіка пересилання, інші сюрпризи
Rootless Docker уникає привілейованого мережевого доступу. Це добре для безпеки, менш добре для «має поводитися як rootful Docker».
Опубліковані порти реалізуються через пересилання в просторі користувача; продуктивність, поведінка прив’язки і взаємодія з фаєрволом відрізняються.
Симптом: порти працюють лише на localhost, або лише для високих портів, або падають при прив’язці до конкретних адрес.
Правил у system iptables ви не побачите, бо rootless їх не програмує.
Виправлення: підтвердіть, що ви в режимі rootless, потім дотримуйтесь специфічної інструкції для rootless (наприклад, явне налаштування пересилання портів).
Якщо вам потрібна класична NAT-поведінка — запускайте rootful Docker на захищених хостах замість напівкроків.
7) Невідповідність зворотного проксі (неправильний upstream, мережа або очікування TLS)
Ви публікуєте порт, але трафік фактично йде через nginx/HAProxy/Traefik на хості або в іншому контейнері.
Ваша проблема може бути в проксі, який говорить з неправильним IP контейнера, неправильною мережею або очікує TLS на plain HTTP (чи навпаки).
Симптом: контейнер працює при прямому curl, але проксі повертає 502/504.
Люди помилково називають це «порт недоступний», бо зовнішній симптом — «сайт впав».
Виправлення: тестуйте підключення upstream з контексту проксі, а не з почуттів вашого ноутбука.
8) Hairpin NAT / «підключення до мого публічного IP з того ж хоста»
Ви на хості і робите curl на публічний IP хоста:8080 — і це не працює, але localhost працює.
Це часто hairpin NAT. Деякі мережеві налаштування (особливо із суворим rp_filter або деякою cloud-маршрутизацією) трактують цей шлях по-іншому.
Виправлення: тестуйте на правильному інтерфейсі і зрозумійте, чи ви проходите через зовнішній роутер/LB і назад.
Якщо вам потрібен hairpin — налаштуйте його явно (або не залежіть від нього).
9) IPv6 «опублікований», але фактично недоступний
Docker може показувати прив’язки [::]:8080. Це не означає, що ваш хост має IPv6-зв’язність,
що фаєрвол дозволяє його, або що шлях до контейнера IPv6-працездатний.
Симптом: IPv4 працює; IPv6 таймаутить. Або клієнти віддають перевагу IPv6 і падають, хоча IPv4 працювало б.
Виправлення: підтвердіть маршрутизацію IPv6 і правила фаєрволу, і будьте явні щодо підтримки IPv6. Випадковий IPv6 — це хобі, а не стратегія.
Типові помилки: симптом → корінь → виправлення
1) «Працює на localhost, але не з іншої машини»
Корінь: порт прив’язано лише до 127.0.0.1, або фаєрвол дозволяє локальні, але блокує зовнішні, або cloud security group блокує.
Виправлення: опублікуйте на 0.0.0.0 (або правильний інтерфейс) і відкрийте порт на правильному шарі (фаєрвол хоста + cloud SG).
2) «docker ps показує 0.0.0.0:PORT, але ss нічого не показує як слушача»
Корінь: покладаєтесь на iptables DNAT без userland-проксі; ss не покаже слушача, хоча NAT працює.
Виправлення: протестуйте curl 127.0.0.1:PORT. Якщо це не працює — інспектуйте iptables/nft. Не сприймайте вивід ss як єдину істину.
3) «Негайно отримую connection refused»
Корінь: всередині контейнера немає процесу, що слухає, або неправильний порт контейнера, або контейнер впав і був замінений.
Виправлення: docker exec ss -lntp та docker logs. Підтвердіть, що додаток прив’язаний до очікуваного порту.
4) «Таймаут (SYN надіслано, немає SYN-ACK)»
Корінь: пакет відкинутий фаєрволом, security group, DOCKER-USER або маршрутизацією/NACL.
Виправлення: tcpdump на зовнішньому інтерфейсі хоста. Якщо SYN ніколи не приходить — upstream. Якщо приходить — перевіряйте фаєрвол/NAT на хості.
5) «Працює з хоста на IP контейнера, але не через опублікований порт»
Корінь: NAT/forwarding не запрограмовано або заблоковано; несумісність бекендів iptables; політика DOCKER-USER.
Виправлення: інспектуйте правила NAT, DOCKER-USER, sysctl forwarding. Зробіть політику фаєрволу явною і протестованою.
6) «Лише деякі клієнти можуть дістатися (інші таймаутять)»
Корінь: проблеми MTU (VPN), асиметрична маршрутизація, пріоритет IPv6, або split-horizon DNS, що вказує на різні IP.
Виправлення: захопіть пакети; тестуйте із примусовим IPv4/IPv6; перевірте MTU і маршрути; не вважайте мережу однорідною.
7) «Зламалось після увімкнення UFW/firewalld hardening»
Корінь: routed/forwarded трафік заблоковано; docker0 потрапив у обмежувальну зону.
Виправлення: дозволіть пересилання для Docker-мереж явним чином, або реалізуйте Docker-aware правила у DOCKER-USER з обережністю.
8) «Зворотний проксі повертає 502, але прямий порт працює»
Корінь: upstream проксі вказує на неправильний IP/мережу контейнера, неправильний протокол (HTTP vs HTTPS), або DNS усередині проксі резолвить інакше.
Виправлення: тестуйте з контексту проксі (контейнер або хост), підтвердіть цілі upstream і переконайтеся, що проксі та додаток ділять правильну docker-мережу.
Чеклісти / поетапний план (робіть це, а не гадання)
Чекліст A: Ви на Linux-хості і порт мертвий звідусіль
- Усередині контейнера: підтвердьте, що процес слухає на очікуваному порту за допомогою
ss -lntp. - Усередині контейнера:
curl 127.0.0.1:PORT(або еквівалент) для перевірки відповіді додатку. - Хост → IP контейнера:
curl CONTAINER_IP:PORT. Якщо це не працює — виправляйте docker-мережу або додаток. - Хост через опублікований порт:
curl 127.0.0.1:PUBLISHED. Якщо це не працює, але попереднє — працює, то це NAT/forwarding. - Правила NAT: перевірте
iptables -t nat -S DOCKERабоnft list ruleset. - Політики: перевірте
iptables -S DOCKER-USERі глобальні FORWARD політики. - Налаштування ядра: перевірте
net.ipv4.ip_forwardі bridge netfilter налаштування. - Лише потім: перезапускайте Docker, якщо ви змінили бекенди правил або конфіг демона. Не перезавантажуйте систему як діагностику.
Чекліст B: Localhost працює, ззовні не працює
- Прив’язка: перевірте, щоб це не було
127.0.0.1:PUBLISHEDуdocker ps/ inspect. - Локальний «зовнішній» тест: зробіть curl на IP хоста замість localhost.
- Фаєрвол: перевірте політики UFW/firewalld щодо routed/forwarded трафіку.
- Наявність пакету: запустіть
tcpdumpна зовнішньому інтерфейсі під час віддаленого тесту. - Хмара/периметр: підтвердіть, що security groups/NACL/LB слухачі спрямовані на правильний хост/порт.
- DNS санітарність: переконайтеся, що клієнти резолвлять на правильний IP (немає застарілого запису або split-horizon сюрпризу).
Чекліст C: Порт «досяжний», але додаток некоректний (502/цикли редиректів/SSL-дивакуватість)
- Прямий тест: зробіть curl на опублікований порт безпосередньо на хості. Отримайте чистий 200 (або очікувану відповідь).
- Шлях проксі: протестуйте з контексту проксі (контейнер або хост) до upstream-цілі.
- Протокол: перевірте HTTP vs HTTPS очікування. Не говоріть TLS на plain порт.
- Хедери: підтвердіть, що
HostіX-Forwarded-Protoвстановлені правильно, якщо додаток їх використовує. - Мережа: переконайтеся, що проксі та додаток у тій самій docker-мережі, якщо ви користуєтесь DNS іменами контейнерів.
Жарт №2: Якщо ваше виправлення — «перезапустити все», ви цього не виправили — ви кинули кості і назвали це інженерією.
Три міні-історії з корпоративного життя
Міні-історія 1: Інцидент через хибне припущення
Команда мігрувала невеликий внутрішній сервіс з VM в Docker на хардованому Linux-бейзлайні. Розгортання виглядало чистим: контейнер запустився, health checks пройшли,
і порт опубліковано. Інженер на чергуванні перевірив curl 127.0.0.1:PORT на хості і побачив очікувану відповідь. Деплой — зроблено.
За десять хвилин реальні користувачі не могли дістатися сервісу. Помилка не була 500; її не було зовсім — таймаути. Це викликало передбачуваний ритуал:
перевdeploy, перезапуск Docker, перебудова образу і нарешті «можливо це мережа». Тим часом балансувальник позначив сервіс як нездоровий і злив трафік.
Неправильне припущення було тонким: «Якщо localhost працює, мережа має працювати». На цьому хості UFW був у стані default incoming deny (добре),
і default routed deny (погано для пересилання Docker). Локальні запити ніколи не проходили ту ж політику пересилання, що зовнішні.
Отже команда довела, що додаток працює, але не перевірила опублікований шлях.
Виправлення було нудне і ефективне: задокументована фаєрвол-політика, що дозволяє routed трафік для конкретної підмережі контейнерів і портів,
плюс крок у руноу, який вимагав віддалений curl-тест (з бастіона в тій же мережі) перед тим, як закривати інцидент.
Після цього подібні відмови здебільшого зникли. Не тому, що Docker став милий — а тому, що команда припинила робити припущення.
Міні-історія 2: Оптимізація, що відплатила
Інша команда хотіла «максимум продуктивності» і видалила все, що здавалося оверхедом. Вони відключили userland-проксі в налаштуваннях демона Docker,
загострили правила фаєрвола і консолідували управління iptables під агента безпеки хоста. В тестуванні все виглядало швидшим і чистішим.
Бенчмарки були гарні. Слайди — ідеальні.
Потім в продакшні частина з’єднань до опублікованих портів почала падати періодично — переважно з певних підмереж.
Відмови не були досить постійними, щоб бути очевидними, але достатньо послідовними, щоб зіпсувати комусь день. Інцидент качався між «мережа» і «платформа»
довше, ніж кому хотілося б зізнатися.
Корінь був у взаємодії циклу оновлення правил агента безпеки хоста і динамічних NAT-прав Docker.
Коли контейнери замінювались, Docker програмував DNAT; агент згодом приводив стан до «бажаного» і видаляв те, чого не розпізнавав.
Оскільки проксі було відключено, резервного слушача не було — лише NAT-правила. Деякі з’єднання падали в вікнах, коли правила відсутні.
Відновлення полягало в тому, щоб припинити сприймати iptables як загальну іграшку. Команда перейшла на явну політику: або агент безпеки керує станом фаєрвола з інтеграцією Docker,
або Docker керує NAT з контрольованою політикою DOCKER-USER. Вони обрали останнє задля простоти.
Продуктивність залишилась гарною. Надійність значно покращилась. Оптимізація не була неправою; неправильною була модель володіння.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Група платформи керувала флотом Docker-хостів за внутрішніми балансувальниками. У них була сувора практика: кожен сервіс мав стандартну «перевірку з’єднання»,
що виконувалась з трьох місць — всередині контейнера, з хоста та з віддаленого канаркового вузла в тому ж сегменті мережі.
Це було буденно. Це також було задокументовано, автоматизовано і застосовувано під час інцидентів.
Одного дня після рутинного оновлення ядра почали надходити хвилі повідомлень «сервіс недоступний». Паніка почала кипіти у звичних Slack-каналах.
Але інженер на чергуванні дотримався проб. Усередині контейнера curl працював. Хост → IP контейнера працював. Хост → опублікований порт працював. Віддалений канарик — не працював.
Це звузило проблему за кілька хвилин: це не Docker і не додаток. Це була вхідна мережна доступність.
Мережеве зміна змінило, які підмережі мають дозвіл на доступ до хостів, і health checks балансувальника тепер бралися з підмережі, яка не була в allowlist.
Група платформи мала захоплення пакетів, щоб довести, що SYN-и ніколи не потрапляли на інтерфейс хоста.
Виправлення — чисте оновлення allowlist периметра.
Практика, що врятувала день, не була хитрим налаштуванням. Це була дисциплінована, повторювана перевірка з кількох точок зору,
з очікуваннями, записаними на папері. Це запобігло контейнерному дебаг-спіралю і скоротило час інциденту.
Нудне — це фіча, коли ви на чергуванні.
Цікаві факти та коротка історія (щоб ви припинили гадати)
- Початкова публікація портів Docker на Linux спиралася на правила NAT iptables, бо це було доступно і швидко в ядрі.
- «Userland proxy» існував для крайових випадків (наприклад, hairpin-з’єднання і деякі поведінки localhost), коли чистий DNAT був недостатній.
- Ланцюг DOCKER-USER з’явився, щоб оператори могли накласти політику попереду автоматично керованих Docker правил без війни з оновленнями Docker.
- iptables має два «світи» на сучасних Linux: legacy xtables і nftables бекенд. Їхнє змішування може породити правила, які ви бачите, але ядро їх ігнPuє (для вашого шляху трафіку).
- За замовчуванням UFW «deny routed» — це розумно для не-контейнерних хостів, але часто ламає пересилання контейнерів, якщо явно не дозволити.
- Rootless Docker став популярним, коли команди безпеки наполягли на least privilege, але він свідомо змінює реалізацію мережі і її спостережуваність.
- Docker Desktop на Mac/Windows завжди включає межу VM, тому опубліковані порти — це функція порт-форвардингу через віртуалізацію, а не лише локальний NAT.
- IPv6 часто «включено випадково», бо прив’язки можуть показувати
[::], навіть якщо середовище не підтримує наскрізну IPv6-доступність.
Висновок з історії: те, що ви бачите, — продукт рішень платформи, безпекової позиції і еволюції ядра/фаєрвол-стеку.
Ставтеся до вашого середовища як до реальної системи з шарами, а не як до магічного Docker-бульбашки.
FAQ
1) Чому docker ps показує порт як опублікований, якщо він не працює?
Тому що це вигляд конфігурації, а не тест підключення. Docker записав прив’язку і, ймовірно, намагався запрограмувати пересилання,
але фаєрволи, маршрутизація або додаток все ще можуть блокувати фактичний трафік.
2) Як відрізнити проблему додатку від мережі?
Використайте трихоповий тест: всередині контейнера (curl localhost), хост → IP контейнера, хост → опублікований порт. Перша невдача вказує шар.
3) Чому localhost працює, а LAN-IP хоста — ні?
Різні шляхи. Localhost може зачіпати OUTPUT правила або локальний DNAT; зовнішній трафік — PREROUTING/FORWARD і підпадає під іншу політику фаєрвола.
Також перевірте, чи не прив’язано випадково до 127.0.0.1.
4) Чи треба відкривати порт у фаєрволі всередині контейнера?
Зазвичай ні. Більшість контейнерів не запускають фаєрвол. Якщо ви запускаєте — поводьтеся з ним як із реальним хостом: дозволяйте вхід на порт додатку.
Але більшість проблем «недоступного опублікованого порту» — це хост/периметр, а не фаєрвол контейнера.
5) Чому ss -lntp іноді не показує слушача для опублікованого порту?
Тому що публікація на основі NAT не потребує процесу, що слухає на порту хоста. Ядро переписує і пересилає пакети.
Якщо використовується userland-проксі — ви побачите docker-proxy.
6) Чи може публікація Docker не працювати через невідповідність iptables/nftables?
Так. Якщо Docker програмує правила в бекенд, який не використовується для вашого трафіку, ви отримаєте «правила є», але пересилання нема.
Перевірте і iptables, і nft та стандартизуйтесь на одному стеці.
7) Що змінюється в rootless Docker?
Rootless не може вільно програмувати системні NAT-правила. Публікація портів зазвичай покладається на механізми в просторі користувача.
Спостережуваність (перегляд iptables) і поведінка (обмеження прив’язки, продуктивність) відрізняються. Спочатку підтвердіть режим.
8) Як дебагувати, якщо хост за балансувальником навантаження?
Зніміть пакети на інтерфейсі хоста, поки LB робить probes. Якщо SYN-и ніколи не приходять — це конфіг LB, security group або маршрути.
Якщо SYN-и приходять, але не доходять до docker0 — це фаєрвол/NAT хоста.
9) Чому працює через IPv4, але падає через IPv6?
Тому що IPv6 вимагає наскрізної маршрутизації і правил фаєрвола. Docker, що показує [::], не гарантує, що мережа підтримує це.
Тестуйте явно IPv4/IPv6 і налаштовуйте свідомо.
10) Чи треба використовувати --network host, щоб «уникнути проблем Docker-мережі»?
Тільки якщо ви розумієте компроміси. Host networking прибирає шар NAT, але збільшує ризик колізій портів і знижує ізоляцію.
Це інструмент, а не пластир для невідомих проблем.
Висновок: наступні кроки, що реально запобігають повторенням
Коли порт Docker опубліковано, але недоступний, система точно вказує, де щось зламалось — треба лише допитати її в потрібному порядку.
Починайте всередині контейнера, рухайтесь назовні і зупиняйтесь, як тільки знайдете перший невдалий хоп. Це і є вузьке місце. Виправте його, а не своє терпіння.
Зробіть ось що
- Кодифікуйте трихоповий тест (container localhost → host container IP → host published port → remote canary) у ваших руноу.
- Стандартизуйте володіння фаєрволом: або Docker володіє NAT і ви використовуєте DOCKER-USER свідомо, або ваш менеджер фаєрволу інтегрується з Docker. Ніякого спільного містичного стану.
- Робіть прив’язки явними у Compose (
0.0.0.0:PORT:PORTvs127.0.0.1), щоб не відправляти «працює на моєму хості» у продакшн. - Інструментуйте досяжність: простий blackbox-чек з віддаленого вузла ловить більшість проблем до користувачів.
- Документуйте спеціальні режими (rootless, IPv6, зворотні проксі, балансувальники) біля сервісу, а не в чиємусь мозку.
Ваша мета — не запам’ятати всі нетрі Docker-мереж. Ваше завдання — скоротити час до істини. Наведений чекліст це робить — надійно, повторювано
і без необхідності перезавантаження та підношень богам мережі.