nginx "connect() failed (111: Connection refused) while connecting to upstream": Read the Upstream Field, Fix the Port or Bind, Docker Compose Service Names, PHP-FPM Sockets, and the localhost [::1] Trap

Logeshwaran
—

connect() failed (111: Connection refused) while connecting to upstream is nginx telling you that it reached the address in your proxy_pass (or fastcgi_pass) and nothing was listening there: the TCP connection was answered with a reset, which Linux reports as errno 111. The browser sees 502 Bad Gateway; the truth is in nginx's error log, in the upstream: field at the end of the line, which names the exact host and port nginx tried. Fix it by making that exact address answer: start the app, bind it to the address nginx uses (not only 127.0.0.1 when nginx is in another container), and match the port. Here is the surprise we hit while reproducing it: proxy_pass http://localhost:3000 creates two upstreams, 127.0.0.1 and [::1], so an app bound only to IPv4 makes nginx fail on [::1], log a 111, mark it "temporarily disabled," and silently retry the other address. Your site works and your error log fills with connection refused. This page shows every variant of the message from a live nginx 1.30, what each means, and the fix for bare metal, Docker Compose, PHP-FPM, Elastic Beanstalk and the IPv6 trap.

⚡ Quick Answer

• Read the line: the upstream: "http://127.0.0.1:3000/" part is the address that refused. Test it from the nginx host: curl -v http://127.0.0.1:3000/. How to read it.

• Nothing listening: the app crashed, isn't started, or listens on another port. ss -ltnp shows what's really bound. Fix.

• Docker Compose: localhost inside the nginx container is the nginx container. Use the service name, and bind the app to 0.0.0.0. Fix.

• Works but the log is full of 111s: localhost resolved to [::1] too. Write 127.0.0.1 in proxy_pass. The IPv6 trap.

• Different numbers: (2) missing Unix socket, (13) SELinux, (110) timeout = 504, (113) no route. The table.

PHP site? The same line reads upstream: "fastcgi://..." and the fix is PHP-FPM's listen address. PHP-FPM.

Jake runs a phone repair shop whose booking site is a small Node app behind nginx on a cheap VPS. After a reboot the site showed 502 Bad Gateway, the nginx log said connect() failed (111: Connection refused) while connecting to upstream, and Jake restarted nginx four times because the error had "nginx" in it. His friend Ethan, a developer, asked him to read the last thing on the line, upstream: "http://127.0.0.1:3000/", and then run curl 127.0.0.1:3000. Connection refused. The Node app hadn't come back after the reboot; nginx was the messenger. This page is that lesson with every variant we could reproduce, on nginx 1.30.4 with nothing but a config file and a few closed ports. New to how a reverse proxy sits in front of an app? Our Node on Amazon Linux guide builds exactly that setup from zero.

🧭 NEW HERE? READ THESE FIRST

New to reverse proxies, Docker networking or gateway errors? These five pages make the rest of this one easy:

📌 Bookmark this; the error table and the Docker section are the parts you'll come back for.

How to read the error line: the upstream field is the whole diagnosis

A full line from the log looks like this (we trimmed the timestamp and process IDs):

[error] connect() failed (111: Connection refused) while connecting to upstream,
        client: 203.0.113.9, server: shop.example, request: "GET /api/bookings HTTP/1.1",
        upstream: "http://127.0.0.1:3000/api/bookings", host: "shop.example"

Read it right to left; you'll find your answer at the end of the line. upstream: is the address nginx actually tried, after resolving whatever name you wrote in proxy_pass; it's the only address that matters for the next five minutes. request: tells you which location block sent it there, useful when one path fails and the rest of the site works. client: is who asked. server: is the server_name that matched (blank in a default server). The number in brackets is the operating system's error code from the connect() call, and each number is a different problem:

