## 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: 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 listen port. Use sudo ss -tulpn | grep to find the PID and ps -p -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 with sudo 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_pass to 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: 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 . ## 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 -V and note build options. Owner: engineer making the change. Frequency: before any major change or quarterly. - Configuration Backup : Before editing, copy nginx.conf and 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 -t after any configuration change. Owner: same as above. Frequency: every edit. - Graceful Reload : Use nginx -s reload or systemctl reload nginx to 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://localhost from 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-ip to test external reachability. Owner: on-call engineer. Frequency: after firewall or network changes. - Log Review : Check /var/log/nginx/error.log for 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 -v to 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 hostname and consider using a resolver directive 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:
SymptomFirst Command to RunExpected Normal Result
Connection refusedsudo ss -tulpn | grep nginxNginx listening on expected port
502 Bad Gatewaytail -f /var/log/nginx/error.logNo upstream connection errors
DNS failuredig +short upstream.example.comIP address returned
Connection timeoutsudo tcpdump -i any -s 0 'tcp port 80'SYN and SYN-ACK packets visible
Permission denied on port 80sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/nginxNo error on start
Routing problemip route get 10.0.0.5Correct 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.