Ситуация из практики: вы делаете docker run -p 8080:80 или прописываете ports: в Compose, контейнер «живой», локально на сервере всё открывается, а снаружи — таймаут. Или наоборот: снаружи доступно, но с самого сервера по своему публичному IP не работает (классический hairpin NAT). Ниже — пошаговая диагностика, что именно ломается: публикация, маршрутизация или firewall.
Как Docker делает publish: что меняется в сети
docker publish (опция -p / --publish) решает две задачи:
- на хосте появляется «вход» на порт (часто это DNAT, иногда участвует userland-proxy);
- пакеты перенаправляются на IP контейнера в docker-сети (обычно bridge
docker0).
Технически Docker добавляет правила в NAT (DNAT в таблице nat) и правила в фильтрах для форвардинга к bridge. В iptables это обычно цепочки DOCKER, DOCKER-USER, DOCKER-ISOLATION-STAGE-1, DOCKER-ISOLATION-STAGE-2. В системах с nftables правила могут лежать в nft (через iptables-nft) или в нативных цепочках — зависит от дистрибутива и того, как настроен firewall.
«Порт опубликован» в Docker не равно «порт разрешён вашим firewall». Docker может создать DNAT, но строгая политика INPUT/FORWARD, UFW или зоны firewalld способны отрезать трафик раньше.
Симптомы: быстро классифицируем проблему
Перед тем как «крутить правила», определите, где именно обрыв:
- Снаружи таймаут (SYN без ответа): порт не слушается на хосте, режется firewall/провайдером или неправильная публикация.
- Снаружи RST: что-то отвечает, но, возможно, не тот сервис (конфликт порта) или приложение сразу закрывает соединение.
- С хоста по 127.0.0.1 работает, по публичному IP нет: часто это hairpin NAT, rp_filter или политика фильтрации.
- Снаружи работает, но из LAN по публичному IP нет: hairpin NAT уже на маршрутизаторе/балансировщике (не всегда лечится на Docker-хосте).
Если вы запускаете контейнеры на VDS, не забывайте про внешние фильтры: в панели провайдера/внешнем firewall порт может быть закрыт даже при «идеальных» правилах на сервере.

