Website Down, but the Server Is Reachable? Trace the Failure from DNS to App
“SSH Works, the Site Doesn’t” — Start with the Right Question
The alerts start firing, users say the site is dead, and your first test gives you a strange kind of relief: SSH still lets you in. That moment feels reassuring because the server is not gone. But it is also where a lot of bad troubleshooting starts, because “I can still log in” is not the same thing as “the website should be working.”

The useful question is not “what can I restart first?” It is “which layer failed first?” A working SSH session proves the machine is reachable on port 22. It does not prove the domain resolves correctly. It does not prove:
- ports 80 and 443 are reachable
- HTTPS is healthy
- the application behind the web server is answering
This guide is built around that distinction, and it stays focused on diagnosis rather than trying to become a full nginx, DNS, TLS, Docker, or database manual.
⚠️ Warning: Blindly restarting nginx, Docker, PHP-FPM, or the whole VPS in the first minutes of an incident can erase the clues you need. Collect one round of evidence first, then change only the layer that actually failed.
The One-Minute Triage Map

Before you go deep, get oriented. The shape of the failure often tells you which layer deserves attention first, even when you do not know the root cause yet.
Treat the following table as a triage shortcut, not a final verdict.
| What you see | What it usually means | What to check first | What not to assume |
|---|---|---|---|
| 🧭 Could not resolve host | The name did not resolve to an address | DNS records, resolver path, typos | The web server is necessarily the problem |
| ⏱️ Timeout | Traffic is blocked, misrouted, or hanging later in the path | External curl, firewall path, routing, listener reachability | All timeouts mean the same thing |
| 🚫 Connection refused | The host is reachable, but nothing useful is accepting connections there | ss -ltnp, service state, bind address | The whole server is down |
| 🔐 TLS or certificate warning | HTTPS reached 443, but the identity or handshake layer failed | Served certificate, hostname match, chain, renewal state | The application itself is definitely dead |
| ⚠️ 502 / 503 / 504 | A reachable frontend or service is failing higher in the chain | Upstream handoff, service availability, timeout location | Every 5xx error means the same fix |
| 🏠 Works locally but not externally | The stack may be fine on the server, but the outside path is broken | Host firewall, provider firewall, CDN path, routing | Local success proves public reachability |
What matters is the first broken stage. If DNS fails, later layers are noise. If 443 answers but TLS breaks, the app is not your first question yet. The mental model below is what makes that sequence feel logical instead of random.
Why SSH Does Not Prove the Website Works
SSH and web traffic are different paths with different jobs. SSH on port 22 proves you can reach the machine through its remote-management door. A website depends on ports 80 and 443, plus the layers behind them. Those are separate tests, so “server reachable” and “website reachable” are not interchangeable statements.

