2026.09 / proxy routing 054
Coolify 502 Bad Gateway? Fix the port and listen address
A Coolify application can deploy successfully, show a running container, and still return 502 Bad Gateway at its domain. That combination usually means the public proxy accepted the request but could not complete the next hop to the application process. The shortest repair path is to prove four values agree: the port the process actually listens on, the address it binds to, Coolify’s exposed port, and Traefik’s selected backend.
This guide is for Git-based Coolify applications and Docker Compose services behind Coolify’s proxy. It deliberately does not “fix” the problem by opening an arbitrary VPS port. Traefik normally reaches the container over a Docker network; publishing the application to the whole internet is neither required nor a substitute for correct internal routing.
A running container proves only that its main process has not exited. It does not prove that a web server is listening where Coolify expects or that another container can reach it.
Classify the response before changing anything
| Observed result | Likely boundary |
|---|---|
| Coolify deployment failed before startup | Build or deployment problem; read that deployment’s logs first |
Domain returns 404 | Hostname/router rule may not match, or the app itself returned 404 |
Domain returns 502 | Proxy has a route but its upstream connection or response failed |
Domain returns 503 no available server | No eligible backend, often due to health or router/service configuration |
Domain returns 504 | Upstream accepted too slowly or failed to respond before the proxy timeout |
| Direct container request works, domain fails | Docker network, selected port, health eligibility, or Traefik configuration |
Capture response headers and the recent application log window. If Cloudflare fronts the domain, note whether the error page identifies Cloudflare or the origin, but do not assume Cloudflare caused it. A proxy chain can have more than one gateway.
curl --silent --show-error --dump-header - \
--output /dev/null https://app.example.com/
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Networks}}'
docker logs --since 10m APP_CONTAINER
Use the real hostname and container name only in your private terminal. Do not share complete inspect output or environments publicly; they can contain repository details, credentials, and internal service names.
1. Prove a web process is listening inside the container
Application logs often print the intended port. Treat that as a clue, then test it. Use a client already present in the image and target the container’s own loopback address:
docker exec APP_CONTAINER sh -lc \
'wget -qSO- http://127.0.0.1:3000/health 2>&1 || true'
# If the image includes curl instead:
docker exec APP_CONTAINER sh -lc \
'curl --silent --show-error --include http://127.0.0.1:3000/health'
Replace 3000 and /health with the application’s real internal port and endpoint. A connection refusal means nothing is listening there yet. A 404 proves the HTTP server answered, so the port is probably right and only the path may be wrong. A 500 proves the server answered but the application has an internal fault. If neither HTTP client exists, inspect the startup log and process configuration rather than installing ad hoc tools into the running production container; make diagnostic tooling an intentional image choice.
2. Distinguish loopback from all-interface binding
An application bound to 127.0.0.1:3000 can answer a command executed inside its own container, while Traefik receives a refusal when it connects to the container’s Docker-network address. For normal reverse-proxy routing, configure the server to listen on 0.0.0.0 at the internal port. This exposes it to the container’s network interfaces, not automatically to the public internet.
# Framework-neutral intention:
HOST=0.0.0.0
PORT=3000
# Example Node server:
server.listen(process.env.PORT || 3000, '0.0.0.0')
Do not set an application’s listen address to the VPS’s public address, the domain name, or another container’s address. Also do not confuse IPv6-only listening with an IPv4 Docker-network connection. Restart or redeploy after changing the bind setting, then repeat the in-container request.
3. Set Coolify’s exposed port to the container port
For a Coolify Application, the Ports Exposes value must match the port inside the container—3000 in this example. It is not the public HTTPS port 443, not a temporary host port such as 18080, and not necessarily the port used by a developer’s local machine.
Traefik’s Docker documentation explains the underlying rule: when automatic port detection is unsuitable, the service’s load-balancer port tells Traefik which container port to connect to. A wrong value sends a perfectly valid public request to a socket where no process is listening, producing the familiar gateway failure.
If the application exposes several ports, make the intended HTTP port explicit. Administrative, metrics, debug, and database ports should not be accidentally selected as the public web backend. Re-fetch the application settings after saving and redeploy once; do not create repeated deployments while settings are still changing.
4. Do not use host publishing as a proxy repair
Docker distinguishes container networking from host port publishing. In Compose, this publishes a host port:
services:
web:
build: .
ports:
- "3000:3000"
Coolify’s official 502 guide recommends removing incorrect host mappings for applications so its proxy can route correctly. In the normal setup, Traefik and the application share a Docker network and Traefik connects directly to the container port. A host mapping can cause conflicts, expose an unintended service, or encourage testing a path that the production proxy never uses.
If a Compose service genuinely needs a host-published port, bind it deliberately and document why. Docker warns that published ports are externally reachable by default; binding to 127.0.0.1 limits host access, but that is still a separate path from container-to-container proxy routing.
5. Prove the proxy can reach the same socket
Once the request succeeds inside the application container, test from a temporary diagnostic container attached to the same Docker network. This separates application readiness from cross-container reachability:
docker run --rm --network COOLIFY_NETWORK curlimages/curl:latest \
--fail --silent --show-error http://APP_CONTAINER:3000/health
Use the exact application container name and one of its networks as shown by docker inspect. Do not attach containers to database-only networks casually. Remove the temporary container automatically with --rm. Interpret the result precisely:
- In-container works; same-network fails: check the
0.0.0.0bind, shared network, container name and network policy. - Both work; domain is 502: inspect Coolify’s port/domain settings and the effective Traefik service labels.
- Both fail: return to application startup, port and dependency logs.
- Health works; root fails: the application route or middleware may be failing even though readiness is good.
6. Check health without confusing 502 and 503
Health checks determine whether a backend is eligible to receive traffic. Inspect the effective Docker health state and recent probe output:
docker inspect APP_CONTAINER \
--format '{{json .State.Health}}'
docker inspect APP_CONTAINER \
--format '{{json .Config.Healthcheck}}'
The probe must use the internal port and a real, unauthenticated path. If it reports curl: not found or wget: not found, repair the final runtime image using the dedicated Coolify health-check executable guide. If it succeeds inside a loopback-bound app while Traefik still cannot connect, the health check is producing a false sense of readiness: fix the listen address.
7. Inspect effective Traefik routing last
After the process, port, bind and network tests pass, inspect the application container’s proxy labels. Focus on the router’s hostname rule, named service, selected load-balancer port, and network. Avoid editing generated labels directly on a running container because a Coolify redeploy will replace them.
docker inspect APP_CONTAINER \
--format '{{json .Config.Labels}}'
# Keep this local and redact before sharing.
Repair the source setting in Coolify or the Compose file, then redeploy. If multiple replicas or old containers exist, check each backend; one stale replica can make the failure intermittent. A direct request to the right container does not prove every backend in the service is correct.
Framework defaults that commonly cause the fault
| Application behaviour | What to verify |
|---|---|
| Development server defaults to localhost | Production command explicitly binds 0.0.0.0 |
Platform injects PORT | Process reads that variable and Coolify exposes the same value |
Dockerfile says EXPOSE 8080, process uses 3000 | Make image metadata, runtime command and Coolify setting agree |
| Multi-service Compose stack | Domain is attached to the web service, not the worker or database |
| Server starts before a dependency | Logs and readiness distinguish temporary startup from permanent failure |
| App trusts proxy headers incorrectly | Look for redirect loops or request rejection after connectivity succeeds |
EXPOSE documents an intended container port but does not make the process listen and does not publish the port to the host. Likewise, adding a Coolify port setting cannot repair a process that exited or chose another port. Verify runtime behaviour, not declarations alone.
Redeploy once and verify every hop
- Commit the bind-address, runtime-port, Dockerfile, Compose, or configuration repair to the source branch.
- Trigger one Coolify deployment and follow its returned deployment identifier to a terminal state.
- Confirm the replacement container is running and healthy.
- Repeat the in-container and same-network requests.
- Fetch the public HTTPS domain and assert unique expected content—not only status
200. - Check an important API or asset route and an intentionally nonexistent path.
- Retain the previous known-good release until acceptance checks pass.
curl --fail --silent --show-error https://app.example.com/ | \
grep -F 'EXPECTED APPLICATION HEADING'
curl --silent --output /dev/null --write-out '%{http_code}\n' \
https://app.example.com/path-that-must-not-exist
A SPA fallback can return its home page with 200 for a nonexistent URL, and a CDN can return an old successful page while the origin is still broken. Match a heading, release marker, API field, or other unique content from the intended deployment.
Compact 502 acceptance checklist
- The application process stays running and its recent logs are understood.
- An HTTP request succeeds on the actual internal port inside the container.
- The server binds to
0.0.0.0, not only container-local loopback. - Coolify’s exposed port matches the process’s internal port.
- No unnecessary public host-port mapping was added as a workaround.
- A request from the shared Docker network reaches the application.
- The health probe uses the same intended port and returns success.
- The effective router selects the intended service, network and load-balancer port.
- One new deployment reaches a terminal successful state.
- The public domain serves unique content from the intended release.
Authoritative references: Coolify’s official Bad Gateway troubleshooting guide identifies incorrect exposed ports, host mapping, localhost-only binding, domain ports and container health as primary checks. Traefik’s Docker routing reference explains explicit load-balancer server ports and wrong-port 502 errors. Docker’s networking documentation explains container networks and the separate security implications of published host ports.