Jump to your cause
Why a "successful" deploy still 502s 1. Wrong host or port (the #1 cause) 2. No web process actually running 3. Crash on boot 4. Out of memory (OOM) 5. A migration is blocking startup 6. Healthcheck / startup timeout The exact fix per framework How to diagnose in 60 seconds FAQWhy a "successful" deploy still 502s
"Deploy succeeded" almost never means "your app is serving traffic." It means the build finished and the container started. After that, the platform's edge proxy tries to forward a request to your process — and if your process isn't listening where the proxy expects, you get Application failed to respond, which the browser shows as a 502 Bad Gateway.
So the question is never "did the deploy work." It's: is a healthy process actually listening on the right host and port? Ninety percent of the time the answer is no, for one of six reasons below.
1. Wrong host or port — the #1 cause
Hosting platforms inject the port your app must listen on via the $PORT environment variable, and they expect you to bind to 0.0.0.0, not localhost/127.0.0.1. Bind to localhost and the app only accepts connections from inside its own container — the proxy can't reach it, so: 502.
// ❌ only reachable from inside the container
app.listen(3000, "127.0.0.1")
// ✅ reachable by the platform proxy
app.listen(process.env.PORT || 3000, "0.0.0.0")
Search your codebase for a hardcoded port and for localhost/127.0.0.1 in your server's listen call. If either is there, that's almost certainly your bug.
2. No web process actually running
If your start command runs a worker, a one-off script, or exits immediately, there's no long-lived server for the proxy to hit. Make sure your start command launches the web server and stays in the foreground (no backgrounding with &, no command that returns instantly).
3. Crash on boot
The container starts, your app throws during startup (a missing environment variable, a bad import, a failed DB connection), and the process dies. The proxy has nothing to talk to. This is the second-most-common cause and it's invisible unless you read the runtime logs from the moment the process started — look for a stack trace or a SIGTERM/exit right after boot.
4. Out of memory (OOM)
On small instances and free tiers, a heavy boot (large dependency tree, building in-process, loading a model) can blow the memory limit. The orchestrator kills the process, you get a 502, and the only fingerprint is an OOMKilled / exit code 137 in the logs. Fix: raise the memory limit, or trim what loads at startup.
5. A migration is blocking startup
A start command that runs migrations before booting the server will hang or fail the whole thing if the migration stalls (locked table, unreachable database, long backfill). The server never starts listening → 502. Run migrations as a separate, pre-deploy step, not inline with the web process start.
6. Healthcheck or startup timeout
If your app takes longer to become ready than the platform's healthcheck/startup window, the platform marks it unhealthy and routes nothing to it. Either speed up startup, or increase the startup/healthcheck timeout, or point the healthcheck at a path that returns 200 as soon as the server can accept connections.
The exact fix per framework
Node / Express
app.listen(process.env.PORT || 3000, "0.0.0.0", () =>
console.log("listening on", process.env.PORT));
Next.js
Next reads PORT automatically — the usual culprit is the start command. Use next start -p $PORT (or just next start and let it read PORT), and for standalone output make sure you run node .next/standalone/server.js with PORT and HOSTNAME=0.0.0.0 set.
HOSTNAME=0.0.0.0 PORT=$PORT node server.js
Python — Flask / FastAPI / Django
# Flask (gunicorn)
gunicorn app:app --bind 0.0.0.0:$PORT
# FastAPI (uvicorn)
uvicorn main:app --host 0.0.0.0 --port $PORT
# Django
gunicorn project.wsgi --bind 0.0.0.0:$PORT
Never ship the dev server (flask run / manage.py runserver / uvicorn --reload) to production.
Go
http.ListenAndServe(":"+os.Getenv("PORT"), nil) // binds all interfaces
Ruby on Rails
bundle exec puma -b tcp://0.0.0.0:$PORT
How to diagnose in 60 seconds
- Open the runtime logs at the moment of boot. Not the build logs — the process logs. Is there a stack trace, an exit code, an
OOMKilled, a "listening on…" line? - Confirm the listen line. Does it say
0.0.0.0and the injected$PORT? If you don't see a "listening" log at all, your process isn't starting a server. - Trace one real request. Does it even reach your app, or die at the proxy? If it never arrives, it's bind/port. If it arrives and errors, it's your code.
- Check the healthcheck. Is the configured path returning 200 within the timeout?
The reason this takes most people an hour instead of a minute is that the four signals above — boot logs, the listen line, the request trace, and the healthcheck — usually live in four different tools (or aren't captured at all). When they're in one place, "Application failed to respond" stops being a mystery.
Stop guessing why it 502s.
Infraveil is a control plane you run on your own servers. It puts your deploys, live runtime logs, request traces, and process supervision in one place — so when a deploy "succeeds" but the app won't answer, you see the exact boot crash, the failed bind, or the OOM immediately, and roll back with one click.
See the live demo →Frequently asked questions
What does "Application failed to respond" mean?
The edge proxy reached your container but your app never answered on the expected port — usually wrong host/port, a boot crash, OOM, or a startup still in progress. It shows up as a 502 Bad Gateway.
Why do I get a 502 after a successful deploy?
"Success" only covers build + container start. A 502 after means the running process isn't serving on the port the proxy expects. Bind to 0.0.0.0 and $PORT, ensure a web process is actually running, and check the boot logs.
How do I fix it?
Bind to 0.0.0.0:$PORT, confirm a web server (not a worker/script) is your start command, read the runtime logs for a crash or OOM, move migrations out of the server-start step, and make sure the healthcheck passes within the timeout.
It works locally but 502s in production — why?
Locally you bind to localhost and a fixed port, which is fine on your machine. In production the proxy needs 0.0.0.0 and the injected $PORT, and your prod environment may be missing a variable or hitting a memory limit that your laptop never does. Read the prod boot logs — they'll tell you which.
One place that tells you why.
Deploy, supervise, trace, secure, recover, and prove what happened — one control plane on your own servers. The next time a deploy goes green and the app goes dark, you'll know why in seconds.
Enter the live demo →