Why Your App Works Locally but Fails After Deploy: The $PORT and 0.0.0.0 Fix
If your app runs perfectly on your laptop but never answers after deploy, it’s almost always listening on the wrong port or the wrong network interface. The fix is two lines: read the port from the PORT environment variable, and bind to 0.0.0.0 instead of localhost.
This guide explains why both mistakes are so common, what they look like on a git-push host, and how to fix them in the languages people deploy most.
Why does an app work locally but not after deploy?
On your machine, you open a browser to http://localhost:3000 and the app answers. Two assumptions are hiding in that URL:
- The port is 3000 because you (or a framework template) wrote
3000in the code. - The request comes from the same machine, so listening only on the loopback interface (
127.0.0.1, a.k.a.localhost) is enough.
In production, neither is true. Your app runs inside a container, and the platform’s router sits outside it. The router forwards traffic to a specific port that the platform chose, and it reaches your container over a network interface that is not loopback. If your app is listening on a different port, or only on 127.0.0.1, the router knocks and nobody is home.
Nothing in the build catches this. Compilation succeeds, dependencies install, the process starts, and your own startup log line cheerfully says “listening on port 3000”. The failure only shows up when something outside the container tries to connect.
What is the PORT environment variable?
Most hosting platforms that run your code in containers use the same simple contract: they put a port number in an environment variable called PORT, and your app is expected to listen on it.
On Candor Deploy, that contract is spelled out in the builds and configuration docs: we set PORT in your app’s environment (it defaults to 8080), route HTTPS traffic from your app’s URL to that port, and health-check the app by opening TCP connections to it. An app that binds to some other hard-coded port builds fine and then never comes live, because it never answers the health check.
The value can differ between platforms and can change, so the rule is simple: never assume the number, always read it.
What does “binding to 0.0.0.0” actually mean?
A server listens on an address as well as a port. The address decides which network interfaces it accepts connections on:
127.0.0.1/localhost: only connections that originate inside the same machine or container.0.0.0.0: every interface, including the one the platform’s router uses to reach your container.
Many dev servers default to localhost for safety, which is exactly right on a laptop in a coffee shop and exactly wrong in a container. Inside a container, 0.0.0.0 doesn’t expose anything extra to the internet; the platform’s router is still the only way in.
What does the failure look like?
The symptoms are distinctive once you know them. According to the troubleshooting guide, a wrong port on Candor Deploy usually shows up one of two ways:
- The deployment sits in “Deploying” and never reaches “Live.” That’s intentional: the previous version keeps serving until the new one proves it’s up, so a broken deploy doesn’t take down a working site.
- The app starts, fails its health check, and restarts over and over. The runtime logs show a clean boot each time, which is what makes this one confusing.
If your runtime logs show a healthy startup message with a port number that isn’t the one in PORT, you’ve found it.
How do I fix it in Node.js?
Read PORT with a local fallback, and pass an explicit host:
const port = Number(process.env.PORT) || 3000;
app.listen(port, "0.0.0.0", () => {
console.log(`listening on ${port}`);
});
Watch for frameworks with their own dev and production servers. Some start scripts accept --port and --hostname (or --host) flags; pass --port $PORT --hostname 0.0.0.0 in your start script rather than relying on defaults that were designed for local development.
How do I fix it in Python?
For a production server like Gunicorn or Uvicorn, put the bind address in the start command:
gunicorn app:app --bind 0.0.0.0:$PORT
uvicorn main:app --host 0.0.0.0 --port $PORT
Avoid shipping the framework’s built-in development server (flask run, manage.py runserver) to production; besides defaulting to localhost, it isn’t built for real traffic. If you must start from code:
import os
port = int(os.environ.get("PORT", "8000"))
app.run(host="0.0.0.0", port=port)
How do I fix it in Go?
Go’s net/http binds to all interfaces when you leave the host empty, so the only job is the port:
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Fatal(http.ListenAndServe(":"+port, mux))
Note that ":"+port means “all interfaces”. Writing "localhost:"+port brings the bug right back.
How do I fix it in Ruby and PHP?
For Rails with Puma, bind explicitly in the start command or config/puma.rb:
bundle exec puma -b tcp://0.0.0.0:$PORT
For PHP, the built-in server needs both pieces as well, though for anything serious you’ll want a proper web server in front of PHP-FPM:
php -S 0.0.0.0:$PORT -t public
What if I’m using a Dockerfile?
The same rules apply inside a container image you build yourself. Two extra traps:
EXPOSE 3000in a Dockerfile is documentation, not configuration. It doesn’t make the platform send traffic to 3000.- Shell-form vs exec-form
CMDmatters for variable expansion.CMD ["node", "server.js", "--port", "$PORT"]passes the literal string$PORT, because exec form doesn’t run a shell. ReadPORTinside your code, or use shell form.
On Candor Deploy, a Dockerfile in your repo is ignored unless you opt in, per the quickstart, which keeps a stale local-dev Dockerfile from breaking your deploy.
Does a background worker need a port?
No. A worker that pulls jobs from a queue or runs a loop doesn’t serve HTTP, so it shouldn’t open a port at all. On Candor Deploy, workers get no port and no URL, and they skip the port health check entirely, as the builds and configuration docs note. If you’re deploying something that never answers HTTP, deploy it as a worker rather than a web service, or its health check will never pass.
What’s a quick pre-deploy checklist?
Before you push, check these five things:
- Search your code for a hard-coded port.
grep -rn "3000\|5000\|8000" src/catches most of them. - Read
PORTfrom the environment, with a local fallback for development. - Bind to
0.0.0.0(or an empty host in Go), neverlocalhostor127.0.0.1. - Use a production server in your start command, not a framework’s dev server.
- Test it like production locally:
PORT=8080 npm start, then hithttp://127.0.0.1:8080from another terminal. If that works with a port you didn’t choose, the platform’s port will work too.
Next step
If your deploy is stuck right now, open the app’s Logs tab and compare the port in your startup line with the PORT value, then walk through the Candor Deploy troubleshooting guide. It covers this failure and the other common ones, step by step.