Log line (reproduced)Browser seesMeaningFirst thing to do
connect() failed (111: Connection refused) while connecting to upstream ... upstream: "http://127.0.0.1:18081/"502Host reachable, nothing listening on that portss -ltnp on the host that owns the address
connect() failed (111: Connection refused) ... upstream: "fastcgi://127.0.0.1:18083"502Same, for PHP-FPM or another FastCGI serverCheck the pool's listen line
[crit] connect() to unix:/run/php/php8.4-fpm.sock failed (2: No such file or directory)502The socket file isn't there: service down or path changedls -l the path; check the FPM version in it
connect() to unix:/run/app.sock failed (13: Permission denied)502Socket exists; nginx's user may not open it, or SELinux blocks the connectionSocket owner/mode; on RHEL-family, the SELinux boolean
upstream timed out (110: Connection timed out) while connecting to upstream ... upstream: "http://10.255.255.1:80/"504Packets go out and nothing answers: firewall, security group, wrong networkFirewall and routing between nginx and the app
connect() failed (113: No route to host) / (113: Host is unreachable)502A router or host firewall rejected it outright (ICMP unreachable)Same as 110, but you'll find a REJECT rule
no live upstreams while connecting to upstream502Every server in a named upstream group is marked failed for fail_timeoutLook a few lines up for the 111s that caused it
[warn] upstream server temporarily disabled while connecting to upstream ... upstream: "http://[::1]:18082/"Usually 200One address of a multi-address name failed; nginx retried anotherThe IPv6 section below
[emerg] host not found in upstream "api" in /etc/nginx/conf.d/default.confnginx won't startA name in proxy_pass didn't resolve at startup (Compose service not up yet)depends_on, or a variable with a resolver

Only the first two rows are "connection refused." If your number is different, jump to the matching section; if yours is 111, the next section is your fix in most cases.

Jake: "So 'connection refused' doesn't mean refused by a password or a rule. It means nobody picked up."

Ethan: "Right. The phone rang at the right number and nobody was home. Everything else on this page is about finding out why nobody was home."

Nothing is listening: find out what the app really bound

Run these on the machine (or inside the container) that should be running your app, not on the nginx side, and compare what you see with the upstream: address from your log:

ss -ltnp                        # every listening TCP socket, with the process that owns it
curl -v http://127.0.0.1:3000/  # the exact address nginx used; "Connection refused" here = nginx is right
systemctl status myapp          # or: docker compose ps / pm2 ls / journalctl -u myapp -n 50

What ss shows you decides your fix:

  1. The port isn't in the list. The app isn't running. After a reboot that usually means it was never enabled as a service: systemctl enable --now myapp, or a process manager that starts at boot. Read the app's own log for why it exited; a crash on startup (missing environment variable, database not up yet) is the usual story, and restarting nginx can't help.
  2. The port is there, but on a different number. LISTEN 0 511 127.0.0.1:3001 when nginx says 3000: the app's config or PORT variable changed. Fix one side to match the other.
  3. The port is there, bound to 127.0.0.1, and nginx is on another machine or in another container. A loopback bind is only reachable from the same network namespace. Bind the app to 0.0.0.0 (or the specific interface), then protect it with a firewall rather than with the loopback address.
  4. The port is there, bound to [::] or 0.0.0.0, and nginx still fails. Now suspect the address nginx resolved: the IPv6 section, or a container network, below.
  5. The port is there and curl works from the host, but nginx fails. nginx runs as a different user, possibly under SELinux or AppArmor, or in a container with its own network. See permission denied and Docker.

Where nginx and your app run decides the only two things you control: the address you write in proxy_pass and the address your app must bind to.

Your layoutWrite in proxy_passYour app must listen onWhy you got 111
nginx and app on the same hosthttp://127.0.0.1:3000127.0.0.1:3000 is enoughApp down or wrong port; or you wrote localhost and hit [::1]
nginx in a container, app on the hosthttp://host.docker.internal:3000 (add extra_hosts: ["host.docker.internal:host-gateway"] on Linux)0.0.0.0:3000, or the bridge IPYou wrote localhost, which is the nginx container
Both in one Compose projecthttp://api:3000 (service name, container port)0.0.0.0:3000 inside its containerApp bound to 127.0.0.1, or published port used instead of container port
Two Compose projects, or Nginx Proxy Manager in its own stack (Nextcloud, Frigate, Jellyfin...)Container name on a shared external network, or the host's LAN IP plus the published port0.0.0.0 inside; port published if you go via the host IPDifferent networks; localhost in the proxy host field
nginx on one machine, app on anotherhttp://10.0.1.20:3000 or a DNS nameThe LAN interface or 0.0.0.0Loopback bind; or (110)/(113) from a firewall or security group
PHP-FPM, Gunicorn or uWSGI over a Unix socketunix:/run/php/php8.4-fpm.sockThe same path, owned so nginx's user may write(2) path changed; (13) permissions

