
Мониторинг шлёт тревогу, пользователи пишут, что «всё лежит», но 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 ООО «МТ ФИНАНС»


