Render Tutorials
When deploys go wrong

Boot and port binding

⏱ 9 min

This is the family of failures where the deploy log is green, the build is fine, and the Events feed still says Deploy failed. The runtime logs are where the answer lives, and the most common pattern on Earth is one of these four:

  • The app started but isn’t listening on a port Render can see.
  • The app is listening on the right port but bound to 127.0.0.1 instead of 0.0.0.0.
  • The Dockerfile has no CMD so nothing starts at all.
  • The startCommand runs an interactive process that hangs forever.

Each one has a very specific log signature. Once you’ve seen it twice, you spot it instantly.

How Render decides a service is “live”

flowchart TB
  build["Build succeeds"]
  start["Render runs<br/>your startCommand"]
  scan["Port scanner waits for<br/>a TCP listener on 0.0.0.0"]
  health{"Health check<br/>configured?"}
  hc["Send GET to<br/>healthCheckPath"]
  ok["Service is LIVE"]

  build --> start --> scan -->|"port found"| health
  health -->|"yes"| hc -->|"200 OK"| ok
  health -->|"no"| ok

Two implicit contracts, both easy to break:

  1. Bind to 0.0.0.0, not 127.0.0.1 or localhost. Render’s port scanner runs in a separate network namespace from your process; a process listening only on loopback is invisible.
  2. Listen on the PORT env var if it’s set. Render injects PORT=10000 by default for web services. If your code hardcodes 3000, Render will scan 10000 and find nothing.

Step 06 covers the health check side. This step covers steps 1 and 2 - the port scanner.

”No open ports detected”

The canonical symptom of a boot-time port problem.

What you’ll see

Runtime log of a stuck deploy
==> Running 'npm start'
> app@1.0.0 start
> node server.js
Server listening on http://localhost:3000
==> No open ports detected, continuing to scan...
==> No open ports detected, continuing to scan...
==> Timed out
==> Deploy failed

The combination is the giveaway: your app says it’s listening (look at line 4), but Render’s scanner can’t see it. Two possible causes, distinguishable by reading carefully:

If line 4 saysCause
Server listening on http://localhost:3000Bound to loopback and wrong port - both problems
Server listening on http://0.0.0.0:3000Right interface, wrong port - Render is scanning 10000
Server listening on http://localhost:10000Right port, wrong interface - bound to loopback

The fix, by language

The pattern is the same in every framework: read PORT from the environment, default it if missing, and bind to 0.0.0.0.

Node / Express
const port = process.env.PORT || 10000;
app.listen(port, "0.0.0.0", () => {
console.log(`Listening on 0.0.0.0:${port}`);
});
Python / Flask
import os
port = int(os.environ.get("PORT", 10000))
app.run(host="0.0.0.0", port=port)
Python / FastAPI (uvicorn)
# In your startCommand:
# uvicorn main:app --host 0.0.0.0 --port $PORT
Python / Django (gunicorn)
# In your startCommand:
# gunicorn myproject.wsgi --bind 0.0.0.0:$PORT
Ruby / Rails (puma)
# config/puma.rb
port ENV.fetch("PORT", 10000)
bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 10000)}"
Go / net/http
port := os.Getenv("PORT")
if port == "" { port = "10000" }
http.ListenAndServe("0.0.0.0:"+port, nil)

Verifying the fix locally

Before pushing, run the exact same command you set as your startCommand, with PORT=10000:

Terminal (local)
PORT=10000 npm start
# Should print: Listening on 0.0.0.0:10000
# In another shell:
curl -v http://localhost:10000/
# Should connect and return your app's response
curl -v http://127.0.0.1:10000/
# Should also work - if it does but the deploy still fails, see below

If curl works locally but the deploy times out, the problem is either bind interface (try binding to 0.0.0.0 explicitly, not localhost) or the framework is binding to IPv6 only - see the next pattern.

The IPv6 trap

A subtler variant: some frameworks bind only to IPv6 by default, which Render’s port scanner doesn’t see.

What you’ll see

Runtime log
==> Running 'rails server'
=> Booting Puma
=> Rails 7.1.0 application starting in production
=> Puma starting in single mode...
* Listening on tcp://[::1]:3000
==> No open ports detected, continuing to scan...

The [::1]:3000 is IPv6 localhost. Render’s scanner is looking for IPv4 on 0.0.0.0.

The fix

Explicitly bind to 0.0.0.0 and set the port:

Rails startCommand
bundle exec rails server -b 0.0.0.0 -p $PORT
Puma config
# config/puma.rb - use the *string* form, not the array form
bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}"

This affects: Rails default, some Node servers in dev mode, some Go HTTP libraries when given :3000 instead of 0.0.0.0:3000.

Docker: missing CMD or ENTRYPOINT

If your service uses the Docker runtime, the boot phase is whatever instruction your Dockerfile’s CMD or ENTRYPOINT line specifies.

What you’ll see

Runtime log: Docker with no CMD
==> Image built successfully
==> Deploying...
(...hangs indefinitely...)
==> Deploy timed out
==> Deploy failed