A reader whose Django site returned 502 had upstream django { server 0.0.0.0:8000; } in the nginx config. 0.0.0.0 is a valid listen address and a meaningless connect address; on Linux it's treated as "this host," which happens to work on bare metal and fails the moment nginx moves into a container. Write the real address: 127.0.0.1:8000 on the same host, the service name in Compose.

Docker and Docker Compose: localhost is the wrong container

If you're running nginx in a container, this is probably your section. It is the largest single source of the error, and it has one cause: inside the nginx container, localhost and 127.0.0.1 mean the nginx container, where your app isn't. A reader who had nginx and Node working on the host moved both into containers with the same config and got nothing but 111s; the config hadn't changed, the meaning of localhost had.

Your rules in Compose:

  • Use the service name as the host. proxy_pass http://api:3000; where api is the service in docker-compose.yml. Compose's internal DNS resolves it to the container's IP on the shared network.
  • Use the container's port, not the published port. ports: ["8080:3000"] publishes 3000 as 8080 on the host; between containers it's still 3000. Nothing needs to be published at all for nginx to reach it.
  • Bind the app to 0.0.0.0 inside its container. Many frameworks default to 127.0.0.1 (Flask, Rails in development, Node's app.listen(3000, 'localhost')), which is unreachable from the nginx container even on the same network. ss -ltnp inside the app container shows which.
  • Same network. Two separate docker compose up projects get two networks; a reverse proxy in one can't see an app in the other unless you attach it to the same named external network.
  • Startup order. nginx resolves the names in proxy_pass when it starts. If the app container isn't up yet, nginx exits with host not found in upstream; if the app restarts and gets a new IP, nginx keeps the old one and logs 111 or timeouts until it reloads. depends_on fixes the first; for the second, use a variable so nginx resolves at request time: resolver 127.0.0.11 valid=10s; set $api http://api:3000; proxy_pass $api; (127.0.0.11 is Docker's embedded DNS).

A reader with two identical projects, one working and one failing, had changed only the service and container names; the nginx config in project B still said project A's service name, which resolved to nothing, and the failing log line pointed at a stale IP from the other project's network. The upstream: field showed it. Our containers explainer covers why each container has its own localhost.

Jake: "So inside the nginx box, 'localhost' is the nginx box. I was telling it to call itself."

Ethan: "And it did, and nobody answered, which is exactly what the log said."

The IPv6 trap: proxy_pass http://localhost creates two upstreams

We set up an app listening on 127.0.0.1:18082 only, pointed nginx at http://localhost:18082/, and sent six requests. All six returned 200. The error log told a different story:

[error] connect() failed (111: Connection refused) while connecting to upstream, request: "GET /localhost/ HTTP/1.1", upstream: "http://[::1]:18082/"
[warn]  upstream server temporarily disabled while connecting to upstream, request: "GET /localhost/ HTTP/1.1", upstream: "http://[::1]:18082/"

On this machine, as on most modern Linux installs, /etc/hosts maps localhost to both ::1 and 127.0.0.1. nginx resolves the name once at startup, and a name with several addresses becomes a round-robin group, so the first request went to [::1], was refused, and was passed to the next address by proxy_next_upstream (whose default, error timeout, covers exactly this). The client saw 200; nginx marked [::1] as failed for fail_timeout (10 seconds by default), used 127.0.0.1 for the next requests, then tried [::1] again. The pattern on a busy site: a 111 every ten seconds or so in the log, no visible outage, and an on-call engineer who can't find anything wrong. The fix is one of:

  • Write the IP in proxy_pass: http://127.0.0.1:3000. Simplest and what most people want.
  • Bind the app to both families (:: with dual-stack, or 0.0.0.0 and ::1), so both upstreams answer.
  • Define a named upstream with the one address you mean, which also gives you a place for keepalive.

