Docker-контейнер запущен, а сайт не открывается: порт, Nginx, firewall и DNS

Ситуация знакомая: docker ps показывает контейнер со статусом Up , ошибок запуска вроде бы нет, а сайт в браузере не открывается. Иногда соединение завершается Connection refused , иногда висит до тайм-аута, иногда Nginx отдаёт 502 или собственную стартовую страницу.

 

Здесь полезно не менять одновременно docker-compose.yml , Nginx, firewall и DNS. Надёжнее пройти путь запроса в том же порядке, в котором его проходит браузер: приложение внутри контейнера → порт контейнера → опубликованный порт хоста → Nginx → firewall → публичный IP → DNS. Задача диагностики — найти последний участок, на котором запрос ещё точно работает.

Контейнер Up — ещё не значит, что внутри работает сайт

Статус Up подтверждает только то, что основной процесс контейнера не завершился. Он не доказывает, что веб-приложение слушает нужный TCP-порт, отвечает по HTTP и прошло healthcheck. Поэтому при недоступном сайте первая проверка должна происходить внутри контейнера, а не в DNS или Nginx.

Начните с состояния и логов:

docker ps
docker logs --tail 100 <container>
docker inspect <container>

Затем зайдите внутрь контейнера и проверьте, слушает ли приложение ожидаемый порт:

docker exec -it <container> sh
ss -ltnp
curl -v http://127.0.0.1:<port>/

Если curl внутри контейнера не получает HTTP-ответ, искать причину в firewall или A-записи домена ещё рано. Нужно разбираться с самим приложением: оно могло не стартовать, слушать другой порт, завершить дочерний процесс после запуска или привязаться только к неподходящему адресу.

В минимальных образах curl или ss могут отсутствовать. Это само по себе не означает неисправность. В таком случае смотрят логи, конфигурацию приложения, healthcheck или используют доступные внутри образа инструменты.

На практике статус Up нередко слишком рано принимают за доказательство того, что сайт уже «жив». Полезнее подтвердить работоспособность по шагам: процесс есть, порт слушается, HTTP-ответ приходит.

Порт приложения работает внутри контейнера, но опубликован ли он на хосте

Если приложение отвечает внутри контейнера, следующий вопрос — опубликован ли его порт на адресе и порту Docker-хоста. EXPOSE в Dockerfile и реальный port mapping — не одно и то же: запись EXPOSE 80 не эквивалентна запуску с -p 8080:80 и сама по себе не создаёт отображение host IP:port → container port.

Проверить фактическую публикацию можно так:

docker ps
docker port <container>
docker inspect <container>
curl -v http://127.0.0.1:8080/

В записи 8080:80 слева указан порт хоста, справа — порт контейнера. То есть запрос на HOST:8080 должен попасть на порт 80 внутри контейнера. Ошибка в одной цифре легко создаёт ситуацию, когда само приложение работает, а через ожидаемый host port его не видно.

EXPOSE и published port — не одно и то же

Для Compose типичная публикация выглядит так:

ports:
  - "8080:80"

Если секции ports нет, EXPOSE 80 не публикует порт на адресе Docker-хоста. Контейнер при этом может оставаться доступным другим контейнерам в подходящей Docker network, а в некоторых схемах — и самому хосту по внутреннему адресу контейнера, но отдельного host port из-за одного EXPOSE не появляется. Ещё одна частая причина — Compose уже изменили, а контейнер не пересоздали, поэтому фактически работает старая схема.

127.0.0.1:8080 и 0.0.0.0:8080: в чём практическая разница

Публикация вида 127.0.0.1:8080:80 делает host port доступным только локально. Это часто правильный вариант, если снаружи должен принимать соединения только Nginx. Запись 0.0.0.0:8080:80 публикует его на всех IPv4-интерфейсах хоста, если доступ не ограничен сетевыми правилами.

На сервере, где Docker уже подготовлен заранее — например, на VPS с готовой Docker-средой , — начальный этап может быть уже выполнен, но схема диагностики остаётся той же: внутренний порт приложения, публикация host port и путь запроса через reverse proxy всё равно проверяются отдельно.

После этого нужен простой результат: curl http://127.0.0.1:8080/ с хоста получает ответ приложения. Если внутри контейнера HTTP работает, а на опубликованном host port — Connection refused , проблема всё ещё находится на уровне port mapping, выбранного адреса или фактического порта.

Найдите точку, до которой запрос уже доходит

Вместо перебора настроек проверьте один и тот же сервис с нескольких точек. Не ищем виноватого — ищем границу отказа.

  1. Из контейнера — к самому приложению.

  2. С хоста — к опубликованному Docker-порту.

  3. С хоста — к Nginx.

  4. С другой машины — к публичному IP сервера.

  5. С другой машины — к доменному имени.

Обычно диагностика резко упрощается, как только становится понятно, какой последний запрос в цепочке точно работает.

Симптом

Что уже можно считать работающим

Где искать дальше

curl внутри контейнера не отвечает

Docker-контейнер запущен, но HTTP-сервис не подтверждён

