Fix Nginx Not Connecting to Gunicorn (Connection Refused)
Fix Nginx connect() failed (111: Connection refused) to Gunicorn: service status, bind address mismatches, stale sockets, firewalls, and startup ordering.
Problem statement
The site returns 502 Bad Gateway, and the Nginx error log shows:
connect() failed (111: Connection refused) while connecting to upstream,
client: 203.0.113.7, server: www.example.com, request: "GET / HTTP/1.1",
upstream: "http://127.0.0.1:8000/", host: "www.example.com"
Error 111 is ECONNREFUSED: Nginx reached the address, and nothing accepted the connection. Either no process is listening there, or the process listens somewhere else.
Distinguish it from its neighbours before digging in:
| Nginx error | Meaning | Where to look |
|---|---|---|
(111: Connection refused) | nothing is listening at that address | Gunicorn status and bind address |
(2: No such file or directory) | the Unix socket path does not exist | Gunicorn bind path, RuntimeDirectory |
(13: Permission denied) | the socket exists but Nginx may not use it | socket ownership and mode |
upstream timed out | Gunicorn accepted but did not respond in time | slow view, worker exhaustion |
This guide walks from "is Gunicorn running?" to "do both sides agree on the address?", then fixes the cause so it survives restarts and reboots.
Quick answer
- Check the service:
systemctl status myappandjournalctl -u myapp -n 50. - Check what is listening:
sudo ss -ltnp | grep 8000for TCP,sudo ss -xlp | grep gunicornfor Unix sockets. - Compare Gunicorn's
--bindwith Nginx'sproxy_pass; they must match exactly. - If Gunicorn is not running, read the journal for the startup error and fix that.
- For TCP binds, make sure Gunicorn listens on
127.0.0.1(not only on a different interface) and no firewall blocks local traffic. - Restart Gunicorn, reload Nginx, and verify with
curldirectly against the upstream and then through Nginx.
Step-by-step solution
1. Is Gunicorn running?
systemctl status myapp --no-pager
journalctl -u myapp -n 50 --no-pager
Common outcomes:
active (running): the process is up; move to step 2 to check where it listens.failedwith a Python traceback: the application does not start. Fix that first; typical causes are a missing environment variable, an import error, or an unreachable database.activatingand restarting repeatedly: Gunicorn crashes on boot; the journal shows why, andRestart=on-failurekeeps retrying.inactive (dead): the service is not enabled or was stopped.sudo systemctl enable --now myapp.
A brief 502 during a deploy restart is normal; a permanent one means the service is not coming back.
2. What is actually listening?
For a TCP bind:
sudo ss -ltnp | grep -E ':8000\b'
Expected:
LISTEN 0 2048 127.0.0.1:8000 0.0.0.0:* users:(("gunicorn",pid=2211,fd=5),...)
For a Unix socket bind:
sudo ss -xlp | grep gunicorn
ls -l /run/myapp/gunicorn.sock
If neither shows anything, Gunicorn is not listening at all: back to step 1. If it shows an address different from what Nginx uses, you have found the mismatch.
3. Compare both sides of the connection
systemctl cat myapp | grep -- --bind
sudo nginx -T 2>/dev/null | grep -E "proxy_pass|upstream" | head
The pairs must line up:
Gunicorn --bind | Nginx proxy_pass |
|---|---|
127.0.0.1:8000 | http://127.0.0.1:8000 |
unix:/run/myapp/gunicorn.sock | http://unix:/run/myapp/gunicorn.sock |
Frequent mismatches:
- Gunicorn moved from a port to a socket (or back) while Nginx kept the old target
- two Nginx server blocks, one still pointing at an old upstream
- a port typo (
8080versus8000) - an
upstreamblock defined but not used by thelocation
4. Wrong interface
Gunicorn bound to a specific external address does not accept connections to 127.0.0.1:
# Listening only on the private IP; Nginx connecting to 127.0.0.1 gets ECONNREFUSED
--bind 10.0.1.11:8000
For Nginx on the same host, bind to 127.0.0.1:8000 (or a Unix socket). Only bind to a routable address when another host must reach Gunicorn directly, and then firewall it.
Binding to 0.0.0.0:8000 works for local connections too, but exposes Gunicorn on every interface unless a firewall prevents it.
5. Stale socket file
If the bind path exists as a file but nothing listens on it, Nginx reports "Connection refused" rather than "No such file":
ls -l /run/myapp/gunicorn.sock
sudo ss -xlp | grep gunicorn.sock
A file with no listener is left over from a process that died without cleaning up. Gunicorn removes an existing socket file when it starts, so the durable fix is to let systemd own the directory:
[Service]
RuntimeDirectory=myapp
ExecStart=/srv/myapp/venv/bin/gunicorn config.wsgi:application --bind unix:/run/myapp/gunicorn.sock
/run is cleared on reboot, and RuntimeDirectory= recreates the directory on every start, so stale files never accumulate.
6. Firewall and SELinux
Local loopback traffic is rarely filtered, but check when the upstream is on another host:
sudo ufw status verbose | head
sudo iptables -S | grep -i 8000
On RHEL-family systems with SELinux enforcing, Nginx is not allowed to open arbitrary network connections by default:
sudo ausearch -m avc -ts recent | grep -i nginx | tail -3
sudo setsebool -P httpd_can_network_connect 1
The boolean is the standard fix for a reverse proxy configuration; keep SELinux enforcing.
7. Startup ordering after a reboot
If the site works until the server reboots, Nginx may start before Gunicorn, and the first requests fail. Nginx retries per request, so this is usually transient, but the service order should still be explicit:
[Unit]
Description=Gunicorn for myapp
After=network.target postgresql.service
Wants=network-online.target
Both services should be enabled:
systemctl is-enabled myapp nginx
If Gunicorn depends on a database or mount that is slow to appear, Restart=on-failure with RestartSec=2 lets it retry until the dependency is ready.
8. Verify the fix
Test the upstream directly, bypassing Nginx:
# Unix socket
curl -sI --unix-socket /run/myapp/gunicorn.sock http://localhost/ -H "Host: www.example.com" | head -1
# TCP
curl -sI http://127.0.0.1:8000/ -H "Host: www.example.com" | head -1
Then through Nginx:
curl -sI http://127.0.0.1/ -H "Host: www.example.com" | head -1
sudo tail -n 20 /var/log/nginx/error.log
Verification check:
- the direct request returns an HTTP status line (any status proves the connection works)
- the request through Nginx returns the same status
- no new
Connection refusedentries appear in the error log - after
sudo systemctl restart myapp && sudo systemctl restart nginx, both still work - after a reboot, the site comes up without manual steps
Explanation
Nginx is a client when it talks to Gunicorn. "Connection refused" is the same answer you would get from any TCP or Unix socket connection to an address where nothing is accepting: the kernel rejects it immediately. That makes the diagnosis a short chain: is the process alive, where is it listening, and does the proxy target that exact address? Fixing the mismatch in the service unit and the Nginx configuration, rather than by starting things manually, is what makes the fix survive restarts and reboots.
Edge cases / notes
Intermittent refusals under load
If refusals appear only during traffic spikes, Gunicorn's listen backlog may be full while all workers are busy. Increase --backlog modestly, add workers, or reduce request duration; the real fix is usually faster requests.
Deploy windows
Restarting Gunicorn drops connections for a moment. Use a graceful reload (systemctl reload with ExecReload=/bin/kill -s HUP $MAINPID) so the listening socket stays open while workers are replaced.
Containers
In Docker, 127.0.0.1 inside the Nginx container is the Nginx container itself. Use the application service name (proxy_pass http://web:8000;) on a shared network.
Several applications on one host
With multiple apps, each needs its own port or socket path. Reusing one port means only the first process binds and the rest fail to start, which looks like intermittent refusals.
Common mistakes
- Changing Gunicorn's bind without updating
proxy_pass(or the reverse). - Binding Gunicorn to a public address instead of loopback for a local proxy.
- Starting Gunicorn by hand after a failed unit, so the fix disappears at reboot.
- Assuming a
502is always Gunicorn being down, without reading the error number in the log.
Next steps
- If the error is
13: Permission deniedinstead, see Fix Gunicorn Socket Permission Denied. - For the wider set of causes, read Django 502 Bad Gateway: Causes and Fixes.
- If the service will not start at all, see Fix a Gunicorn systemd Service That Fails to Start.
- Review the reference setup in Deploy Django with Gunicorn and Nginx on Ubuntu.
FAQ
What does "connect() failed (111: Connection refused)" mean in Nginx?
Nginx reached the upstream address but nothing accepted the connection: Gunicorn is not running, or it is listening on a different port, interface, or socket path.
How do I check what Gunicorn is listening on?
Use sudo ss -ltnp | grep gunicorn for TCP ports and sudo ss -xlp | grep gunicorn for Unix sockets, then compare with proxy_pass in the Nginx configuration.
Should Gunicorn bind to a port or a Unix socket?
Either works. A Unix socket avoids the network stack and cannot be reached from other hosts; a loopback TCP port is easier to test and works across containers.
Why does the error come back after every reboot?
The service is probably not enabled, or its socket directory was created manually under /run, which is cleared at boot. Enable the unit and use RuntimeDirectory=.