A 502 Bad Gateway means the proxy in front of your app (Caddy, nginx, Traefik, or the proxy Coolify runs) forwarded the request and got no valid answer. The app may be stopped, listening on the wrong address or port, on another Docker network, or too slow; restarting things at random hides which. This wizard reads the proxy's own error first, then checks the path to the container one link at a time.
Answer one question at a time. Each step shows a command to run on your server and what its output means. The checks only read: they look into the container's network from the host with nsenter, without entering the container. Every command that changes something (a recreate, a reload) is marked before you run it. The page never runs anything and sends nothing anywhere; your answers are kept in the page's link, never your output.
Find the cause, one step at a time
Which reverse proxy answers with 502?
A 502 Bad Gateway is not an error of your app: it is the proxy saying that it tried to forward the request and got no valid answer from the app behind it. The page itself (its style, its wording) tells you which proxy answered, and each proxy writes the reason in its own log.
Coolify runs its own proxy container, coolify-proxy: Traefik by default, or Caddy. If you run another proxy, pick the one whose configuration language you use.
Every step as text
The same tree as the wizard above, all branches at once, for reading or printing.
Show every question, check and solution of the wizard
Which reverse proxy answers with 502?
A 502 Bad Gateway is not an error of your app: it is the proxy saying that it tried to forward the request and got no valid answer from the app behind it. The page itself (its style, its wording) tells you which proxy answered, and each proxy writes the reason in its own log.
Coolify runs its own proxy container, coolify-proxy: Traefik by default, or Caddy. If you run another proxy, pick the one whose configuration language you use.
Answers
- Caddy: go to “What does Caddy's log say?”
- nginx: go to “What does nginx's error log say?”
- Traefik: go to “What does Traefik's log say?”
- Coolify (its own proxy): go to “What does Coolify's proxy log say?”
What does Caddy's log say?
Caddy logs every failed proxy attempt as an error with the address it dialed: dial tcp 172.18.0.5:3000: connect: connection refused, dial tcp: lookup app on 127.0.0.11:53: no such host, or context deadline exceeded. Replace <proxy> with the name of Caddy's container, and request the site once before you run the command so a fresh line is there.
If Caddy runs on the host (the package and its systemd service) and not in a container, read journalctl -u caddy --since "15 min ago" instead.
docker logs --since 15m <proxy> 2>&1 | grep -iE 'dial|reverse_proxy' | tail -n 20Answers
- It could not connect: connection refused, no such host, a dial timeout, or no error line at all: go to “Is the app's container running?”
- It connected, then timed out waiting for the response: go to “The app answers slower than the proxy waits”
- The app closed the connection: EOF, connection reset, or upstream prematurely closed: go to “The app drops connections: it crashes or is killed mid-request”
What does nginx's error log say?
nginx writes the reason of a 502 to its error log, with the word upstream in it: connect() failed (111: Connection refused) while connecting to upstream, host not found in upstream, upstream prematurely closed connection while reading response header from upstream, or upstream sent too big header while reading response header from upstream. The official nginx image sends the error log to the container's output.
If nginx runs on the host and not in a container, read sudo tail -n 50 /var/log/nginx/error.log instead.
docker logs --since 15m <proxy> 2>&1 | grep -i upstream | tail -n 20Answers
- It could not connect: connection refused, no such host, a dial timeout, or no error line at all: go to “Is the app's container running?”
- It connected, then timed out waiting for the response: go to “The app answers slower than the proxy waits”
- The app closed the connection: EOF, connection reset, or upstream prematurely closed: go to “The app drops connections: it crashes or is killed mid-request”
- upstream sent too big header: go to “The app's response headers are larger than nginx's buffer”
What does Traefik's log say?
Traefik answers 502 when it cannot reach the server of a service, and names the cause in its log, such as dial tcp 172.18.0.5:3000: connect: connection refused or EOF. At the default log level some of these lines are not written; if you see nothing, look at the access log or raise the level to DEBUG for a moment.
A 404 from Traefik is a different problem: no router matched the domain. This wizard is about the 502.
docker logs --since 15m <proxy> 2>&1 | grep -iE '502|dial|eof' | tail -n 20Answers
- It could not connect: connection refused, no such host, a dial timeout, or no error line at all: go to “Is the app's container running?”
- It connected, then timed out waiting for the response: go to “The app answers slower than the proxy waits”
- The app closed the connection: EOF, connection reset, or upstream prematurely closed: go to “The app drops connections: it crashes or is killed mid-request”
What does Coolify's proxy log say?
Coolify runs one proxy container on each server, coolify-proxy, and forwards every domain over a Docker network to the container of the resource. Its log is also under Servers → your server → Proxy → Logs in the dashboard. Traefik writes errors such as dial tcp …: connect: connection refused; Caddy writes dial tcp … lines the same way.
In the next steps, <proxy> is coolify-proxy, and <container> is the container of your app: Coolify adds a generated suffix to its name, so copy it from docker ps.
docker logs --since 15m coolify-proxy 2>&1 | tail -n 50Answers
- It could not connect: connection refused, no such host, a dial timeout, or no error line at all: go to “Is the app's container running?”
- It connected, then timed out waiting for the response: go to “The app answers slower than the proxy waits”
- The app closed the connection: EOF, connection reset, or upstream prematurely closed: go to “The app drops connections: it crashes or is killed mid-request”
Is the app's container running?
A stopped container has no address and no name on the network, so the proxy gets connection refused or no such host. Look at the STATUS of your app's container: Up 3 hours, Up 2 minutes (healthy), Up 10 seconds (health: starting), Up (unhealthy), Restarting (1) 5 seconds ago or Exited (137).
docker ps -a --format 'table {{.Names}}\t{{.Status}}'Answers
- Up, healthy or without a healthcheck: go to “On which address and port does the app listen inside its container?”
- Up, but health: starting or unhealthy: go to “The app is still starting or fails its healthcheck”
- Restarting, Exited, or not in the list: go to “The app's container is not running”
On which address and port does the app listen inside its container?
Each container has its own network namespace, with its own 127.0.0.1. An app that listens on 127.0.0.1 there is reachable only from inside that same container, never from the proxy, so the proxy gets connection refused. It has to listen on every address: 0.0.0.0, * or [::].
This command reads the sockets that listen in the container's namespace with the host's own ss, without entering the container or changing it (nsenter -n switches only the network namespace). Find the port your proxy targets: in Caddy reverse_proxy app:3000, in nginx proxy_pass http://app:3000, in Traefik the label traefik.http.services.<name>.loadbalancer.server.port, in Coolify the field Ports Exposes.
sudo nsenter -t "$(docker inspect -f '{{.State.Pid}}' <container>)" -n ss -ltnpAnswers
- The proxy's port, on 0.0.0.0, * or [::]: go to “Does the proxy run in a container or on the host?”
- The proxy's port, but only on 127.0.0.1 (or [::1]): go to “Is the app a Next.js server (output: standalone)?”
- It listens, but on a different port than the proxy targets: go to “The proxy targets a different port than the app listens on”
- Nothing listens (an empty list): go to “Nothing listens in the container”
Is the app a Next.js server (output: standalone)?
The fix is the same for every app, listening on 0.0.0.0, but the setting has a different name in each framework. Next.js in standalone output takes it from the HOSTNAME environment variable, and that variable has its own trap behind a proxy.
Answers
- Yes, Next.js standalone: go to “Next.js standalone listens on 127.0.0.1: set HOSTNAME=0.0.0.0”
- No, another app: go to “The app listens on 127.0.0.1 inside its container”
Does the proxy run in a container or on the host?
The app listens correctly, so the question is how the proxy reaches it. A proxy in a container (Coolify's always is) reaches the app over a shared Docker network, by name and the container's own port. A proxy installed on the host (an nginx or Caddy package) cannot resolve container names; it reaches the app through a port published on the host.
Answers
- In a container (including Coolify): go to “Do the proxy and the app share a Docker network?”
- On the host: go to “Is the app's port published on the host?”
Do the proxy and the app share a Docker network?
Containers reach each other only over a network they are both attached to. Compose puts each project on its own network (<project>_default), so a proxy from one Compose project does not see an app from another unless both join a common network. This command prints the networks of both containers, one line each.
docker inspect -f '{{.Name}}: {{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' <proxy> <container>Answers
- At least one network is on both lines: go to “Does the proxy use a name the network knows?”
- No network is on both lines: go to “The proxy and the app are on different networks”
Does the proxy use a name the network knows?
On a user-defined network Docker's DNS answers for the container's name and for its aliases; Compose adds the service name as an alias. This command prints, per network, the container's address and its aliases. Compare them with the host name in the proxy's configuration (app in reverse_proxy app:3000 or proxy_pass http://app:3000). Traefik and Coolify use the container's address directly; for them, check that the address is on the shared network.
Note the address on the shared network: the next step uses it as <ip>.
docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}: {{$v.IPAddress}} {{$v.Aliases}}{{println}}{{end}}' <container>Answers
- The proxy's host name is the container's name or one of its aliases on the shared network: go to “Does the app answer from where the proxy stands?”
- The proxy uses another name, or a fixed IP address: go to “The proxy uses a name or address the network does not know”
Does the app answer from where the proxy stands?
This sends one HTTP request to the app from the proxy's network namespace, with the host's own curl, to the address you noted and the container's port. It prints the status code and how long the answer took, and gives up after 90 seconds. Any status code, even 404 or 401, means the path works: the app answered.
sudo nsenter -t "$(docker inspect -f '{{.State.Pid}}' <proxy>)" -n curl -sS --max-time 90 -o /dev/null -w '%{http_code} %{time_total}s\n' http://<ip>:<port>/Answers
- A status code, within a few seconds: go to “When does the 502 appear?”
- A status code after a long time, or curl: (28) timeout: go to “The app answers slower than the proxy waits”
- Empty reply from server, or 400 about plain HTTP sent to an HTTPS port: go to “The app speaks HTTPS and the proxy speaks HTTP to it”
- Connection refused, or no route to host: go to “No match yet: what to collect”
Is the app's port published on the host?
A proxy on the host reaches a container through a published port: ports: ["127.0.0.1:3000:3000"] in Compose. Publishing on 127.0.0.1 of the host is right here, unlike listening on 127.0.0.1 inside the container: it keeps the port off the internet and the host's proxy still reaches it. This command lists the container's published ports as 3000/tcp -> 127.0.0.1:3000.
docker port <container>Answers
- The proxy's port is listed, on 127.0.0.1 or 0.0.0.0: go to “Does the app answer on the host's port?”
- Nothing is listed, or another port: go to “The host's proxy has no published port to reach”
Does the app answer on the host's port?
This is the request your proxy makes: from the host, to the published port. It prints the status code and how long the answer took, and gives up after 90 seconds. Any status code means the app answered.
curl -sS --max-time 90 -o /dev/null -w '%{http_code} %{time_total}s\n' http://127.0.0.1:<port>/Answers
- A status code, within a few seconds: go to “When does the 502 appear?”
- A status code after a long time, or curl: (28) timeout: go to “The app answers slower than the proxy waits”
- Empty reply from server, or 400 about plain HTTP sent to an HTTPS port: go to “The app speaks HTTPS and the proxy speaks HTTP to it”
- Connection refused or reset: go to “No match yet: what to collect”
When does the 502 appear?
The app answers on the path the proxy uses, so the request breaks in the proxy itself or only in some requests. When the 502 appears, and on which requests, tells these causes apart.
Answers
- On every request, although the app answered the test: go to “The proxy sends requests to another address than the one you tested”
- On live features only: WebSocket, live updates, a web terminal: go to “The proxy does not pass the WebSocket upgrade”
- On some pages only, often after login or with large cookies: go to “The app's response headers are larger than nginx's buffer”
- For a few seconds or minutes during each deploy: go to “The old container stops before the new one is ready”
- At random, more often under load: go to “The app drops connections: it crashes or is killed mid-request”
The app's container is not running
There is nothing for the proxy to connect to: a stopped container has no address on the network. A container that keeps restarting shows the real error in its own log; the proxy's 502 is only the consequence.
What to do
- Read why it stopped: the last lines before it exited.
docker logs --tail 50 <container> - If it is in a restart loop, follow its exit code in the restart loop wizard on this site before you start it again.
- Once the cause is fixed, start the service again.
docker compose up -d <service> - Request the site again through the proxy; the response code is the first number.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
The app is still starting or fails its healthcheck
The process runs but does not answer yet, or answers with errors. Some proxies stop sending traffic to a container that is not healthy, and the app's port may not be open until its startup finishes (migrations, a cache warm-up, a model loading).
What to do
- Read the last healthcheck results: the command's output and exit code tell what fails.
docker inspect -f '{{json .State.Health}}' <container> - If the app is only slow to start, give it time: raise
start_period(andstart_interval) of the healthcheck instead ofretries, so a slow start is not counted as a failure. - If the check itself is wrong (a path that does not exist, a tool the image lacks such as
curl), fix thetestso it checks the port the proxy uses. - Recreate the container so the change takes effect (Compose replaces the container; the data in its volumes stays).
docker compose up -d --force-recreate <service>
The app listens on 127.0.0.1 inside its container
Many frameworks listen on 127.0.0.1 by default in development mode. Inside a container that address belongs to the container alone, so the proxy, which comes from another container or from the host, is refused. The app has to listen on 0.0.0.0 (every address); the published ports and the shared network decide who can reach it.
What to do
- Set the listening address in the app's command or environment: for example
--host 0.0.0.0(uvicorn, Vite),--bind 0.0.0.0:8000(gunicorn),-b 0.0.0.0(Rails),--host=0.0.0.0(Flask), orHOST=0.0.0.0where the app reads it. - Recreate the container so the change takes effect (Compose replaces the container; the data in its volumes stays).
docker compose up -d --force-recreate <service> - Check that the port now listens on 0.0.0.0, * or [::].
sudo nsenter -t "$(docker inspect -f '{{.State.Pid}}' <container>)" -n ss -ltnp - Request the site again through the proxy; the response code is the first number.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
Next.js standalone listens on 127.0.0.1: set HOSTNAME=0.0.0.0
The server.js of the standalone output listens on the address in HOSTNAME, and Next.js documents HOSTNAME=0.0.0.0 for listening on every address. A value of 127.0.0.1 or localhost leaves the server unreachable from the proxy; Docker's own HOSTNAME, the container's id, binds it to one address of one network only.
This site runs the same way, a Next.js standalone server behind Coolify's proxy, and recorded one more trap: bound to the literal address 127.0.0.1, Next.js treated the proxy's internal rewrite as a cross-origin request and sent it into a redirect loop. HOSTNAME=0.0.0.0 fixed both.
What to do
- Set
HOSTNAME=0.0.0.0(andPORT, if it is not 3000) in the image (ENV HOSTNAME=0.0.0.0in the Dockerfile) or in the service's environment; in Coolify, as an environment variable of the resource. - Recreate the container so the change takes effect (Compose replaces the container; the data in its volumes stays).
docker compose up -d --force-recreate <service> - Check that the server now listens on 0.0.0.0.
sudo nsenter -t "$(docker inspect -f '{{.State.Pid}}' <container>)" -n ss -ltnp - Open the site and follow a redirect: it must end on a page, not bounce between two addresses.
curl -sS -o /dev/null -L --max-redirs 5 -w '%{http_code} %{url_effective}\n' https://<domain>/
Nothing listens in the container
The process runs but has not opened a TCP port: it is still starting, it failed after starting while staying alive, it listens on a Unix socket, or the container runs a different command than the one that starts the server.
What to do
- Read the app's log for the line that says where it listens, or for the error after it.
docker logs --since 15m <container> 2>&1 | tail -n 50 - Check the command the container really runs.
docker inspect -f '{{.Path}} {{.Args}}' <container> - If the app listens on a Unix socket (PHP-FPM, some gunicorn setups), point the proxy at it with the matching protocol (
fastcgi_passin nginx,php_fastcgiin Caddy) or make it listen on a TCP port.
The proxy targets a different port than the app listens on
The proxy must target the port inside the container, not a port published on the host: with ports: ["8080:3000"] the proxy on the shared network uses 3000. Traefik uses the first port the image exposes unless a label says otherwise, which is a common 502 for images that expose more than one port.
What to do
- Caddy:
reverse_proxy app:3000. nginx:proxy_pass http://app:3000;. Traefik: the labeltraefik.http.services.<name>.loadbalancer.server.port=3000. Coolify: the field Ports Exposes of the resource. - In Coolify, also remove a host port mapping you do not need: the proxy reaches the container over the network, and a published port bypasses the proxy's routing.
- Caddy: reload the Caddyfile (Caddy validates it first and keeps the old configuration if it is invalid).
docker exec <proxy> caddy reload --config /etc/caddy/Caddyfile - nginx: check the configuration, then reload it without dropping open connections.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - Traefik and Coolify read labels from the app's container: after changing a label (or a setting in the Coolify dashboard), recreate or redeploy the app, not the proxy.
- Request the site again through the proxy; the response code is the first number.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
The proxy and the app are on different networks
Each Compose project gets its own default network, and Coolify places resources on the network of their destination. A proxy reaches only the containers that share one of its networks; the usual fix is one external network that the proxy and every app behind it join.
What to do
- To test it now, attach the running app to the proxy's network. This lasts until the container is recreated.
docker network connect <network> <container> - To keep it, declare the network in the app's Compose file:
networks: { proxy: { external: true, name: <network> } }at the top level, andnetworks: [default, proxy]on the service. With Traefik on several networks, add the labeltraefik.docker.network=<network>so it uses that one. - In Coolify, for a Docker Compose resource that must reach other resources of its destination, enable Connect to Predefined Network, and check the generated name before you use it as a host name.
- Recreate the container so the change takes effect (Compose replaces the container; the data in its volumes stays).
docker compose up -d --force-recreate <service>
The proxy uses a name or address the network does not know
The proxy's host name must be the container's name, the Compose service name, or an alias, on a network both share; names do not resolve on Docker's default bridge network at all. A fixed IP address breaks the next time the container is recreated, because it gets a new one.
What to do
- Use the Compose service name (or an alias from the previous step) in the proxy's configuration, never an IP address.
- In Coolify, names get a generated suffix: copy the exact name from
docker ps, or let Coolify route by domain instead of a name you typed. - Caddy: reload the Caddyfile (Caddy validates it first and keeps the old configuration if it is invalid).
docker exec <proxy> caddy reload --config /etc/caddy/Caddyfile - nginx: check the configuration, then reload it without dropping open connections.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - Request the site again through the proxy; the response code is the first number.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
The host's proxy has no published port to reach
A proxy installed on the host cannot use container names or reach a port that is not published. Publish the app's port on the host's loopback address, so only the local proxy reaches it, and point the proxy at 127.0.0.1 and that port.
What to do
- In the service:
ports: ["127.0.0.1:3000:3000"](host port first, container port second). - Recreate the container so the change takes effect (Compose replaces the container; the data in its volumes stays).
docker compose up -d --force-recreate <service> - In the proxy:
reverse_proxy 127.0.0.1:3000(Caddy) orproxy_pass http://127.0.0.1:3000;(nginx), then reload the proxy's service. - Check the mapping.
docker port <container>
The app speaks HTTPS and the proxy speaks HTTP to it
Some images serve only HTTPS on their port, with a certificate of their own. A proxy that sends plain HTTP to that port gets an empty reply or a 400, and answers the visitor with 502. Either the proxy speaks HTTPS to the app, or the app serves plain HTTP inside the network and the proxy keeps TLS at the edge.
What to do
- Prefer plain HTTP inside the network: most images have a setting or a second port for it.
- Otherwise tell the proxy: Caddy
reverse_proxy https://app:8443, nginxproxy_pass https://app:8443;, Traefik the labeltraefik.http.services.<name>.loadbalancer.server.scheme=https. A self-signed certificate on the app also needs the proxy to trust it. - Caddy: reload the Caddyfile (Caddy validates it first and keeps the old configuration if it is invalid).
docker exec <proxy> caddy reload --config /etc/caddy/Caddyfile - nginx: check the configuration, then reload it without dropping open connections.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - Traefik and Coolify read labels from the app's container: after changing a label (or a setting in the Coolify dashboard), recreate or redeploy the app, not the proxy.
The app answers slower than the proxy waits
Each proxy waits a limited time for the response headers. nginx waits 60 seconds by default (proxy_read_timeout) and then answers 504; a proxy that gives up while connecting, or an app that drops a slow request, shows as 502. Raising the limit hides a slow request; it does not fix it.
What to do
- Find the slow request in the app's log and fix it first (a missing index, a call to another service, a long job that belongs in a background worker).
docker logs --since 15m <container> 2>&1 | tail -n 50 - If a route must be slow (an export, an upload), raise the limit for that route only: nginx
proxy_read_timeout 300s;, Caddytransport http { response_header_timeout 300s }, TraefikforwardingTimeouts.responseHeaderTimeoutof a serversTransport. - Check the app's own timeout too: a worker killed after 30 seconds (gunicorn's default
--timeout) closes the connection and the proxy answers 502. - nginx: check the configuration, then reload it without dropping open connections.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload'
The proxy does not pass the WebSocket upgrade
A WebSocket starts as an HTTP request with Upgrade: websocket and Connection: upgrade. Caddy and Traefik pass it without configuration. nginx does not: it speaks HTTP/1.0 to the app and drops these hop-by-hop headers unless told otherwise, and it closes an idle connection after proxy_read_timeout. Caddy closes open WebSockets when its configuration reloads, unless stream_close_delay is set.
What to do
- nginx: in the
location,proxy_http_version 1.1;,proxy_set_header Upgrade $http_upgrade;andproxy_set_header Connection $connection_upgrade;, with amap $http_upgrade $connection_upgrade { default upgrade; '' close; }in thehttpblock. - nginx: raise
proxy_read_timeoutfor thatlocation(for example3600s), or have the app send pings more often than the limit. - Caddy: set
stream_close_delayinreverse_proxyif reloads cut live connections. - Behind a second proxy or a CDN, check that it allows WebSockets too.
- nginx: check the configuration, then reload it without dropping open connections.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload'
The app's response headers are larger than nginx's buffer
nginx reads the response headers into one buffer, proxy_buffer_size, one memory page by default (4k or 8k). Large Set-Cookie headers (a session with many claims, several cookies after login) or a long Link header overflow it, nginx logs upstream sent too big header, and the visitor gets 502 on exactly those pages.
What to do
- Raise the buffers in the
location(orserver):proxy_buffer_size 16k;andproxy_buffers 8 16k;, andproxy_busy_buffers_size 32k;if nginx then complains about it. - nginx: check the configuration, then reload it without dropping open connections.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - Look at why the headers are that large: a session stored whole in a cookie belongs on the server.
The old container stops before the new one is ready
During a deploy, the proxy sends requests to the new container as soon as it exists, while the app inside is still starting, or to the old one after it stopped. Without a healthcheck the proxy cannot tell the two apart.
What to do
- Give the app a healthcheck that answers only when it can serve requests (with a
start_periodthat covers its startup). - In Coolify, an application (not a Docker Compose one) gets a rolling update: the new container starts while the old one runs, and with a health check Coolify waits for it to pass. A port published on the host or a custom container name makes Coolify stop the old container first; remove them if the proxy is the only way in.
- Watch the health of the new container during the next deploy.
docker ps --format 'table {{.Names}}\t{{.Status}}'
The app drops connections: it crashes or is killed mid-request
The proxy was connected and the app closed the connection without an answer. Usually a worker died: killed for memory (the kernel's OOM killer, or the container's memory limit), killed by its own timeout, or crashed on that request. Under load it happens more, because more workers hit the limit at once.
What to do
- Check whether the container was killed for memory and how often it restarted.
docker inspect -f 'oom={{.State.OOMKilled}} restarts={{.RestartCount}} exit={{.State.ExitCode}}' <container> - Read the app's log around the time of the 502: a worker timeout, a stack trace, or a worker restart.
docker logs --since 15m <container> 2>&1 | tail -n 50 - Watch memory against the limit while the 502s happen.
docker stats --no-stream <container> - If the container itself restarts, follow its exit code in the restart loop wizard; if memory is the cause, raise the limit or reduce the workers, and size the server again.
The proxy sends requests to another address than the one you tested
The app answers, yet every request through the proxy fails, so the proxy is not sending to that address. The classic case is nginx: a name in proxy_pass is resolved once, when nginx starts. After the app's container is recreated it gets a new IP address, and nginx keeps sending to the old one until it reloads. A fixed IP in the configuration fails the same way.
What to do
- Compare the address in the proxy's error line with the app's current address from the name check step.
- nginx: check the configuration, then reload it without dropping open connections.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - To keep nginx from caching the address, use Docker's DNS with a short validity and a variable:
resolver 127.0.0.11 valid=10s;thenset $app http://app:3000;andproxy_pass $app;. - In Coolify, check that the domain is set on the resource you deployed and redeploy it, so the proxy's labels point at the current container.
- Request the site again through the proxy; the response code is the first number.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
No match yet: what to collect
The app listens, yet a request from the proxy's side does not reach it. Something between them blocks the traffic: a firewall rule in the DOCKER-USER chain, a network created with internal: true in the wrong place, or a host with a non-default Docker setup (rootless Docker, or firewall software that rewrites Docker's rules). Collect these before you ask for help; none of them changes the server.
What to do
- The proxy's configuration block for this site (without secrets).
- The proxy's last error lines.
docker logs --since 15m <proxy> 2>&1 | tail -n 50 - The shared network: its driver, whether it is internal, and the containers on it.
docker network inspect <network> - The firewall rules Docker and you added.
sudo iptables -S DOCKER-USER - Docker's version and setup.
docker info --format '{{.ServerVersion}} {{.SecurityOptions}}'
What this does not cover
The wizard covers a single Linux host with Docker Engine, where the proxy runs in a container or on the host itself. It does not check your server from outside, and it does not cover a CDN or load balancer in front of the proxy, Kubernetes ingress controllers, Docker Swarm services or Docker Desktop.
The commands assume root through sudo and the host's own ss and curl; rootless Docker places containers in another namespace, and the checks there differ. When no branch matches, the last step lists what to collect; it does not guess.
Related pages
Sources
- Docker Docs: docker container ls
- Docker Docs: docker container logs
- Coolify Docs: Bad Gateway error
- Compose file reference: healthcheck
- Dockerfile reference: HEALTHCHECK
- Docker Docs: docker container inspect
- ss(8), Linux manual page
- nsenter(1), Linux manual page
- Docker Docs: Networking overview
- Next.js Docs: output (standalone)
- nginx: Module ngx_http_proxy_module
- Caddy Documentation: reverse_proxy
- Traefik Docs: Docker routing configuration
- Docker Docs: docker network connect
- Compose file reference: networks
- Docker Docs: Networking in Compose
- Coolify Docs: Docker Compose
- Docker Docs: Bridge network driver
- Docker Docs: Port publishing and mapping
- Docker Docs: docker container port
- curl man page
- Traefik Docs: ServersTransport
- nginx: WebSocket proxying
- Coolify Docs: Rolling updates
- Coolify Docs: Health checks
- Traefik Docs: Logs
- nginx: Module ngx_http_core_module, resolver
How ANAROQ sources the facts on its pages is described in the methodology. Read the methodology