No application output at all. The image was built, Render tried to start a container, and nothing ran.

What it means

The Dockerfile has no CMD or ENTRYPOINT, or both have been overridden to something that exits immediately. Render’s troubleshooting docs call this out explicitly: a Dockerfile without one of these directives must be paired with an explicit dockerCommand field in your Blueprint.

The fix

Add the CMD to your Dockerfile:

Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Render needs to know what to run. Pick one:
# Form 1: exec form (recommended - signals propagate correctly)
CMD ["node", "dist/server.js"]
# Form 2: shell form (signals don't propagate cleanly)
# CMD node dist/server.js

Or set dockerCommand in your Blueprint:

render.yaml
services:
- type: web
name: api
runtime: docker
dockerfilePath: ./Dockerfile
dockerCommand: node dist/server.js

Hanging start commands

The third common variant: your startCommand runs something interactive that waits for stdin forever.

What you’ll see

Runtime log: hanging start
==> Running 'rails console'
Loading production environment (Rails 7.1.0)
==> No open ports detected, continuing to scan...
==> No open ports detected, continuing to scan...
Runtime log: migrate as the start command
==> Running 'rails db:migrate && rails server'
==> Migrating database
== CreateUsers: migrating ===========================
-- create_table(:users)
==> No open ports detected, continuing to scan...

The second variant looks fine until you realise the migration finished but rails server hasn’t started yet - and the next time it deploys, the migration runs again. Long migrations can blow past the deploy timeout entirely.

The fix

Two principles:

  1. The startCommand must be a server, not an interactive REPL or one-off script.
  2. Migrations belong in preDeployCommand, not the start command. preDeployCommand runs once per deploy against the new image; startCommand runs every time an instance starts up.
render.yaml
services:
- type: web
name: api
runtime: ruby
buildCommand: bundle install && bundle exec rake assets:precompile
preDeployCommand: bundle exec rails db:migrate
startCommand: bundle exec rails server -b 0.0.0.0 -p $PORT

The whole point of preDeployCommand is exactly this: stuff that needs to happen between build and boot but isn’t part of the running service.

”Detected service running on multiple ports”

A rarer one, but worth knowing.

What you’ll see

Runtime log: multiple listeners
==> Detected service running on port 10000
==> Detected service running on port 8080
==> Routing traffic to port 10000

This isn’t a failure on its own, but it usually means your app is listening on a port other than PORT, often because something else (an admin endpoint, a debug server, a metrics exporter) opens its own listener.

The fix

Make sure the main HTTP listener binds to PORT, and everything else either uses a different port or doesn’t open a public listener at all. Render routes traffic to the first port it detects, which is sometimes the wrong one if startup races aren’t deterministic.

For metrics exporters, use a sidecar pattern (a private service) or scrape from inside the same process at a unique path.

Putting it together: a worked example

A user reports: “I pushed a Node app, the build went green, but the service never goes live. Here’s the log.”

The log
==> Cloning from https://github.com/example/api
==> Checking out commit a8f3c1e in branch main
==> Using Node version 20.11.0
==> Running build command 'npm ci && npm run build'
==> Build successful
==> Running 'node server.js'
Server listening on http://localhost:3000
==> No open ports detected, continuing to scan...
==> Timed out
==> Deploy failed

Walking the method (step 02):

  1. Reproduce: every deploy fails the same way. Deterministic.
  2. Locate the surface: boot phase - build succeeded, ==> Running 'node server.js' ran, but no live status.
  3. First error: No open ports detected. The app says it’s listening on http://localhost:3000 - wrong port (Render wants 10000) and wrong interface (localhost instead of 0.0.0.0).
  4. Hypothesis: server.js hardcodes app.listen(3000, "localhost").
  5. Smallest fix: change to app.listen(process.env.PORT || 10000, "0.0.0.0"). One commit, one redeploy.
  6. Result: ==> Detected service running on port 10000 followed by ==> Your service is live.

Total time from log open to fixed: about three minutes once you know the pattern.

Your service's runtime log shows `Server listening on http://0.0.0.0:3000` followed by `==> No open ports detected, continuing to scan...` repeatedly. Which fix actually addresses the root cause?

What you learned

  • Render's port scanner is looking for a TCP listener on `0.0.0.0:$PORT`. Default `PORT` is `10000`. Both must be right
  • `Server listening on http://localhost:...` plus `No open ports detected` = bound to loopback. Switch to `0.0.0.0`
  • Read `PORT` from the environment in every framework: `process.env.PORT`, `os.environ.get('PORT')`, `ENV.fetch('PORT')`, `os.Getenv("PORT")`
  • Docker boot hangs with no application output usually means missing `CMD`/`ENTRYPOINT` - add one or set `dockerCommand` in the Blueprint
  • Migrations and one-off scripts belong in `preDeployCommand`, not `startCommand`. `startCommand` must be a long-running server
  • When in doubt, test the exact `startCommand` locally with `PORT=10000` before pushing