Мониторинг шлёт тревогу, пользователи пишут, что «всё лежит», но SSH пускает на сервер. Значит, машина включена, и 22-й порт доступен. О домене, портах 80 и 443, TLS, веб-сервере и приложении это пока ничего не говорит… Кажется, что нужно просто перезапустить nginx, но перезапуск стирает часть следов и может превратить частичную аварию в полную беду. Под катом расскажу, как пройти путь запроса сверху вниз и найти место, где он остановился.

Шаг 1. Воспроизводим ошибку

Фраза «сайт не открывается» ещё ни о чём не говорит, сначала проверьте его из другой сети. Если проблема только у вас, возможны локальный DNS-кеш, запись в /etc/hosts, блокировка адреса или маршрут провайдера. Если жалуются все, запускайте curl на клиентской машине:

curl -v --connect-timeout 5 --max-time 15 https://example.com/

Также посмотрите на последнюю успешно пройденную стадию:

  • Could not resolve host ведёт к DNS. Если в выводе нет Connected to, то соединение не дошло до готового TCP или другой стадии подключения. 

  • Когда TCP уже установлен, а дальше висит TLS-рукопожатие, проверять нужно сертификаты и настройки TLS. 

  • Если HTTP-запрос ушёл, но ответа нет до --max-time, ищите проблему с прокси, приложением и его зависимостями.

Мгновенный Connection refused чаще всего значит, что на порту никто не слушает, либо фильтр ответил отказом. Тайм-аут чаще указывает на DROP, проблему маршрута или зависание на поздней стадии. Однако одной строки с ошибкой мало, смотрите весь вывод.

Коды HTTP тоже могут сузить поиск. При 502 шлюз получил некорректный ответ от upstream, при 504 — не дождался ответа вовремя, а 503 означает временную недоступность или перегрузку и может прийти как от прокси, так и от приложения.

Отдельно сравните IPv4 и IPv6:

curl -4 -v --connect-timeout 5 --max-time 15 https://example.com/

curl -6 -v --connect-timeout 5 --max-time 15 https://example.com/

Если IPv4 работает, а IPv6 нет, проверьте AAAA-запись, маршрутизацию и слушающий сокет. Сломанную запись лучше исправить сразу, а не надеяться на то, что клиенты переключатся на другой протокол. 

Шаг 2. Сверьте DNS и нужный сервер

Зачастую SSH идёт на новый сервер по IP, а домен остаётся на старом адресе, или A-запись исправили, AAAA забыли, а некоторые резолверы ещё держат ответ в пределах TTL. Для проверки проблемы используйте: 

dig +short A example.com

dig +short AAAA example.com

В целом, сравните адреса с реальной схемой. За NAT публичного IP внутри виртуалки может не быть, а домен за CDN не должен указывать на origin. Нужный сервер проверяйте без правки DNS (некоторые команды буду писать с слэшами-переносами, чтобы в кучку всё не сваливалось):

curl -v \

  --connect-timeout 5 \

  --max-time 15 \

  --resolve example.com:443:203.0.113.10 \

  https://example.com/

--resolve подменяет IP, сохраняя доменное имя в HTTP и SNI. Если обычный запрос падает, а origin отвечает, смотрите DNS, CDN или балансировщик. Сервер, который принимает трафик только с адресов CDN, должен отклонить прямой запрос — его проверяют из разрешённого источника, либо через CDN.

Шаг 3. Узнайте, кто слушает 80 и 443

Дальше заходите по SSH и смотрите сокеты в текущем сетевом пространстве:

sudo ss -ltnp 'sport = :80'

sudo ss -ltnp 'sport = :443'

Пустой вывод означает, что в текущем network namespace нужный порт никто не слушает. Если строки есть, посмотрите, к какому адресу привязан сокет: 127.0.0.1:443 доступен только с самого сервера, а 0.0.0.0:443 принимает IPv4-соединения на всех его интерфейсах. Запись [::]:443 относится к IPv6 — может ли такой сокет одновременно принимать IPv4-соединения, зависит от параметра IPV6_V6ONLY и настроек приложения. Быстрее всего это проверить отдельными запросами (писал их выше). Для nginx команды такие:

sudo systemctl status nginx --no-pager -l

sudo nginx -t

sudo journalctl -u nginx --since '-30 minutes' --no-pager

Active: active (running) подтверждает, что процесс жив, но это не говорит о правильности виртуального хоста и upstream. А nginx -t проверяет синтаксис и доступность указанных в конфигурации файлов. Также после теста перечитайте конфиг без остановки старых воркеров:

