PORT
A widely used convention for specifying the port number a web server should listen on. Used by virtually every Node.js web framework (Express, Fastify, Koa, etc.) and platform-as-a-service providers. Defaults vary by framework but 3000 is common.
Last updated:
PORT is the de-facto convention for the TCP port a web server listens on. It is not defined by any single spec — it is a shared agreement that platforms-as-a-service (Heroku, Railway, Render, Cloud Run, App Engine) rely on: they inject PORT at runtime and expect your app to bind to it. The single most common deploy failure on these platforms is hardcoding `app.listen(3000)` instead of `app.listen(process.env.PORT)`, which makes the platform's health check time out because nothing is listening on the port it routed traffic to.
- Provider
- Node.js
- Category
- networking
- Set by
- Set manually or automatically by cloud platforms (Heroku, Railway, Cloud Run, etc.)
- Example
- 3000
How to set PORT
Node.js / Express
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`listening on ${port}`));bash
PORT=8080 node server.jsdocker-compose
services:
web:
environment:
PORT: 8080
ports:
- "8080:8080"Where does the PORT convention come from?
Heroku. Their dyno model (2009 onward) routes traffic to a port the platform picks at dyno start, so the contract became "we set PORT, you bind to it" — and because so much early PaaS tooling imitated Heroku, the convention stuck. Today Cloud Run injects PORT=8080 and fails the deploy if the container never listens on it, App Engine does the same, Azure App Service for Linux containers uses PORT (with WEBSITES_PORT as the override knob), and Railway and Render follow the Heroku shape. None of this is a standard in the RFC sense; it is the most successful gentleman's agreement in deployment.
PORT is a string — and that matters
Environment variables are always strings. Node's server.listen('8080') happens to coerce, so JavaScript developers rarely notice — until a strict comparison like port === 8080 silently fails, or a config validator rejects the value. Typed languages force the issue immediately:
# Python: int() it or socket.bind() raises TypeError
port = int(os.environ.get("PORT", "8000"))// Go: env vars come out of os.Getenv as strings
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Fatal(http.ListenAndServe(":"+port, nil))Why does my container get traffic locally but not in production?
Two distinct failure modes, both about the listen call rather than PORT's value:
- Bound to the wrong interface. Inside a container,
localhostmeans the container's own loopback — the runtime's port mapping can't reach it. Bind to0.0.0.0(all interfaces) in containers. This is the single most common "EXPOSE/ports look right but connection refused" bug in Docker, and it bites Flask (app.run()defaults to 127.0.0.1) andvite devusers constantly. - Bound to the wrong port. Hardcoding
3000while the platform routed traffic to its injected PORT. The health check times out, the platform kills the instance, and the logs show a perfectly healthy app — on a port nobody is checking.
In docker-compose setups, remember the mapping syntax is "HOST:CONTAINER" — changing PORT inside the app means updating the right-hand side, not the left.
Privileged ports and who may bind them
On Linux, binding below 1024 traditionally requires root — which is why dev servers cluster at 3000, 5000, 8000, and 8080. The modern alternatives to running as root: setcap 'cap_net_bind_service=+ep' ./server grants just the bind capability, systemd socket activation hands the app an already-bound socket, and the sysctl net.ipv4.ip_unprivileged_port_start=0 removes the threshold entirely (it's how rootless container runtimes cope). macOS dropped the restriction outright in Mojave (10.14) — unprivileged processes can bind 80 and 443 there, which regularly surprises people when the same command fails on Linux. In practice most deployments sidestep all of this: the app listens high (PORT=8080) and a load balancer or ingress owns 80/443.
EADDRINUSE and finding the squatter
# who is on port 3000?
lsof -iTCP:3000 -sTCP:LISTEN # macOS / Linux
ss -ltnp 'sport = :3000' # Linux
# then either kill the PID or pick another port:
PORT=3001 npm run devA port can also linger in TIME_WAIT for a minute after a crash; servers that set SO_REUSEADDR (Node does by default) rebind immediately, which is why the error usually means a live process, not a ghost. Setting PORT per invocation — PORT=3001 npm run dev — is the quickest way to run two branches of the same app side by side. For how that inline syntax works and its Windows equivalent, see the Node.js environment variables guide.
References
Frequently Asked Questions
Why does my app work locally but fail to start on Cloud Run / Heroku?
The platform sets PORT and expects your server to bind to it. If you hardcode a port (e.g. app.listen(3000)), the platform routes traffic to a different port, the health check finds nothing listening, and the deploy is marked failed. Read process.env.PORT instead.
Stay up to date
Get notified about new guides, tools, and cheatsheets.