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.1instead of0.0.0.0. - The Dockerfile has no
CMDso nothing starts at all. - The
startCommandruns 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:
- Bind to
0.0.0.0, not127.0.0.1orlocalhost. Render’s port scanner runs in a separate network namespace from your process; a process listening only on loopback is invisible. - Listen on the
PORTenv var if it’s set. Render injectsPORT=10000by default for web services. If your code hardcodes3000, Render will scan10000and 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
==> Running 'npm start'> app@1.0.0 start> node server.jsServer listening on http://localhost:3000==> No open ports detected, continuing to scan...==> No open ports detected, continuing to scan...==> Timed out==> Deploy failedThe 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 says | Cause |
|---|---|
Server listening on http://localhost:3000 | Bound to loopback and wrong port - both problems |
Server listening on http://0.0.0.0:3000 | Right interface, wrong port - Render is scanning 10000 |
Server listening on http://localhost:10000 | Right 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.
const port = process.env.PORT || 10000;app.listen(port, "0.0.0.0", () => { console.log(`Listening on 0.0.0.0:${port}`);});import osport = int(os.environ.get("PORT", 10000))app.run(host="0.0.0.0", port=port)# In your startCommand:# uvicorn main:app --host 0.0.0.0 --port $PORT# In your startCommand:# gunicorn myproject.wsgi --bind 0.0.0.0:$PORT# config/puma.rbport ENV.fetch("PORT", 10000)bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 10000)}"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:
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 responsecurl -v http://127.0.0.1:10000/# Should also work - if it does but the deploy still fails, see belowIf 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
==> 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:
bundle exec rails server -b 0.0.0.0 -p $PORT# config/puma.rb - use the *string* form, not the array formbind "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
==> Image built successfully==> Deploying...(...hangs indefinitely...)==> Deploy timed out==> Deploy failedNo 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:
FROM node:20-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --only=productionCOPY . .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.jsOr set dockerCommand in your Blueprint:
services: - type: web name: api runtime: docker dockerfilePath: ./Dockerfile dockerCommand: node dist/server.jsHanging start commands
The third common variant: your startCommand runs something interactive that waits for stdin forever.
What you’ll see
==> Running 'rails console'Loading production environment (Rails 7.1.0)==> No open ports detected, continuing to scan...==> No open ports detected, continuing to scan...==> 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:
- The
startCommandmust be a server, not an interactive REPL or one-off script. - Migrations belong in
preDeployCommand, not the start command.preDeployCommandruns once per deploy against the new image;startCommandruns every time an instance starts up.
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 $PORTThe 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
==> Detected service running on port 10000==> Detected service running on port 8080==> Routing traffic to port 10000This 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.”
==> 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 failedWalking the method (step 02):
- Reproduce: every deploy fails the same way. Deterministic.
- Locate the surface: boot phase - build succeeded,
==> Running 'node server.js'ran, but no live status. - First error:
No open ports detected. The app says it’s listening onhttp://localhost:3000- wrong port (Render wants 10000) and wrong interface (localhostinstead of0.0.0.0). - Hypothesis:
server.jshardcodesapp.listen(3000, "localhost"). - Smallest fix: change to
app.listen(process.env.PORT || 10000, "0.0.0.0"). One commit, one redeploy. - Result:
==> Detected service running on port 10000followed by==> Your service is live.
Total time from log open to fixed: about three minutes once you know the pattern.
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