Шаг 1. Проверяем, что Docker действительно опубликовал порт
Начните с очевидного — что опубликовано и на какие адреса:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
Обратите внимание на биндинг:
0.0.0.0:8080->80/tcp— слушает на всех IPv4-адресах;127.0.0.1:8080->80/tcp— доступно только локально на хосте (частая причина «снаружи не открывается»);[::]:8080->80/tcp— публикация по IPv6; если клиенты идут по IPv4, а IPv4-биндинга нет, снаружи будет «не работает».
Проверьте, что на хосте реально есть слушающий сокет:
ss -lntp | grep -E '(:8080\s|:8080$)'
Если слушателя нет — Docker не поднял публикацию (контейнер не запущен, порт занят, ошибка в параметрах). Проверьте конфликты:
ss -lntp | grep -E '(:80\s|:80$|:8080\s|:8080$)'
Шаг 2. Проверяем путь: проблема внутри сервера или снаружи
С хоста проверьте доступ к опубликованному порту по разным адресам:
curl -sS -D- http://127.0.0.1:8080/ -o /dev/null
curl -sS -D- http://$(hostname -I | awk '{print $1}'):8080/ -o /dev/null
Если на 127.0.0.1 работает, а на IP интерфейса нет — это уже не проблема приложения, а фильтрация/маршрутизация/NAT.
С другой машины (в интернете или хотя бы другой подсети) проверьте TCP-рукопожатие:
nc -vz SERVER_IP 8080
Если nc висит с таймаутом, самые частые причины: публикация на 127.0.0.1, хостовый firewall, провайдерский firewall, либо трафик не доходит до сервера (ошибка маршрутизации/не тот IP).
Шаг 3. iptables: проверяем, что добавил Docker (NAT и FORWARD)
Даже если вы «открыли порт», Docker-трафик к контейнеру часто проходит через DNAT и цепочку FORWARD, а не через INPUT. Поэтому важно смотреть обе части: NAT и фильтр.
NAT: есть ли DNAT на контейнер
iptables -t nat -S | sed -n '1,200p'
Найдите цепочку DOCKER и правило, где внешний порт маппится на IP контейнера. Точечный поиск по порту:
iptables -t nat -S | grep -F -- '--dport 8080'
FILTER: где режется форвардинг (FORWARD и DOCKER-USER)
Если DNAT сработал, пакет дальше пойдёт через форвардинг на bridge-интерфейс. Блокировки часто сидят в FORWARD или в DOCKER-USER:
iptables -S FORWARD
iptables -S DOCKER-USER
Типичная ловушка: администратор добавил «deny all» в DOCKER-USER (что логично для базовой политики), но не добавил явное разрешение для нужных портов/подсетей. В результате Docker публикует порт, но трафик режется до контейнера.
Диагностика счётчиками
Счётчики помогают понять, доходит ли трафик до нужного места. Запустите тест подключения снаружи и смотрите, растут ли counters:
iptables -nvL DOCKER-USER
iptables -t nat -nvL DOCKER
Минимальный пример: разрешить вход на опубликованный порт через DOCKER-USER
Если у вас политика «всё запрещено» в DOCKER-USER, добавьте разрешение выше общего DROP. Пример для TCP/8080 (подставьте свой внешний интерфейс в -i при необходимости):
iptables -I DOCKER-USER 1 -p tcp --dport 8080 -j ACCEPT
Если хотите ограничить доступ только с доверенной подсети:
iptables -I DOCKER-USER 1 -p tcp -s 203.0.113.0/24 --dport 8080 -j ACCEPT
Важно: сохранение правил зависит от дистрибутива (iptables-persistent, systemd unit, свои скрипты). Не ограничивайтесь «применил и забыл».
Шаг 4. nftables: когда iptables «как будто есть», но реально рулит nft
В современных дистрибутивах iptables может быть лишь интерфейсом совместимости (iptables-nft), а реальные правила живут в nft. Поэтому диагностика должна включать просмотр nft-правил:
nft list ruleset | sed -n '1,200p'
Удобно искать нужный порт:
nft list ruleset | grep -nF 'dport 8080'
Частый сценарий «порт опубликован, но не доступен»: строгая политика в inet таблицах (input/forward) отбрасывает пакет раньше, чем он попадёт в правила, которые ожидает Docker. В nftables критичны hook и priority: правило может существовать, но стоять «позже» глобального drop.
Если nftables — ваш основной firewall, лучше явным образом описать: что разрешено на вход (input к хосту) и что разрешено на форвардинг к docker bridge. Не полагайтесь только на автогенерацию Docker.
Для более детального разбора цепочек Docker и типовых конфликтов nft/iptables см. материал Docker и firewall: как устроены цепочки iptables/nftables.