The same mechanism explains "it fails only sometimes" for any hostname with several A or AAAA records, and it's why a retry that hides the first failure isn't free: a POST that was passed to the next server after nothing was sent is fine, but the retry only happens if nothing has been sent to the client yet, so a large upload that fails on the first address returns the error.

PHP-FPM: fastcgi:// in the upstream field

If you run PHP, the same line in your log reads upstream: "fastcgi://127.0.0.1:9000" or connect() to unix:/run/php/php8.4-fpm.sock failed (2: No such file or directory). The two ends have to agree on the address in two files:

  • FPM's side: /etc/php/8.4/fpm/pool.d/www.conf (Debian, Ubuntu, Kali) or /etc/php-fpm.d/www.conf (RHEL family) has listen = /run/php/php8.4-fpm.sock or listen = 127.0.0.1:9000, plus listen.owner, listen.group and listen.mode for a socket.
  • nginx's side: fastcgi_pass unix:/run/php/php8.4-fpm.sock; or fastcgi_pass 127.0.0.1:9000;, matching the first.

The classic failure is a PHP upgrade: a reader who upgraded Ubuntu found nginx still pointing at php5-fpm.sock while the new package listened on php7.0-fpm.sock, hence "(2: No such file or directory)." On Docker, FPM in its own container listens on TCP 9000 and must be reached by service name, and the SCRIPT_FILENAME path must exist inside the FPM container, which is the next error after this one is fixed. A reader on Compose saw upstream: "fastcgi://172.2x.x.x:9000": the IP resolved, the PHP container's FPM wasn't listening yet or had exited; docker compose logs php had the reason.

(13: Permission denied): the socket is there and nginx may not open it

When your number is 13, the connection wasn't refused by a missing listener; the kernel refused nginx itself. You have two possible causes:

  • Unix socket permissions. ls -l /run/app.sock shows the owner and mode; nginx's worker user (www-data on Debian-family, nginx on RHEL-family) needs write permission. Fix it in the app that creates the socket (FPM's listen.owner/listen.group, Gunicorn's --umask, uWSGI's chmod-socket), not with a chmod 777 in a startup script.
  • SELinux on RHEL, Rocky, Alma, Amazon Linux, Fedora. By default the web server context may not open outbound network connections, so a TCP proxy_pass fails with 13 even though curl as root works. setsebool -P httpd_can_network_connect 1 allows it (the -P makes it survive reboots); ausearch -m avc -ts recent shows the denial if you want proof first. For a Unix socket under SELinux, the socket file needs a context nginx may connect to, which semanage fcontext and restorecon set.

(110) and (113): the address is right and the network is in the way

If you see a timeout (upstream timed out (110: Connection timed out) while connecting to upstream, which the browser sees as 504) means the SYN went out and nothing came back for proxy_connect_timeout (60 seconds by default, which is why these pages hang). (113: No route to host) means something answered with an ICMP rejection. Both tell you the app's address is wrong or walled off, not that your app is down:

  • On AWS, the security group of the app instance must allow the app port from nginx's security group or subnet; a group that allows only 80 and 443 from the internet produces exactly this when nginx is on another instance. Our ALB 502 vs 504 guide covers the same distinction one layer up.
  • A host firewall (ufw, firewalld, iptables) on the app machine with a REJECT rule gives 113, with DROP gives 110.
  • A reader whose home setup broke after a router swap had the app's LAN IP change with the new DHCP server; nginx still pointed at the old address, which now belonged to nobody. Static leases or a hostname fix it.
  • A wrong subnet or VPN route gives 110 with nothing in any log on the app side, because the packets never arrive.

Elastic Beanstalk, Node and Gunicorn: the port nginx expects