sudo systemctl reload nginx

Для Apache используйте sudo apachectl configtest — юнит обычно называется apache2 в Debian-подобных системах и httpd в RHEL-подобных. Знаю, что вы всегда проверяете конфиг перед перезапуском, но на всякий случай решил напомнить. 

Шаг 3. Разделите аварию

Локальный запрос проверяет веб-сервер без внешнего маршрута. Для HTTP передайте правильный Host:

curl -v --noproxy '*' --max-time 15 http://127.0.0.1/ -H 'Host: example.com'

Для HTTPS нужен ещё и SNI:

curl -v --noproxy '*' --max-time 15 --resolve example.com:443:127.0.0.1 https://example.com/

Если сертификат уже известен как проблемный, запрос можно один раз повторить с -k. В мониторинг и рабочие скрипты этот ключ переносить не рекомендую.

Локальный 200 или ожидаемый редирект подтверждает, что веб-сервер обработал запрос с нужными Host и SNI через loopback. Если сайт по-прежнему недоступен, то проблему нужно искать между клиентом и сервером — в локальном файрволе, сетевом экране хостера, балансировщике, CDN или маршрутизации.

Локально работает, снаружи тишина

Если ситуация такая, то начните с правил на самом сервере. Сначала разберитесь, какой firewall backend используется. Для nftables команда будет такой:

sudo nft list ruleset

Если хост использует iptables, одного iptables -S мало, ведь он показывает только filter table текущего семейства. Для полной картины пригодятся:

sudo iptables-save

sudo ip6tables-save

К слову, Docker создаёт правила для bridge-сетей и публикации портов, а опубликованный порт способен обойти обычную логику ufw. Поэтому для начала уточните сетевой режим и firewall backend Docker. 

Следом глядите на сетевой экран в панели хостера — он находится за пределами гостевой ОС, поэтому в локальном наборе не появится. То же относится к балансировщику и списку разрешённых адресов на origin.

Если на сервере установлен Fail2ban, проверьте, не заблокировал ли он IP-адрес, с которого выполняется внешний запрос. В Fail2ban правила сгруппированы по jail, то есть по отдельным наборам фильтров для SSH, nginx и других сервисов. Первая команда покажет активные jail:

sudo fail2ban-client status

Затем подставьте имя нужного jail во вторую команду. В её выводе будет список заблокированных адресов:

sudo fail2ban-client status ИМЯ_JAIL

Если правила выглядят нормально, смотрите пакеты:

sudo tcpdump -ni any 'tcp port 80 or tcp port 443'

В другом окне повторите внешний curl. Если нет SYN на origin, значит, запрос потерялся раньше или ушёл на другой IP. Если SYN есть, но SYN-ACK не уходит, проверяйте firewall, policy routing и сокет. Если SYN-ACK ушёл, но клиент его не видит, смотрите обратный маршрут и провайдера.

Фронтенд отвечает, приложение нет

Если nginx принимает запрос, но возвращает 502 или 504, то проблема чаще всего находится между ним и приложением. То же касается ситуации, когда статическая страница открывается, а динамические разделы не работают. Тут смотрите на активную конфигурацию через sudo nginx -T и найдите директиву, по которой nginx передаёт запросы дальше: proxy_pass, fastcgi_pass или uwsgi_pass.

После этого обратитесь к upstream напрямую. Используйте тот же протокол, адрес, порт, Host и путь, которые указаны в конфигурации nginx:

sudo ss -ltnp 'sport = :8000'

curl -v --max-time 10 http://127.0.0.1:8000/health -H 'Host: example.com'

Порт 8000 и путь /health привёл для примера. Подставьте адрес и URL из конфигурации nginx. Если отдельного healthcheck нет, запросите рабочий динамический маршрут.

HTTP-upstream может быть подключён и через Unix-сокет. В таком случае смотрите через: 

curl --unix-socket /run/myapp/app.sock http://localhost/health

Сокеты из fastcgi_pass и uwsgi_pass работают не по HTTP, поэтому обычным curl их не проверить. Сначала проверьте наличие сокета, права на него, статус PHP-FPM и журналы. Для полноценного запроса потребуется FastCGI-клиент, например, cgi-fcgi, с параметрами конкретного приложения.

Для контейнеров нужны другие команды:

docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

docker logs --since 30m --tail 200 myapp

docker inspect --format '{{json .State.Health}}' myapp

docker port myapp

