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
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.
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.
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 sees | Meaning | First thing to do |
|---|---|---|---|
connect() failed (111: Connection refused) while connecting to upstream ... upstream: "http://127.0.0.1:18081/" | 502 | Host reachable, nothing listening on that port | ss -ltnp on the host that owns the address |
connect() failed (111: Connection refused) ... upstream: "fastcgi://127.0.0.1:18083" | 502 | Same, for PHP-FPM or another FastCGI server | Check the pool's listen line |
[crit] connect() to unix:/run/php/php8.4-fpm.sock failed (2: No such file or directory) | 502 | The socket file isn't there: service down or path changed | ls -l the path; check the FPM version in it |
connect() to unix:/run/app.sock failed (13: Permission denied) | 502 | Socket exists; nginx's user may not open it, or SELinux blocks the connection | Socket 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/" | 504 | Packets go out and nothing answers: firewall, security group, wrong network | Firewall and routing between nginx and the app |
connect() failed (113: No route to host) / (113: Host is unreachable) | 502 | A 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 upstream | 502 | Every server in a named upstream group is marked failed for fail_timeout | Look a few lines up for the 111s that caused it |
[warn] upstream server temporarily disabled while connecting to upstream ... upstream: "http://[::1]:18082/" | Usually 200 | One address of a multi-address name failed; nginx retried another | The IPv6 section below |
[emerg] host not found in upstream "api" in /etc/nginx/conf.d/default.conf | nginx won't start | A 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:
- 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. - The port is there, but on a different number.
LISTEN 0 511 127.0.0.1:3001when nginx says 3000: the app's config orPORTvariable changed. Fix one side to match the other. - 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 to0.0.0.0(or the specific interface), then protect it with a firewall rather than with the loopback address. - The port is there, bound to
[::]or0.0.0.0, and nginx still fails. Now suspect the address nginx resolved: the IPv6 section, or a container network, below. - The port is there and
curlworks 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 layout | Write in proxy_pass | Your app must listen on | Why you got 111 |
|---|---|---|---|
| nginx and app on the same host | http://127.0.0.1:3000 | 127.0.0.1:3000 is enough | App down or wrong port; or you wrote localhost and hit [::1] |
| nginx in a container, app on the host | http://host.docker.internal:3000 (add extra_hosts: ["host.docker.internal:host-gateway"] on Linux) | 0.0.0.0:3000, or the bridge IP | You wrote localhost, which is the nginx container |
| Both in one Compose project | http://api:3000 (service name, container port) | 0.0.0.0:3000 inside its container | App 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 port | 0.0.0.0 inside; port published if you go via the host IP | Different networks; localhost in the proxy host field |
| nginx on one machine, app on another | http://10.0.1.20:3000 or a DNS name | The LAN interface or 0.0.0.0 | Loopback bind; or (110)/(113) from a firewall or security group |
| PHP-FPM, Gunicorn or uWSGI over a Unix socket | unix:/run/php/php8.4-fpm.sock | The 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;whereapiis the service indocker-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.0inside its container. Many frameworks default to127.0.0.1(Flask, Rails in development, Node'sapp.listen(3000, 'localhost')), which is unreachable from the nginx container even on the same network.ss -ltnpinside the app container shows which. - Same network. Two separate
docker compose upprojects 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_passwhen it starts. If the app container isn't up yet, nginx exits withhost 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_onfixes 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.11is 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, or0.0.0.0and::1), so both upstreams answer. - Define a named
upstreamwith the one address you mean, which also gives you a place forkeepalive.
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) haslisten = /run/php/php8.4-fpm.sockorlisten = 127.0.0.1:9000, pluslisten.owner,listen.groupandlisten.modefor a socket. - nginx's side:
fastcgi_pass unix:/run/php/php8.4-fpm.sock;orfastcgi_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.sockshows the owner and mode; nginx's worker user (www-dataon Debian-family,nginxon RHEL-family) needs write permission. Fix it in the app that creates the socket (FPM'slisten.owner/listen.group, Gunicorn's--umask, uWSGI'schmod-socket), not with achmod 777in 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_passfails with 13 even thoughcurlas root works.setsebool -P httpd_can_network_connect 1allows it (the-Pmakes it survive reboots);ausearch -m avc -ts recentshows the denial if you want proof first. For a Unix socket under SELinux, the socket file needs a context nginx may connect to, whichsemanage fcontextandrestoreconset.
(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
- Copy the
upstream:value from your log line. Everything below is about that address. - From the nginx host (or inside the nginx container):
curl -vthat address. Refused = go to step 3; works = the problem is between nginx's user and the socket (step 7); hangs = firewall (step 8). - 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.1while nginx is elsewhere = bind0.0.0.0. - In Compose: service name, container port, same network, app bound to
0.0.0.0. - If the log shows
[::1]: write127.0.0.1inproxy_pass. - If the log shows
unix:and (2): the socket path or the FPM version changed;ls -lthe path. - If (13): socket owner and mode, then SELinux boolean on RHEL-family.
- If (110) or (113): security group, host firewall, routing, stale IP.
- After any change to nginx config:
nginx -t, thensystemctl reload nginx; nginx doesn't re-readproxy_passnames without a reload. - Prove it to yourself:
tail -f /var/log/nginx/error.logwhile 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.
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.