If your platform ships nginx for you, it hides the config but not the rule. On Elastic Beanstalk the bundled nginx proxies to 127.0.0.1:8080 (Node, Docker platforms) or 127.0.0.1:8000 (Python with Gunicorn), and the first deployment of an app that listens on 3000 produces this exact log line in /var/log/nginx/error.log on the instance. Listen on process.env.PORT, set the PORT environment property, or change the platform's proxy port; our Beanstalk 502 guide walks through each platform. Gunicorn behind nginx on a plain server has the matching trap in the other direction: gunicorn app:app binds 127.0.0.1:8000 by default, which is right for nginx on the same host and wrong for nginx in a container.

"no live upstreams" and the single-server exception

When you use a named upstream block with several servers, each failure counts against max_fails (default 1) within fail_timeout (default 10 seconds), and once every server is marked down you get no live upstreams while connecting to upstream instead of a fresh 111 for each request. The 111s that caused it are a few lines earlier in the log. With a single server in the group, max_fails and fail_timeout are ignored and the server is never marked unavailable, which is why our one-server pool kept logging 111 on every request instead of switching to the "no live upstreams" message.

The ten-minute checklist, in order

  1. Copy the upstream: value from your log line. Everything below is about that address.
  2. From the nginx host (or inside the nginx container): curl -v that address. Refused = go to step 3; works = the problem is between nginx's user and the socket (step 7); hangs = firewall (step 8).
  3. On the app host or container: ss -ltnp. Not listed = start the app and read its log; wrong port = fix the port; 127.0.0.1 while nginx is elsewhere = bind 0.0.0.0.
  4. In Compose: service name, container port, same network, app bound to 0.0.0.0.
  5. If the log shows [::1]: write 127.0.0.1 in proxy_pass.
  6. If the log shows unix: and (2): the socket path or the FPM version changed; ls -l the path.
  7. If (13): socket owner and mode, then SELinux boolean on RHEL-family.
  8. If (110) or (113): security group, host firewall, routing, stale IP.
  9. After any change to nginx config: nginx -t, then systemctl reload nginx; nginx doesn't re-read proxy_pass names without a reload.
  10. Prove it to yourself: tail -f /var/log/nginx/error.log while you hit the page; a clean tail is your only pass.

nginx connect() failed (111: Connection refused): frequently asked questions

What does "connect() failed (111: Connection refused) while connecting to upstream" mean?

nginx opened a TCP connection to the address in proxy_pass or fastcgi_pass and the operating system answered that nothing is listening there (errno 111). The app behind nginx is down, on another port, or bound to an address nginx can't reach. The browser gets 502 Bad Gateway; the upstream: field in the log names the exact address.

How do I fix nginx 502 Bad Gateway connection refused?

Read the upstream: address in /var/log/nginx/error.log, test it with curl from the nginx host, and run ss -ltnp where the app should be. Start the app if it isn't running, match the port, bind it to 0.0.0.0 if nginx is in another container or machine, and use 127.0.0.1 instead of localhost in proxy_pass to avoid the [::1] retry.

Why does nginx say connection refused when my app is running?

Because it's running somewhere nginx isn't looking: bound to 127.0.0.1 inside a different container, on another port, or reachable by IPv4 while nginx resolved the name to [::1]. Compare ss -ltnp on the app side with the upstream: field in the log, including the IP family.

Why does nginx log connection refused for [::1] when the site works?

proxy_pass http://localhost resolves to both ::1 and 127.0.0.1, so nginx round-robins between them; the IPv6 one is refused, logged, marked temporarily disabled, and the request is retried on 127.0.0.1. Write 127.0.0.1 in proxy_pass or make the app listen on both families.

How do I fix connection refused with nginx in Docker Compose?

Use the service name as the host in proxy_pass, the container's internal port (not the published one), put both services on the same network, bind the app to 0.0.0.0 inside its container, and add depends_on so nginx starts after the app. localhost inside the nginx container is the nginx container.

What is the difference between 502 and 504 from nginx?

502 means nginx got a definite failure from the upstream side: connection refused, a reset, an invalid response, a missing socket. 504 means nginx waited for proxy_connect_timeout or proxy_read_timeout and heard nothing, which points at a firewall, routing or a hung app rather than a stopped one.