Пустой docker port норма, если прокси находится в той же сети или используется host network. Up означает лишь, что жив PID 1, а Health появится только при настроенном healthcheck.

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

pg_isready -h 127.0.0.1 -p 5432 -d appdb

mysqladmin --host=127.0.0.1 --port=3306 ping

Напомню, имена юнитов зависят от пакета: postgresql.service бывает мета-юнитом, а MySQL может называться mysql. При 504 смотрите также блокировки, медленные запросы и пул соединений.

Шаг 4. Проверьте TLS 

Для OpenSSL 1.1.1+/3.x цепочку и соответствие имени можно проверить так:

openssl s_client \

  -connect 203.0.113.10:443 \

  -servername example.com \

  -verify_hostname example.com \

  -verify_return_error \

  -brief </dev/null

-servername передаёт SNI, -verify_hostname проверяет имя, а -verify_return_error не даёт s_client продолжить работу после ошибки проверки. А все сертификаты, присланные сервером, выводит команда:

openssl s_client \

  -connect 203.0.113.10:443 \

  -servername example.com \

  -showcerts </dev/null

На старой системе сначала смотрите openssl version (часть ключей может отсутствовать). Сертификат выбранного IP легче всего проверить через curl --resolve.

Шаг 5. Обратите внимание на ресурсы  

Если процессы на месте, но запросы висят, смотрите ресурсы:

df -h

df -i

free -h

uptime

vmstat 1 5

sudo journalctl -k -g 'oom|out of memory|killed process'

Первая строка vmstat 1 содержит средние значения с момента загрузки, текущую картину дают последующие. В systemd версий до 237 нет journalctl -g, там используйте:

sudo journalctl -k --no-pager \

  | grep -Ei 'oom|out of memory|killed process'

Если df -h показывает заполненную файловую систему, выясните, какой каталог занял место. Например, содержимое /var можно проверить так:

sudo du -xhd1 /var 2>/dev/null | sort -h

Ключ -x не даёт du переходить на другие файловые системы, а -d1 ограничивает проверку каталогами первого уровня. После уже можете изучить самый крупный каталог.

К слову, показания df и du иногда не совпадают — это происходит, когда файл уже удОлён из каталога, но процесс продолжает держать его открытым. Имени у файла больше нет, поэтому du его не видит, однако занятые блоки всё ещё учитываются в df. Найти такие файлы поможет команда:

sudo lsof +L1

Место освободится, когда процесс закроет файл, но в слепую его не завершайте. 

На счёт df -i. Если закончились inode, свободное место на диске не поможет — система не сможет создать новый файл. Причиной могут быть каталоги с множеством мелких файлов, например, кэшем, сессиями или очередью.

Если с диском всё в порядке, переходите к памяти и I/O. В выводе free -h смотрите прежде всего на available, а в vmstat следите за si и so. Постоянный обмен данными со свопом при низком available говорит о нехватке памяти. Записи Killed process в журнале ядра подтверждают, что до процессов уже добрался OOM Killer (про него рассказывал тут).

Высокий load при свободном CPU чаще всего связан не с вычислениями, а с процессами в состоянии D, которые ждут диск, NFS или другой I/O. На виртуалке проверяйте и %st в top — высокие цифры значат, что гипервизор регулярно забирает процессорное время у гостевой системы.

Есть ли короткий маршрут до причины

Начните с внешнего curl и определите, на какой стадии обрывается запрос. Затем проверьте DNS через dig и нужный origin через curl --resolve. На сервере посмотрите слушающие сокеты, состояние nginx и конфигурацию через nginx -t.

После этого выполните локальный запрос. Если локально сайт работает, а снаружи нет, проверяйте файрвол, сетевой экран хостера, балансировщик, CDN и маршрутизацию. Если nginx возвращает 502 или 504, переходите к upstream, приложению, сокетам и базе. TLS, диск, память и I/O проверяйте по симптомам, которые уже показали curl, журналы и состояние сервисов.

После починки НАСТРОЙТЕ УЖЕ внешний мониторинг, ротацию логов и алерты на диск, inode, память и сервисы. Если используете Certbot, прогоните certbot renew --dry-run. Автозагрузку проверяйте по реальным именам юнитов:

systemctl is-enabled nginx

systemctl is-enabled myapp

systemctl is-enabled postgresql@16-main

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

Делитесь в комментариях, сталкивались ли вы с такой ситуацией? Что в итоге оказалось причиной?

© 2026 ООО «МТ ФИНАНС»