The easiest way to picture it is as an office building. DNS helps a visitor find the building address. Ports 80 and 443 are the front desk for public visitors. The web server is the receptionist who accepts the request and decides where it goes next. The application is the office that does the real work. A database or another dependency may sit further inside the building. SSH is a different entrance entirely. It is useful for staff, but it does not prove that the front desk is open or that the office handoff works.
Browser
↓
DNS lookup
↓
IP address
↓
Port 80 / 443
↓
Web server
↓
App / upstream
↓
Database / dependencyWhen people say a service is “listening,” they mean it is actually accepting connections on the expected endpoint. That is the distinction that turns a vague “the server is up” moment into a traceable request path.
That matters because HTTPS can fail before the app ever answers, and proxy or application failures can happen after the frontend is already reachable. So the next move is always the same: look from outside first and find the last successful stage of the request.
Step 1: Reproduce the Failure from Outside the Server
Start from the client side, not from inside the VPS. If possible, test from another network or device first so you do not confuse a local DNS cache, an old /etc/hosts entry, or a local firewall/VPN issue with a real server outage.
Use a verbose external request so you can see how far the request gets before it dies:
curl -v --connect-timeout 5 --max-time 15 https://example.com/curl -v is not an expert-only tool here. Read it as a progress trace. If it never resolves the name, you are in the DNS branch. If it connects and then says Connection refused, the host answered but nothing useful is accepting traffic there. If it hangs until timeout, think filtering, routing, or a deeper hang later in the request path. If you receive an HTTP response, even an error page, you have already moved past the connection layer and into a higher branch.
💡 Tip: Compare IPv4 and IPv6 early. A forgotten AAAA record can make the outage look inconsistent because some clients prefer IPv6 first and others do not.
Run the same test once per protocol family when dual stack is in play:
curl -4 -v --connect-timeout 5 --max-time 15 https://example.com/
curl -6 -v --connect-timeout 5 --max-time 15 https://example.com/If IPv4 works and IPv6 fails, or the reverse, you have already narrowed the incident faster than a service restart ever would. If the failure begins at name resolution or destination choice, DNS is the next clean branch to check.
Step 2: Check DNS and Confirm the Right Destination
Before you debug nginx, confirm the domain is actually sending visitors to the server you think it is. That is especially important after migrations, IP changes, CDN adjustments, or partial record edits.
Check the public records first:
dig +short A example.com
dig +short AAAA example.comThese two lines answer a very practical question: where does the internet believe example.com lives right now? One common failure shape is that SSH by IP reaches the new VPS, but the domain still points to the old address. Another is that the A record was updated, but the AAAA record still points somewhere stale. In that case, only part of your traffic fails.
📝 Note: curl --resolve is safer than changing public DNS mid-incident. It lets you test the origin you intend to use while keeping the hostname and SNI intact.
Use curl --resolve to force a test against the IP you expect without touching public records:
curl --resolve example.com:443:203.0.113.10 https://example.com/If that works while the public domain still fails, the server may be fine and DNS may still be the broken layer. One bounded CDN note is worth keeping in mind here. If your origin is locked down to accept traffic only from CDN IP ranges, a direct-origin test may fail simply because the origin expects edge traffic, not arbitrary public requests. Once the destination is confirmed, the next question is whether anything useful is answering on 80 or 443 there.
Step 3: Verify What Is Listening on 80/443
Now switch to the server and ask a narrow question: is anything actually accepting web connections on the expected ports? The machine can be alive, SSH can work, and nginx can even be installed. Yet the public web ports can still have no useful listener.
Check listeners first:
sudo ss -ltnpEmpty output for :80 or :443 means nothing useful is listening there. A listener on 127.0.0.1 means the service is accepting connections only from the local machine. A listener on 0.0.0.0 means it is bound on IPv4 interfaces. [::] usually means IPv6 interfaces. Do not assume IPv6 binding automatically guarantees the IPv4 path you need.
Then use a small nginx health bundle before changing anything:
sudo systemctl status nginx --no-pager -l
sudo nginx -t
sudo journalctl -u nginx --since '-30 minutes' --no-pager- If systemctl says active (running), that only proves the service process exists
- nginx -t tells you whether the config is valid
- journalctl shows whether a recent reload failed, a certificate file went missing, or a vhost broke on startup.
For Apache readers, the equivalent syntax check is apachectl configtest. Once you know something is listening, the next proof is more specific: does the right site answer locally when you remove the outside network from the equation?
💡 Tip: Test config first, then prefer reload over a blind restart when appropriate. A reload validates the new config and keeps the old workers if the new config is bad; a blind restart is much rougher in the middle of an incident.
Step 4: Test the Site Locally with the Right Host and SNI
This is the most important fork in the whole investigation. A plain curl 127.0.0.1 can be misleading. Many servers host multiple sites and choose the response based on the Host header or, for HTTPS, SNI. You are not asking whether something responds locally. You are asking whether the correct site path responds locally.
Use local tests that preserve the hostname logic:
curl -I http://127.0.0.1/ -H 'Host: example.com'
curl -v --resolve example.com:443:127.0.0.1 https://example.com/
# Only as a one-off diagnostic if you already know the cert is bad:
curl -vk --resolve example.com:443:127.0.0.1 https://example.com/A meaningful success is the expected page, expected redirect, or expected application response from the correct site. It is not the default nginx host, the wrong certificate, or a generic “it returned HTML.” From here there are three clean outcomes: local success, local wrong-site or default-cert behavior, or local failure/timeout. A good local result points outward to firewall, provider, CDN, or routing checks. A bad local result keeps you inside the stack, in the upstream or TLS branches.
Step 5: If It Works Locally but Not Externally, Trace the Network Path
Once the local test is good, stop second-guessing nginx for a moment. The site stack is probably alive on the server, and the missing piece is usually somewhere between the visitor and that working local service. Start with the host firewall because it is the closest external boundary you control.
Inspect the host-side rules with the tool your system actually uses, and do one fast sanity check for self-inflicted blocking while you are there:
sudo nft list ruleset
# Or, on systems still using iptables directly:
sudo iptables-save
sudo ip6tables-save
# Fast sanity check for self-inflicted blocking:
sudo fail2ban-client statusThat output only shows the guest OS layer. It does not show provider-side filtering, security groups, or panel-side firewall rules that live outside the VPS itself. On an AlexHost VPS, for example, the machine firewall and any panel-level network controls are separate questions. Both matter when local tests work but public visitors still fail.
⚠️ Warning: If Docker publishes ports on the host, do not assume UFW output tells the whole story. Docker can route published container traffic through NAT before UFW’s usual chains. That means “UFW looks fine” does not always mean the packet path is fine.
CDNs and load balancers deserve their own branch here too. The origin may be healthy and still unreachable directly because only edge IP ranges are allowed to talk to it. When you need proof of whether packets arrive at all, use tcpdump as a yes-or-no tool:
📝 Note: A failed direct-origin test behind a CDN allowlist usually points to the edge policy, not to a dead origin. In that setup, the origin is designed to trust the CDN path, not every direct visitor.
sudo tcpdump -ni any 'tcp port 80 or tcp port 443'If you see no SYN packets at all, the traffic is not reaching the server. If SYNs arrive and no SYN-ACK leaves, the server or firewall path is still blocking the handoff. If neither pattern appears to be the blocker, the remaining failures usually sit behind the frontend in the upstream handoff.
Step 6: If the Frontend Answers but the Site Is Still Broken, Follow the Upstream
In this branch, the web server is reachable, but the next hop behind it is not healthy enough to complete the request. Here, “upstream” means the service nginx hands the request to next: an app process, a socket-backed runtime, a container, or another internal dependency.
Static pages working while login, search, checkout, or API routes fail is a strong clue that the frontend is present and the failure starts at the handoff behind it.
📝 Note: Treat 502 as “the next hop answered badly” and 504 as “the next hop answered too slowly.” Both are signs to follow the upstream path rather than stop at the frontend.
Inspect the active handoff, then test the upstream directly:
sudo nginx -T
# Direct HTTP upstream example
curl -i http://127.0.0.1:3000/
# Unix-socket-backed HTTP example
curl --unix-socket /run/app.sock http://localhost/In the nginx output, look for directives such as proxy_pass, fastcgi_pass, or uwsgi_pass. You are checking whether nginx is pointing at the right target, over the right protocol, on the right port or socket. If containers are involved, add a short container health pass instead of guessing:
docker ps
docker logs --tail 50 <container_name>
docker inspect --format '{{json .State.Health}}' <container_name>
docker port <container_name>If the direct app test fails, the problem is behind the web server. If it works directly but fails through nginx, the handoff config is the branch to inspect. Database reachability matters only as a dependency check here, not as a separate deep-dive. If this upstream-failure pattern keeps repeating, that is the right moment to switch to a dedicated troubleshooting guide instead of stretching one incident into guesswork.
Step 7: Isolate TLS and Certificate Failures
This branch is narrower: something is answering on 443, but the browser still cannot complete a clean, trustworthy HTTPS session. A successful TCP connection to port 443 does not prove the certificate, hostname match, or handshake path is healthy.
Inspect what certificate is actually being served:
openssl s_client -connect example.com:443 -servername example.com -verify_hostname example.com -briefThis is where you catch the common failure shapes: the wrong hostname, an expired certificate, an incomplete chain, or a renewal that never finished cleanly. In plain language, SNI tells the server which hostname you meant. -verify_hostname checks whether the certificate it served matches that hostname. After recovery, validate the renewal path so this does not become the next outage:
⚠️ Warning: If you rely on HTTP-01 validation for certificate renewal, inbound port 80 must be reachable. A firewall or provider rule blocking 80 can quietly break renewals long before users report that HTTPS looks dead.
sudo certbot renew --dry-runStep 8: Check Resource Pressure Before You Call It Random
Some incidents are not reachability failures at all. The path is technically intact, but the server is too starved, blocked, or overloaded to answer in time. That is when a site can look “partly alive” from one angle and still feel dead to users.
Run a small first-pass resource bundle:
df -h
df -i
free -h
uptime
vmstat 1 5
sudo journalctl -k -g 'oom|out of memory|killed process'Read the results in patterns, not in isolation.
- df -h shows ordinary disk exhaustion.
- df -i catches inode exhaustion, where space appears to exist but the filesystem cannot create more entries.
- free -h matters most when available memory collapses and swap activity rises.
- uptime can show high load even when CPU is not pegged, which often means tasks are waiting on disk or memory pressure rather than active compute.
- Kernel log lines about OOM events tell you whether the system started killing processes to survive.
Provider graphs can confirm the timeline. On an AlexHost VPS, they can be useful for checking whether spikes in RAM, disk, or I/O line up with the outage. But the terminal evidence should still lead the diagnosis. This section is not a tuning guide; it is the branch that tells you the site may be failing under pressure rather than failing to route.
Think in Layers, Not in Panic

When SSH works but the website will not open, keep the chain short and repeatable:
- reproduce the failure externally
- identify the last successful stage
- confirm DNS and destination
- verify a real listener on 80/443
- test the correct site locally
- branch into network path, upstream, TLS, or resources
💡 Tip: Do not close your last working SSH session until you have confirmed a fresh login still works and you still have a backup access path, such as provider console access. During a live incident, preserving control matters as much as fixing the first symptom.
Keep habits light: monitor externally, keep logs, and test certs with certbot renew –dry-run. Secure access with backups and a console path. Provider tools — firewall, graphs, console (including AlexHost) — should support troubleshooting, not replace it. Focus on fixing the first broken layer so each incident is handled with clearer evidence and less panic.
on All Hosting Services