The Debugging Checklist — from the outside in
"It doesn't work." You'll hear that a lot, and nine times out of ten the fix turns out to be boring. Walk the layers from the outside in, one at a time, and the culprit almost always shows itself.
The five questions, in order
1. Can you find it?
Does DNS resolve? Does the IP look right? Are you even reaching the correct machine?
2. Right port?
Is anything actually listening on that port on that host? Is the app port the same as the exposed port?
3. Can traffic reach it?
Route tables, IGW/NAT, VPC peering — is there a path at all?
4. Is it blocked?
Firewalls, security groups, NACLs, host firewalls, NetworkPolicies — somewhere along the line, something is probably dropping your packet without a word.
5. NAT issues?
Can private hosts even reach the internet? Does the NAT have enough ephemeral ports?
Step by step
Step 1 — Resolve the name
dig +short api.example.com
# Empty? Wrong IP? DNS is the problem, stop and fix it.
# Compare to what your app is using:
getent hosts api.example.com
Step 2 — Check the port is up on the target
# From your machine
nc -zv api.example.com 443
# From inside a cluster
kubectl run tmp --rm -it --image=nicolaka/netshoot -- \
nc -zv api.example.com 443
If nc hangs or times out, you can't reach the port — stop here, the problem's on the network. If it connects, the network's doing its job and you should look further up the stack.
Step 3 — Confirm the app is listening at all
SSH into the server (or kubectl exec into the pod):
# Linux
ss -tlnp
# macOS
lsof -i -P -n | grep LISTEN
What you're hoping to see is the app bound to the port you expect, on 0.0.0.0 (all interfaces) — not just 127.0.0.1, which only talks to itself.
A classic bug: the app listens on localhost:3000, so nothing outside the container/host can reach it. Change the bind address to 0.0.0.0 and it works.
Step 4 — Trace the path
traceroute -T -p 443 api.example.com
# Windows: tracert -T
mtr api.example.com
Watch for where the packet dies. The last hop that answers is usually the last thing under your control — that's where you start digging.
Step 5 — Check every firewall on the way
- Host firewall on the server —
ufw status,iptables -L. - Cloud SG / NSG — does it allow inbound from the source CIDR on this port?
- NACL — stateless; check the reverse direction too.
- Kubernetes NetworkPolicy — is a policy blocking pod-to-pod traffic?
- WAF / API gateway — did it 403 the request?
Step 6 — Verify outbound / NAT
If the server can't reach out (package repos, webhooks, external APIs):
curl -v https://api.github.com
curl https://ifconfig.me # what public IP am I coming from?
Compare that against the NAT Gateway's IP. Different? Then your traffic isn't going out through the NAT you think it is.
Before you open a single config panel, run three quick checks from your own machine and you'll usually know which way to head. First, dig +short thedomain.com — do you even get an IP back, and is it the right one? Then nc -zv thedomain.com 443 — does the port answer, or does it just hang? Then curl -v https://thedomain.com — do you get a response, a TLS error, or nothing at all? Whichever one first misbehaves tells you the layer to focus on, so you're not guessing. Paste those three lines into the ticket too — it saves the next person from starting over.
A one-page mental model
When you're deep in a broken system, don't tweak three configs and re-test. Change one, verify, move on. Otherwise you'll never know which change actually fixed it.
Key takeaways
- Diagnose in order: DNS → port → path → firewall → NAT → app.
- Most bugs are boring: wrong port, missing route, silent firewall drop.
- Use minimal, isolated tests —
nc,curl -v,dig. - Change one thing at a time.