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 errorMeaningWhere to look
(111: Connection refused)nothing is listening at that addressGunicorn status and bind address
(2: No such file or directory)the Unix socket path does not existGunicorn bind path, RuntimeDirectory
(13: Permission denied)the socket exists but Nginx may not use itsocket ownership and mode
upstream timed outGunicorn accepted but did not respond in timeslow 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.

Nginx connecting to GunicornNginxproxy_pass targetAddresssocket or portGunicorn bindmust matchConnectionaccepted or refusedConnection refused means nothing is listening at the address Nginx was given.

Quick answer

  1. Check the service: systemctl status myapp and journalctl -u myapp -n 50.
  2. Check what is listening: sudo ss -ltnp | grep 8000 for TCP, sudo ss -xlp | grep gunicorn for Unix sockets.
  3. Compare Gunicorn's --bind with Nginx's proxy_pass; they must match exactly.
  4. If Gunicorn is not running, read the journal for the startup error and fix that.
  5. For TCP binds, make sure Gunicorn listens on 127.0.0.1 (not only on a different interface) and no firewall blocks local traffic.
  6. Restart Gunicorn, reload Nginx, and verify with curl directly 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.
  • failed with 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.
  • activating and restarting repeatedly: Gunicorn crashes on boot; the journal shows why, and Restart=on-failure keeps 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
both sides must name the same placeNginx proxy_passGunicorn --bindUnix sockethttp://unix:/run/gunicorn.sockunix:/run/gunicorn.sockTCP on loopbackhttp://127.0.0.1:8000127.0.0.1:8000Mismatch example✕http://localhost:8000!127.0.0.1:8000localhost can resolve to ::1, which Gunicorn is not listening ondjango-deployment.com
Copy the exact string from one side to the other.

The pairs must line up:

Gunicorn --bindNginx proxy_pass
127.0.0.1:8000http://127.0.0.1:8000
unix:/run/myapp/gunicorn.sockhttp://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 (8080 versus 8000)
  • an upstream block defined but not used by the location

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
listening, but not where Nginx looksWrong interface✕bound to 127.0.0.1 in a container✕Nginx in another container✕or Nginx tries ::1 firstRight interface✓0.0.0.0:8000 inside the container✓Nginx uses the service name✓127.0.0.1 on a single hostInside a container, 127.0.0.1 is only that containerdjango-deployment.com
0.0.0.0 inside a container is safe when the port is not published.

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
Nginx was ready firstServer bootsunits start in parallelNginx upaccepts requestsGunicorn stillstartingor failed: env missingRequests get 502connection refusedFixenable the unit, socket activationA unit that was started but never enabled does not come backdjango-deployment.com
systemctl is-enabled gunicorn catches the most common reboot surprise.

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 refused entries 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 502 is always Gunicorn being down, without reading the error number in the log.

Next steps

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=.

Keep going