Skip to content

Ports, and why a domain shows the wrong thing

Your app runs inside a container. It listens on a port there, and we route your domain to that port. For your app to be reachable, both halves have to agree.

Listen on the port in the PORT environment variable, and bind 0.0.0.0.

We set PORT for every app, and we route your domain to the same number. An app that reads it is always correct, and you never have to configure a port at all.

// Node
app.listen(process.env.PORT || 3000, '0.0.0.0');
# Python, with gunicorn
gunicorn myapp.wsgi:application --bind 0.0.0.0:$PORT
// Go
port := os.Getenv("PORT")
if port == "" { port = "8080" }
http.ListenAndServe(":"+port, nil) // ":" alone binds all interfaces

If you would rather set the port yourself, you can. Open your app, go to Settings, and set Listening port to whatever your app binds.

Under Settings your app has a Listening port card that asks your container what it is actually listening on and compares it to where your domain points. It tells you which of these you are looking at.

You set your app to port 8000, we are routing to 3000. Requests arrive at a port nothing is answering on.

The card offers to change our side for you. Or change your app and redeploy. Either works, they just have to match.

Your app is listening on the right port, but only on localhost

Section titled “Your app is listening on the right port, but only on localhost”

This one is confusing, because every number involved is correct.

A service bound to 127.0.0.1 (also written localhost) can only be reached from inside its own container. The proxy that serves your domain is outside it, so it gets nothing. Do not change the port. Bind 0.0.0.0 instead, which means “every interface”.

In most frameworks that is a host argument next to the port:

Framework Change
Express, Fastify, Koa app.listen(port, '0.0.0.0')
Next.js next start -H 0.0.0.0
Flask, Django dev server --host 0.0.0.0
uvicorn --host 0.0.0.0
gunicorn --bind 0.0.0.0:$PORT
Vite preview --host 0.0.0.0

The container is running but holding no port. If you deployed in the last minute it may still be starting. Otherwise it started and then stopped listening, which usually appears in your deployment logs or your app logs.

Your app is not reached at an IP and a port

Section titled “Your app is not reached at an IP and a port”

This is worth being clear about, because it is a common assumption.

Your app’s port is internal. You do not open it to the internet, and you cannot visit your app at an IP address followed by a port number. Your app is reached at its domain, over HTTPS, on the standard ports, and we handle the certificate.

If you visit a server’s IP address on some port and see something unexpected, you are not looking at your app. You are looking at whatever else happens to be answering there.

If you write your own Dockerfile, two lines are what matter:

# A default for running the image outside Twelveinks. We override it.
ENV PORT=3000
# The shell form, not ["..."]. A JSON-array CMD does not expand variables, so your app would
# receive the literal text ${PORT} and fail to start.
CMD node server.js --host 0.0.0.0 --port ${PORT}

You can also keep the port and build settings in your repository rather than setting them here. See Configuration file.