Why do I get "no live upstreams while connecting to upstream"?

Every server in a named upstream block has failed max_fails times within fail_timeout and is marked unavailable, so nginx stops trying for the rest of the window. The earlier 111 or 110 lines show the real cause. A single-server group never shows this message because max_fails is ignored for it.

What does "connect() to unix:/run/php/php-fpm.sock failed (2: No such file or directory)" mean?

nginx is pointing at a Unix socket that doesn't exist: PHP-FPM is stopped, or its listen path changed, typically after a PHP version upgrade renamed the socket. Check the pool's listen line and make fastcgi_pass match it.

What does "(13: Permission denied) while connecting to upstream" mean?

The socket or port exists but nginx's worker user isn't allowed to connect: the Unix socket's owner or mode excludes www-data or nginx, or SELinux on a RHEL-family system blocks the web server from making network connections. Fix the socket ownership in the app's config, or setsebool -P httpd_can_network_connect 1.

Why did nginx stop working after a reboot?

Usually the app wasn't enabled as a service and didn't come back, so nginx (which did) refuses on its behalf. systemctl enable your app, or configure the process manager to start at boot. Restarting nginx changes nothing because nginx isn't the part that's down.

Why does only one location give 502 while the rest of the site works?

That location proxies to a different upstream or port; the request: field in the log tells you which. A reader whose Gunicorn site worked except /static had a proxy_pass in the static location pointing at a port with nothing on it; serving static files with root or alias instead of proxying fixed it.

Should I use 0.0.0.0 in proxy_pass?

No. 0.0.0.0 is a listen address, meaning "all interfaces," and has no meaning as a destination; it happens to behave like localhost on Linux and breaks in containers. Use 127.0.0.1, the real IP, or a service name.

How do I see which address nginx actually connects to?

The upstream: field at the end of the error line, or $upstream_addr in a custom log_format for successful requests. Both show the resolved address, including [::1] for IPv6, which the proxy_pass text alone hides.

Does nginx re-resolve hostnames in proxy_pass?

Not by default: names are resolved when nginx starts or reloads and cached for its lifetime, so a container or instance that changes IP leaves nginx talking to the old one. Use a variable in proxy_pass with a resolver directive (127.0.0.11 inside Docker), or the resolve parameter on an upstream server with a shared zone, to re-resolve at runtime.

How do I fix connection refused on Elastic Beanstalk?

Make the app listen on the port the platform's nginx proxies to: 8080 for Node and Docker platforms, 8000 for Python, read from the PORT environment property. The instance's /var/log/nginx/error.log shows the same connect() failed line with the expected port in the upstream: field.

Why does curl work but nginx gets connection refused?

curl ran from a different place or as a different user: from the host instead of the nginx container, over IPv4 while nginx tried [::1], or as root where SELinux doesn't confine it. Run curl inside the nginx container (docker compose exec nginx curl ...) against the exact upstream: address.

πŸ“š ALSO READ

Where to go next, from the AWS gateway errors one layer up to the Docker setups that put nginx in a container:

📌 Bookmark this; the error table was reproduced on nginx 1.30.4 on October 24, 2026.

Jake's VPS now has the Node app enabled as a systemd unit, 127.0.0.1:3000 instead of localhost in the nginx config, and a line in his notes that reads "502 = look at the app first." The shop's new Compose setup took one more afternoon, because the first version proxied to localhost again, from inside a container this time, and Ethan made him find it in the upstream: field himself before saying a word. He wrote the line below on the back of a repair ticket.

📌 If you keep one line from this page

Read the upstream: address at the end of the log line, curl it from where nginx runs, and fix whatever isn't listening there; nginx is the messenger, not the problem.

111 = nobody listening; 2 = socket missing; 13 = permission; 110/113 = firewall or route; [::1] = write 127.0.0.1.

Revision note. Written October 24, 2026, for everyone who restarted nginx four times before reading the end of the line. "Don't shoot the messenger," says the old proverb; read the address it brought you.

Related