Приложение, его логи, порт, bind, healthcheck

Внутри контейнера работает, 127.0.0.1:8080 на хосте нет

Само приложение отвечает

Docker port mapping, адрес публикации, фактический порт

127.0.0.1:8080 работает, Nginx отдаёт 502

Backend и опубликованный порт доступны

proxy_pass , upstream, сеть Nginx, error.log

Локально работает, публичный IP снаружи нет

Сервис работает на сервере

Bind, firewall ОС, внешний ACL/firewall

Публичный IP работает, домен нет

Сервер и HTTP-доступность подтверждены

DNS, AAAA, server_name , HTTPS

После каждой правки повторяйте именно ту проверку, которая раньше не проходила. Если сразу менять три слоя, легко получить новый симптом и потерять исходную причину.

Если Docker отвечает, а Nginx отдаёт 502, 404 или чужой сайт

Если curl http://127.0.0.1:8080/ на хосте работает, а запрос через Nginx нет, Docker-часть цепочки уже в основном проверена. Дальше нужно смотреть маршрут client → Nginx → upstream .

502: Nginx не получает нормальный ответ от upstream

502 Bad Gateway — полезный симптом в том смысле, что запрос уже добрался до Nginx. Теперь нужно выяснить, почему сам Nginx не получает нормальный ответ от backend. В error.log часто встречаются connect() failed (111: Connection refused) , upstream timed out или host not found in upstream .

Проверьте конфигурацию и сам upstream:

nginx -t
nginx -T
systemctl status nginx
journalctl -u nginx
curl -v http://127.0.0.1:8080/

В конфигурации смотрят прежде всего proxy_pass , порт и адрес назначения. Здесь важно разделять две архитектуры. Nginx, установленный на хосте, обычно обращается к опубликованному host port, например 127.0.0.1:8080 . Nginx внутри той же Docker network чаще может идти прямо на backend по имени сервиса и внутреннему container port. Имя контейнера или сервиса не обязано разрешаться для Nginx, который работает вне этой сети.

404 и default page: запрос попал не в тот server block

Если Nginx отвечает, но показывает 404, стандартную страницу или чужой сайт, проверяйте server_name , listen , подключение нужного конфигурационного файла и порядок server blocks. В этом случае соединение до Nginx уже есть; проблема обычно в выборе server block, location или дальнейшей маршрутизации.

Как проверить virtual host без ожидания DNS

Для HTTP удобно передать Host вручную:

curl -v -H "Host: example.com" http://127.0.0.1/

Если с таким заголовком открывается нужный сайт, Nginx умеет обслуживать домен, а искать причину дальше стоит в DNS или внешнем маршруте. Если backend напрямую отвечает мгновенно, а через Nginx появляется 502, полезнее читать error.log , чем снова перезапускать контейнер.

Локально работает, с другого компьютера нет: проверяем bind и firewall

Когда сайт отвечает на самом VPS, но не открывается с другой машины, область поиска заметно сужается. Сначала нужно увидеть, на каком адресе слушают host-сервисы, затем — пропускается ли входящий трафик.

Сначала убедитесь, что Nginx слушает нужный адрес

ss -ltnp | grep ':80\|:443'

Запись 127.0.0.1:8080 означает, что обычный host-сервис на таком сокете доступен только локально. Для backend за Nginx это нормально. Но сам Nginx, который должен принимать HTTP/HTTPS из Интернета, обычно должен слушать соответствующий внешний интерфейс на 80/443.

0.0.0.0:80 означает прослушивание всех IPv4-интерфейсов, а [::]:80 относится к IPv6. После этого можно сделать внешний тест с другой машины:

curl -v http://SERVER_IP/
nc -vz SERVER_IP 80

Firewall сервера и внешний firewall — два разных слоя

Если локальный запрос проходит, а внешний зависает или не соединяется, проверяют правила ОС:

ufw status verbose
nft list ruleset
iptables -S

На сервере может использоваться только один основной механизм фильтрации, поэтому нет смысла бездумно править всё сразу. Кроме локальных правил, у VPS или облачной платформы может быть отдельный firewall, security group или ACL в панели провайдера. Его легко забыть, потому что внутри Linux всё выглядит правильно.

У опубликованных Docker-портов есть дополнительный нюанс: трафик через -p проходит через правила, которыми управляет сам Docker, поэтому одного ufw status недостаточно, чтобы понять реальный путь пакета. Если проблема касается именно published port контейнера, смотрят также Docker-managed правила iptables/nftables и внешний firewall инфраструктуры.

Полностью отключать firewall как стандартный способ диагностики не стоит. Если localhost отвечает, а с другой машины соединение висит до тайм-аута, Compose пока лучше не трогать: сначала проверяют bind и путь входящего трафика.

По IP сайт открывается, по домену нет: проверяем DNS и IPv6

Если сайт стабильно открывается по публичному IP, а по домену нет, вероятность проблемы на уровне Docker становится значительно ниже. Теперь нужно проверить, куда домен реально резолвится и какой virtual host выбирает Nginx.