UFW и Docker: почему «ufw allow 8080/tcp» может не помочь
UFW обычно ориентирован на входящий трафик в цепь INPUT. А Docker-публикация часто превращает вход в DNAT + FORWARD к контейнеру. Поэтому «разрешил порт» в UFW не всегда лечит доступ к контейнеру.
Что проверить в первую очередь:
- включён ли форвардинг:
net.ipv4.ip_forward; - какова политика цепи
FORWARDв iptables; - нет ли
DEFAULT_FORWARD_POLICY="DROP"в настройках UFW.
ufw status verbose
sysctl net.ipv4.ip_forward
iptables -S FORWARD
Если UFW включён и FORWARD фактически «закрыт», Docker-трафик к контейнерам будет отваливаться даже при разрешённом порте на INPUT.
firewalld и Docker: зоны, masquerade и forward
В firewalld добавляется концепция зон и masquerade. Типовые причины недоступности опубликованных портов:
- порт открыт в «не той» зоне (например, открыт в public, а интерфейс с публичным IP находится в drop);
- запрещён forward между зонами (docker0 в отдельной зоне без разрешений);
- в некоторых схемах не включён masquerade там, где он нужен для корректного обратного пути.
Проверьте состояние и привязку интерфейсов:
firewall-cmd --state
firewall-cmd --get-active-zones
firewall-cmd --zone=public --list-ports
Если интерфейс с публичным IP привязан не к той зоне, «открытие порта» не сработает так, как вы ожидаете.
Hairpin NAT: почему с самого сервера не открывается по своему публичному IP
Hairpin NAT (NAT loopback) — это сценарий, когда клиент находится «внутри» (на том же хосте или в той же L2/LAN), но обращается к сервису по внешнему (публичному) IP, и трафик должен «завернуться» обратно внутрь. В контексте Docker это часто выглядит так:
- снаружи сервис доступен;
- на хосте
curl http://127.0.0.1:8080работает, аcurl http://PUBLIC_IP:8080— нет; - контейнеры не могут ходить на сервис по публичному IP хоста, хотя по внутреннему адресу всё нормально.
Как быстро понять, hairpin ли это
Проверьте, как ядро маршрутизирует запрос к собственному публичному IP:
ip route get PUBLIC_IP
Проверьте rp_filter (строгая проверка обратного пути иногда ломает «нестандартные» схемы):
sysctl net.ipv4.conf.all.rp_filter
sysctl net.ipv4.conf.default.rp_filter
Что делать на практике
В реальной эксплуатации почти всегда достаточно одного из подходов:
- для локальных healthcheck и проверок использовать
127.0.0.1или IP интерфейса, а не публичный IP; - использовать split-horizon DNS: внутри сети имя резолвится во внутренний адрес;
- если нужен именно hairpin, настраивать NAT/маршрутизацию осознанно и в одном месте (nftables или iptables, без «перетягивания» несколькими фреймворками).
Типовые причины «порт опубликован, но не доступен»
1) Публикация на localhost
В Compose/CLI легко случайно сделать биндинг на 127.0.0.1. Тогда снаружи порт не будет доступен.
docker run -p 127.0.0.1:8080:80 nginx
Для внешнего доступа нужен биндинг на 0.0.0.0 (по умолчанию) или конкретный публичный IP.
2) Хостовый firewall блокирует INPUT на порт
Если публикация реализована через userland-proxy или сервис слушает на хосте, блокировка будет в INPUT. Тогда открывайте порт именно в input/zone для публичного интерфейса и проверяйте counters на входящих правилах.
3) FORWARD=DROP и/или блок в DOCKER-USER
Самая частая причина при «правильной» безопасности: вы запрещаете форвардинг, и контейнерный DNAT не проходит. Лечится явными allow-правилами к нужным портам/подсетям и аккуратным базовым DROP.
4) Конфликт nftables приоритетов/хуков
Правила есть, но они срабатывают «после» общего drop из-за приоритетов. В nftables это встречается регулярно при кастомных inet-таблицах.
5) Внешний firewall провайдера
Признак: на хосте порт слушается и локально доступен, но counters на входящих правилах не растут при внешнем тесте. Тогда проверяйте фильтры на уровне провайдера/панели.
Практический чек-лист диагностики (в порядке эффективности)
Проверить биндинг в
docker psи слушатель вss -lntp.Проверить доступ с хоста:
127.0.0.1, затем IP интерфейса, затем публичный IP (если нужно понять hairpin).С внешней машины проверить TCP (
nc) и, если это HTTP, запрос (curl -v).Посмотреть DNAT:
iptables -t nat -Sи поиск по--dport.Проверить
FORWARDиDOCKER-USER, затем counters (iptables -nvL).Если используется nftables:
nft list ruleset, поиск поdport, проверить порядок (hook/priority) относительно drop.Если включён UFW: убедиться, что не ломает форвардинг.
Если firewalld: проверить активные зоны интерфейсов и где открыт порт.
Что запомнить
Когда docker publish «не работает», чаще всего Docker как раз сделал свою часть (DNAT/цепочки), а проблема в том, что трафик к контейнерам идёт через NAT и FORWARD, а ваш firewall настроен под классические сервисы на хосте (INPUT). Разделяйте эти два мира, проверяйте цепочки и counters, и держите в голове hairpin NAT: он отлично объясняет «странности» с доступом по публичному IP с самого сервера.


