Intro
When Nginx stops routing traffic the way you expect, the cause is usually hiding somewhere in the network layer: a DNS lookup that no longer resolves, a port that is already bound, a firewall rule that silently drops packets, or a route that points the wrong way. Figuring out which one is the problem can feel like guesswork unless you work through the stack methodically.
This guide gives you a practical troubleshooting workflow for Nginx networking. It is built around three phases: gather a clean inventory of your environment, make changes safely with a rollback path, and verify each change with concrete commands before moving on. Every step includes the exact command to run, what normal output looks like, and how to recover if something goes wrong.
We will use a typical reverse proxy setup as our running example throughout the article: a client reaches Nginx on host A (IP 192.168.1.100) listening on ports 80 and 443, and Nginx forwards requests to a backend application on host B (IP 10.0.0.5) listening on port 8080. Adjust the IPs and ports to match your own environment as you follow along.
Version and Environment Inventory
Before you change anything, know exactly what you are working with. This saves you from chasing a problem that is actually caused by an old Nginx version or a module that was never compiled in.
Check Nginx Version and Build Options
Run this command to see the Nginx version and the configure arguments used at build time:
nginx -V
Expected output is verbose, but the key lines look like this:
nginx version: nginx/1.18.0
built by gcc 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04)
built with OpenSSL 1.1.1f 31 Mar 2020
TLS SNI support enabled
configure arguments: --with-http_ssl_module --with-stream --with-stream_ssl_module ...
The --with-stream flag matters if you are troubleshooting TCP or UDP proxying. If you need stream support and it is missing, you will have to install a different package (like nginx-full on Debian/Ubuntu) or recompile Nginx yourself. Do not waste time debugging a stream configuration that the binary cannot even parse.
Identify Network Topology
Draw a simple diagram of your setup, even if it is just on a whiteboard. For our running example, the topology is:
Client -> Nginx (host A, 192.168.1.100:80/443) -> Backend app (host B, 10.0.0.5:8080)
Then confirm the IP addresses and interfaces on the Nginx host with:
ip addr show
Relevant output snippet:
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.100/24 brd 192.168.1.255 scope global dynamic eth0
valid_lft 86366sec preferred_lft 86366sec
Note the interface name (eth0), the IP address (192.168.1.100), and the subnet mask (/24). You will need these when checking routes and firewall rules later.
Check Listening Ports
Use ss to see which ports Nginx is currently listening on. This often immediately reveals a port conflict or a misconfigured listen directive.
sudo ss -tulpn | grep nginx
Expected output example for a default HTTP server:
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1234,fd=6))
tcp LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=1234,fd=7))
If you see nothing, Nginx is either not running or is configured to listen on a different port than you expected. Check systemctl status nginx and your listen directives.
Safe Configuration Path
Never edit the main nginx.conf directly when you are experimenting. Instead, use a separate configuration file that you can add and remove without touching the working setup.
Use a Separate Configuration File for Testing
Create a new file in the conf.d directory with a minimal server block that changes only the listen port to something safe, like 8081. This lets you test whether the problem is specific to a certain port or binding.
Example file /etc/nginx/conf.d/test-networking.conf:
server {
listen 8081;
server_name localhost;
location / {
return 200 "Test OK";
}
}
Test the configuration before applying it:
nginx -t
Expected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If the test passes, reload Nginx gracefully. This does not drop existing connections, so production traffic is unaffected.
sudo nginx -s reload
Verify that the new port is listening:
sudo ss -tulpn | grep 8081
Expected output:
tcp LISTEN 0 511 0.0.0.0:8081 0.0.0.0:* users:(("nginx",pid=5678,fd=8))
To revert, remove the test file and reload again:
sudo rm /etc/nginx/conf.d/test-networking.conf
sudo nginx -s reload
Change DNS Resolution Configuration Safely
If you suspect DNS issues, do not change /etc/resolv.conf on the whole server right away. Instead, test a different DNS server only for Nginx by adding a resolver directive inside a specific server or location block.
For example, to test whether Google Public DNS (8.8.8.8) resolves your upstream hostname correctly, add this to your test server block:
resolver 8.8.8.8 valid=30s;
Then add a location that proxies to your backend using a variable so Nginx re-resolves at runtime:
set $backend "backend.example.com";
location / {
proxy_pass http://$backend;
}
Test, reload, and then make a request. If the request succeeds with the new resolver but failed before, your system DNS was the problem. If it still fails, check connectivity to the DNS server itself (ping 8.8.8.8) and the response with dig @8.8.8.8 backend.example.com.
Test Firewall Rules Incrementally
When opening a new port, do it in a controlled way so you can remove the rule quickly if it causes issues. On a host using iptables, insert a temporary ACCEPT rule for the test port:
sudo iptables -I INPUT -p tcp --dport 8081 -j ACCEPT
After testing, delete the rule by repeating it with -D:
sudo iptables -D INPUT -p tcp --dport 8081 -j ACCEPT
Always list the current rules with packet counts before and after to confirm the change and catch duplicates:
sudo iptables -L -n -v
Expected snippet after adding the temporary rule:
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8081
Verification and Diagnostics
Once you have made a change, verify it works from the inside out. Start with local connectivity, then DNS, then packet-level inspection.
Check Basic Connectivity with curl
Use curl with the -v flag to see the full request and response, including connection details.
curl -v http://localhost:8081/
Expected output (partial):
* Trying 127.0.0.1:8081...
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET / HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.68.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.18.0
< Content-Type: text/plain
< Content-Length: 7
<
Test OK
If the connection fails, read the error message carefully:
Connection refused: Nginx is not listening on that port, or a firewall is actively rejecting the connection with a TCP RST.Connection timed out: The host is unreachable, or a firewall is silently dropping packets (no response at all).Could not resolve host: DNS resolution failed for the name you used in the URL.
Test DNS Resolution
If your Nginx configuration uses a hostname in proxy_pass without a variable, Nginx resolves that hostname once at startup. A DNS failure then prevents Nginx from starting. Test resolution independently with dig:
dig +short backend.example.com
Expected output if it resolves:
10.0.0.5
If you get no output, check /etc/resolv.conf for the system DNS servers and ensure they are reachable:
cat /etc/resolv.conf
Example output:
nameserver 192.168.1.1
Then test the server directly:
dig @192.168.1.1 backend.example.com
If that fails, the DNS server is down or cannot resolve the name. For dynamic DNS environments, use the resolver directive with a valid parameter as shown earlier to make Nginx re-resolve at runtime.
Use tcpdump to Capture Traffic
When higher-level checks are inconclusive, capture packets to see exactly what is on the wire. Run tcpdump on the Nginx host and filter for your test port:
sudo tcpdump -i any -s 0 -A 'tcp port 80'
In another terminal, make a request with curl http://localhost/. You should see the TCP three-way handshake (SYN, SYN-ACK, ACK) followed by the HTTP request and response.
Expected tcpdump output snippet:
12:34:56.789012 IP client.54321 > server.http: Flags [S], seq 123456789, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
12:34:56.789123 IP server.http > client.54321: Flags [S.], seq 987654321, ack 123456790, win 65160, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
12:34:56.789234 IP client.54321 > server.http: Flags [.], ack 1, win 502, length 0
If you see only SYN packets without any SYN-ACK responses, the server is not answering on that port. That points to Nginx not listening (check ss), a firewall dropping the SYN, or the packet never arriving due to a routing problem.
Check Routing and Firewall Rules
Make sure the route to the destination is correct. For outbound connections from Nginx to an upstream, check the routing table:
ip route show
Expected output example for our setup:
default via 192.168.1.1 dev eth0 proto dhcp metric 100
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100 metric 100
The default route goes through 192.168.1.1, which should be your gateway. For a backend on 10.0.0.0/24, you would need a specific route or a VPN/tunnel. Use ip route get 10.0.0.5 to see which interface and gateway would be used for that IP.
For firewall rules, list them with packet counts to see if any are being hit:
sudo iptables -L -n -v
Look for DROP or REJECT rules that affect your port or source/destination. Packet counters that increase when you make a test request indicate the rule is being matched.
Inspect Nginx Error Logs
Nginx logs detailed errors to /var/log/nginx/error.log by default. Increase the log level temporarily to debug if the default error level does not give enough detail. Add this at the top of nginx.conf inside the http block (or globally):
error_log /var/log/nginx/error.log debug;
Reload Nginx, reproduce the issue, and examine the log. A DNS failure might appear as:
[error] 1234#1234: *1 backend.example.com could not be resolved (110: Operation timed out), client: 192.168.1.10, server: localhost, request: "GET / HTTP/1.1", host: "backend.example.com"
Remember to change the log level back to error or warn after you are done to avoid filling the disk:
error_log /var/log/nginx/error.log error;
Then reload again.
Failure Modes and Recovery
Even with careful testing, changes can break things. Here are the most common failures and how to recover quickly.
Nginx Fails to Start After Configuration Change
If nginx -t passes but Nginx still cannot start, check the error log for startup errors. Common causes:
- Port already in use: another process is bound to the
listenport. Usesudo ss -tulpn | grep <port>to find the PID andps -p <PID> -o comm=to identify it. - Permission denied: listening on a port below 1024 requires root or
CAP_NET_BIND_SERVICE. If you run Nginx as a non-root user, you can grant the capability withsudo setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx. - Invalid directive that only fails at runtime: some directives are syntactically valid but fail when Nginx tries to apply them (e.g., a
proxy_passto a non-existent UNIX socket).
Recovery: Revert to the known-good configuration. Always keep a timestamped backup before editing:
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%Y%m%d)
If needed, restore it:
sudo cp /etc/nginx/nginx.conf.bak-20240315 /etc/nginx/nginx.conf
sudo nginx -t && sudo systemctl start nginx
Then check systemctl status nginx to confirm the process is running.
Upstream DNS Resolution Failure
If Nginx cannot resolve an upstream hostname at startup (when proxy_pass uses a literal hostname without a variable), it will fail to start. To avoid this, use a static IP in proxy_pass, or use a variable combined with a resolver directive to allow runtime resolution and a graceful 502 error if DNS fails:
resolver 8.8.8.8;
set $backend "backend.example.com";
location / {
proxy_pass http://$backend;
}
With this setup, if DNS fails at request time, Nginx returns 502 Bad Gateway instead of crashing. You can then fix the DNS issue (check dig, adjust /etc/resolv.conf or the resolver IP) and reload.
Recovery: Verify DNS with dig +short backend.example.com, fix the resolver configuration, and reload Nginx.
Firewall Blocks Traffic Unexpectedly
If you add a firewall rule that blocks legitimate traffic, remove it quickly. List rules with line numbers:
sudo iptables -L INPUT --line-numbers
Output snippet:
Chain INPUT (policy ACCEPT)
num target prot opt source destination
1 ACCEPT tcp -- anywhere anywhere tcp dpt:ssh
2 DROP tcp -- anywhere anywhere tcp dpt:80
Delete the DROP rule on line 2:
sudo iptables -D INPUT 2
If you use iptables-persistent or a similar tool, remember to save the current rules after fixing them so a reboot does not restore the bad rule:
sudo netfilter-persistent save
Network Interface Misconfiguration
If the network interface is down or misconfigured, Nginx may run but not be reachable from the network. Check the interface state:
ip link show eth0
Expected output if the interface is up:
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff
If the state is DOWN, bring it up:
sudo ip link set eth0 up
Then verify the IP address was restored with ip addr show eth0. If the IP is missing, the interface may be configured for DHCP; check sudo dhclient -v eth0 or your network manager.
Common Pitfalls and How to Avoid Them
Even experienced administrators trip over these recurring issues. Knowing them in advance saves time.
1. Forgetting to test the configuration before reloading
Why it happens: You are in a hurry, make a quick edit, and run nginx -s reload without nginx -t.
How to avoid: Always run nginx -t first. Make it a muscle-memory habit. The command takes less than a second and catches 90% of syntax errors.
Recovery: If you already reloaded and Nginx now fails to serve traffic, the old worker processes may still be running. Check ps aux | grep nginx and, if necessary, restart the service with systemctl restart nginx after fixing the config.
2. Editing the wrong configuration file
Why it happens: Multiple config files in conf.d/ or sites-enabled/ with similar names, or you edit the main nginx.conf but the actual server block is in an included file.
How to avoid: Use nginx -T to dump the complete effective configuration and confirm which file contains the directive you are changing. Keep all your server blocks in clearly named files.
Recovery: Revert the accidental edit by restoring a backup of that file, or use version control (git) for /etc/nginx/ to roll back.
3. Using resolver with a DNS server that is not reachable from the Nginx host
Why it happens: You copy a resolver 8.8.8.8; directive from an online guide, but your firewall blocks outbound DNS to external servers, or you are in an isolated network.
How to avoid: Test DNS server reachability first with dig @8.8.8.8 example.com. Use internal DNS servers if available, and always set a valid parameter to avoid hammering a slow resolver.
Recovery: Change the resolver to a reachable server (e.g., your gateway 192.168.1.1), test and reload.
4. Changing firewall rules without a rollback plan
Why it happens: You add a DROP rule to block a port, then close the SSH session or get locked out because you also blocked port 22.
How to avoid: For remote servers, always schedule a firewall reset as a safety net. For example, run sudo iptables -P INPUT ACCEPT; sudo iptables -F or use a tool like ufw with sudo ufw reset. Better yet, use a configuration management tool with automatic rollback. At minimum, test changes from a second terminal or console.
Recovery: If locked out, use out-of-band console access (cloud provider console, IPMI) to reset the firewall, or reboot the server if the rules are not persistent.
5. Ignoring Nginx error logs during troubleshooting
Why it happens: The error log can be verbose, and you may be focused on network-level tests.
How to avoid: Run tail -f /var/log/nginx/error.log in a separate terminal while you reproduce the issue. The log often contains the exact reason (e.g., connect() failed (111: Connection refused) while connecting to upstream).
Recovery: If you are not seeing enough detail, temporarily enable debug logging as described earlier, reproduce, and then revert.
6. Assuming Nginx reload applies all changes
Why it happens: Some changes require a full restart (e.g., changing the user directive or the worker_processes setting).
How to avoid: Know which directives are reloadable. Nginx logs a warning when you try to reload a non-reloadable change, but it may still leave the old behavior in place. If in doubt, do a full systemctl restart nginx during a maintenance window.
Recovery: If a reload did not pick up your change, restart Nginx and verify with nginx -T | grep <directive>.
Operations Checklist
Use this checklist for routine Nginx networking health checks and before/after any configuration change. Assign a single owner (e.g., the on-call engineer or the person making the change) and review the results weekly or after every change.
- Version Check: Run
nginx -Vand note build options. Owner: engineer making the change. Frequency: before any major change or quarterly. - Configuration Backup: Before editing, copy
nginx.confand any included files to a timestamped backup (e.g.,cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%Y%m%d)). Owner: whoever edits config. Frequency: every edit. - Syntax Test: Always run
nginx -tafter any configuration change. Owner: same as above. Frequency: every edit. - Graceful Reload: Use
nginx -s reloadorsystemctl reload nginxto apply changes without dropping connections. Owner: same as above. Frequency: after successful test. - Port Verification: After reload, check listening ports with
sudo ss -tulpn | grep nginx. Owner: same as above. Frequency: after every reload. - Connectivity Test: Use
curl -I http://localhostfrom the server itself to test local response. Owner: on-call engineer. Frequency: after every change, or as part of health checks. - External Test: From a different machine, use
curl -I http://server-ipto test external reachability. Owner: on-call engineer. Frequency: after firewall or network changes. - Log Review: Check
/var/log/nginx/error.logfor any new errors after changes. Owner: on-call engineer. Frequency: after every change and during incident response. - Firewall Audit: Periodically review firewall rules with
sudo iptables -L -n -vto ensure no unintended blocks. Owner: security or infrastructure team. Frequency: monthly or after any firewall change. - DNS Health: If using hostnames, verify they resolve with
dig +short hostnameand consider using aresolverdirective with a fallback. Owner: infrastructure team. Frequency: weekly or when DNS changes.
Here is a concise reference table for common symptoms and the first commands to run:
| Symptom | First Command to Run | Expected Normal Result |
|---|---|---|
| Connection refused | sudo ss -tulpn | grep nginx | Nginx listening on expected port |
| 502 Bad Gateway | tail -f /var/log/nginx/error.log | No upstream connection errors |
| DNS failure | dig +short upstream.example.com | IP address returned |
| Connection timeout | sudo tcpdump -i any -s 0 'tcp port 80' | SYN and SYN-ACK packets visible |
| Permission denied on port 80 | sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx | No error on start |
| Routing problem | ip route get 10.0.0.5 | Correct interface and gateway |
Conclusion
Systematic troubleshooting of Nginx networking issues is not about memorizing every command; it is about following a repeatable process. Start with a clean inventory of versions, interfaces, and ports. Make changes incrementally in a separate config file with nginx -t and graceful reloads. Verify each change with curl, ss, dig, and tcpdump as needed. And always have a rollback path: backups, reversible firewall rules, and documented recovery steps.
The most common pitfalls we covered—skipping config tests, editing the wrong file, unreachable resolvers, risky firewall changes, and ignoring error logs—are all avoidable with a little discipline. Add the operations checklist to your team's runbook and assign clear owners so nothing slips through.
Start with a narrow, measurable pilot change, such as adding a test server on port 8081 or introducing a resolver directive. Inspect the result locally, then expand to production. By practicing these steps, you will reduce rework, shorten incident response times, and keep your Nginx infrastructure stable. Next time a networking issue appears, you will know exactly where to look first.