Сравниваем A-запись с IP сервера

dig example.com A +short
dig www.example.com A +short

Полученный IPv4-адрес должен совпадать с актуальным адресом сервера или с адресом используемого proxy/CDN. После недавнего переноса сайта возможны старые ответы из кэша, различия между www и основным доменом или забытая A-запись на прежний VPS.

Почему старая AAAA-запись может ломать доступ только у части пользователей

Отдельно проверьте IPv6:

dig example.com AAAA +short

Есть неприятный вариант: A уже исправили, а про старую AAAA забыли. Она продолжает вести на прежний сервер или на IPv6-адрес, где сайт не обслуживается. Пользователь с рабочим IPv6 может пойти именно туда, поэтому у одного человека домен открывается, у другого — нет.

Для HTTP можно отделить DNS от Nginx, передав нужный Host вручную:

curl -v -H "Host: example.com" http://SERVER_IP/

Для HTTPS удобнее сразу проверить нужный hostname, SNI и конкретный IP без изменения DNS:

curl -v --resolve example.com:443:SERVER_IP https://example.com/

Если запрос с --resolve проходит, нужный HTTPS virtual host на этом IP работает, а проблема остаётся снаружи этой проверки: DNS, другой адрес, кэш или сетевой маршрут. Если же ответ приходит с неправильным сертификатом или не из того server block, смотреть нужно уже конфигурацию TLS/SNI и виртуального хоста.

Что говорит сама ошибка: connection refused, timeout, 502 и NXDOMAIN — это разные ветки

Тип ошибки часто полезнее общей фразы «сайт не открывается». Он не даёт стопроцентного диагноза, но подсказывает, откуда разумнее начинать проверку.

Симптом

Что уже произошло

Первая зона проверки

Connection refused

Хост доступен, но соединение на этом адресе/порту не принимается

Listener, bind, Docker port mapping, неверный порт

Timeout

Ответ на соединение не получен вовремя

Firewall, маршрут, внешний ACL, зависший сервис

502 Bad Gateway

Запрос дошёл до Nginx, но возникла проблема с upstream

proxy_pass , backend, Docker network, error.log

404 или default Nginx page

Nginx уже отвечает

server_name , location , default server

NXDOMAIN / ERR_NAME_NOT_RESOLVED

Имя не разрешилось через DNS

DNS-зона, NS, A/AAAA, опечатка в имени

Несовпадение SSL-сертификата

HTTPS-соединение дошло до какого-то сервера

DNS, SNI, virtual host, сертификат

Не стоит превращать таблицу в жёсткое правило. Timeout бывает и из-за firewall, и из-за зависшего приложения. Но если Nginx уже отдаёт 502, начинать с изменения DNS обычно бессмысленно: запрос дошёл до reverse proxy, а сбой возник дальше по цепочке.

Диагностика за 10 минут: проверяем цепочку снизу вверх

Когда сайт уже лежит, удобнее идти по короткому чек-листу и останавливаться на первом сломанном шаге. После каждого теста должно быть понятно, какой слой уже подтверждён.

  1. Проверить состояние контейнера: docker ps .

  2. Посмотреть последние сообщения приложения: docker logs --tail 100 <container> .

  3. Проверить HTTP внутри контейнера на фактическом порту приложения.

  4. Посмотреть реальные публикации портов: docker port <container> и при необходимости docker inspect <container> .

  5. Сделать запрос к опубликованному backend с хоста: curl http://127.0.0.1:PORT/ .

  6. Проверить, что Nginx и другие host-сервисы слушают ожидаемые адреса и порты: ss -ltnp .

  7. Проверить синтаксис Nginx: nginx -t и при необходимости посмотреть nginx -T .

  8. Проверить нужный virtual host: curl -H "Host: example.com" http://127.0.0.1/ .

  9. С другой машины проверить публичный IP сервера.

  10. Если внешний доступ не проходит, проверить firewall ОС, Docker-managed правила и внешний firewall/ACL.

  11. Сравнить A и AAAA домена с реальными адресами сервера.

  12. Для HTTPS при необходимости проверить конкретный IP через curl --resolve , а затем открыть домен в обычном виде.

Рабочая цепочка выглядит так: backend отвечает внутри контейнера, published port отвечает на хосте, Nginx отдаёт нужный virtual host, публичный IP доступен извне, домен резолвится на правильный адрес. Как только один шаг не проходит, следующий слой пока можно не трогать.

В поддержке сообщение «Docker работает, сайт нет» становится намного полезнее, если вместе с ним есть хотя бы четыре результата: docker ps , последние строки docker logs , ответ curl напрямую в backend и ответ запроса к домену. Эти данные уже показывают направление диагностики и позволяют не стрелять по всей цепочке сразу.


У Вас не достаточно прав для комментирования!

Инфо

Информативно о компьютерных технологиях. Различные материалы относительно компьютерного железа, софта (программ) и сетевых технологий. При полном или частичном копировании информации - прямая ссылка на сайт (We-it.net) обязательна.