Use when deploying or fixing an app on Render — web services, background workers, cron jobs, private services, managed Postgres and Key-Value, and especially the render.yaml Blueprint. Covers deploys that fail with no open ports detected, 502s on first deploy, free-tier cold starts, and a free Postgres about to expire with its data. NOT the generic ship and release flow (that is `deployment`), NOT another PaaS such as `railway` or `fly-io`.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add ericrisco/rsc-harness --skill render --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Render?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ericrisco-render)More formats (shields.io, HTML) on the badges page.
---
name: render
description: "Use when deploying or fixing an app on Render — web services, background workers, cron jobs, private services, managed Postgres and Key-Value, and especially the render.yaml Blueprint. Covers deploys that fail with no open ports detected, 502s on first deploy, free-tier cold starts, and a free Postgres about to expire with its data. NOT the generic ship and release flow (that is `deployment`), NOT another PaaS such as `railway` or `fly-io`."
tags: [render, paas, deployment, render-yaml, cron, background-worker, infrastructure-as-code]
recommends: [deployment, postgresdb, docker, domains-dns, github-actions, scaling]
origin: risco
---
# Render — make this repo deploy correctly, wired as code
Take any repo and make it deploy on **Render** (render.com) in one pass: pick the right
service type, declare everything in a version-controlled `render.yaml` Blueprint, bind the
port the way Render expects, and wire `DATABASE_URL` / secrets across services so they
survive rotation. Steer clear of the four traps that eat first deploys: the
"no open ports detected" 502, the free-tier spin-down, the 30-day free-Postgres expiry,
and the 12-hour cron kill.
```text
pick service type → render.yaml at repo root → bind 0.0.0.0:$PORT → wire env across services → push (auto-deploy)
│ │
web|worker|cron|pserv|static|keyvalue full key surface → references/blueprint-reference.md
```
Facts here are dated to **2026-06-02** against render.com/docs. Render ships changes; if a
key or limit looks off, confirm against the live Blueprint spec before betting a deploy on it.
## The one decision that decides everything: service type
Pick the type **before** you write a line of YAML. The type sets the billing model, whether
the process gets a public URL, and whether Render expects it to bind a port. Choosing wrong
means a worker that never starts because Render waited for a port, or a 12-hour job that dies
silently as a cron.
| The process… | Type | `runtime` required? | Gets a public URL? |
| --- | --- | --- | --- |
| serves HTTP/WebSocket traffic | `web` | yes | yes |
| runs forever, no inbound URL (queue consumer, Celery) | `worker` | yes | no |
| runs, does work, exits — on a schedule | `cron` | yes | no |
| internal-only API, reachable only inside Render's network | `pserv` | yes | no (internal host only) |
| pre-built static assets (SPA, docs) | `web` + `runtime: static` | yes (`static`) | yes |
| cache / queue / Redis-compatible store | `keyvalue` | **no** | no (internal) |
`runtime` enum (everything except `keyvalue`): `node`, `python`, `docker`, `image`,
`static`, `go`, `ruby`, `elixir`, `rust`.
## Blueprint-first: everything lives in render.yaml
**Rule: declare every service, database, and env var group in `render.yaml` at the repo
root. Touch the dashboard only for `sync: false` secrets and one-off debugging.** Why: the
Blueprint is the reproducible, reviewable source of truth — it powers preview environments
and a clean re-deploy, while dashboard-only config is invisible state that drifts and can't
be code-reviewed.
Top-level keys: `services`, `databases`, `envVarGroups`, `projects`, `previews`.
## Minimal correct render.yaml (annotated)
A multi-service app — a Node web service, a Python worker, a nightly cron, a Postgres db,
and a shared env group. Every load-bearing key is commented.
```yaml
databases:
- name: app-db
plan: starter # NOT free for anything you care about — free expires in 30 days
postgresMajorVersion: "17"
region: frankfurt
envVarGroups:
- name: app-shared
envVars:
- key: LOG_LEVEL
value: info
- key: SENTRY_DSN
sync: false # prompt once at setup; never stored in git
services:
- type: web
name: api
runtime: node
region: frankfurt
plan: starter # $7/mo — avoids the free-tier 15-min spin-down on an API
buildCommand: npm ci && npm run build
startCommand: npm start # MUST bind 0.0.0.0 and read $PORT — see next section
healthCheckPath: /healthz # gates zero-downtime rollout; new instance must pass first
preDeployCommand: npm run migrate # runs before the new instance serves traffic
autoDeployTrigger: commit # commit | checksPass | off
envVars:
- fromGroup: app-shared
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString # never hardcode the URL
- key: SESSION_SECRET
generateValue: true # Render generates a random secret
- type: worker
name: jobs
runtime: python
plan: starter
buildCommand: pip install -r requirements.txt
startCommand: celery -A app worker -l info # no port — workers don't bind one
envVars:
- fromGroup: app-shared
- key: DATABASE_URL
fromDatabase: { name: app-db, property: connectionString }
- key: API_URL
fromService: # reference another service
name: api
type: web
property: hostport
- type: cron
name: nightly-cleanup
runtime: python
schedule: "0 3 * * *" # required for cron; 03:00 UTC daily
buildCommand: pip install -r requirements.txt
startCommand: python -m app.cleanup
envVars:
- key: DATABASE_URL
fromDatabase: { name: app-db, property: connectionString }
```
## Port binding — do this first (the #1 first-deploy failure)
**Rule: a `web` service MUST listen on host `0.0.0.0` and read the `PORT` env var (Render
sets it, default `10000`).** If Render detects no bound port within its window, the deploy
**fails with "no open ports detected"** and visitors get a **502**. This is the single most
common first-deploy break — binding `localhost`/`127.0.0.1` or a hardcoded port does it.
```javascript
// Bad — binds the wrong host and ignores Render's PORT → "no open ports detected"
app.listen(3000);
// Good — bind 0.0.0.0 and honor $PORT
app.listen(process.env.PORT || 3000, "0.0.0.0");
```
```bash
# Bad — gunicorn on a fixed local port
gunicorn app:app --bind 127.0.0.1:8000
# Good — bind 0.0.0.0 and Render's $PORT
gunicorn app:app --bind 0.0.0.0:$PORT
```
Only `web` services need this. A `worker`/`cron` that tries to bind a port is fine but
pointless; a `web` service that doesn't is broken.
## Wiring env vars across services
**Rule: never hardcode `DATABASE_URL`, `REDIS_URL`, or shared secrets as literal `value:`
strings. Reference the resource** so the value survives a rotation, recreate, or region
move, and so secrets never land in git.
| Form | Use it for |
| --- | --- |
| `fromDatabase: { name, property: connectionString }` | the Postgres connection string |
| `fromService: { name, type, property }` | another service's host/port/URL (`property: hostport`, `host`, or `port`) |
| `fromGroup: <group-name>` | pull a whole shared env var group |
| `generateValue: true` | a random secret Render generates and stores (session keys) |
| `sync: false` | a secret you type once at setup; not stored in the repo |
For a Key Value store, reference its connection string the same way you reference Postgres,
via `fromService` against the `keyvalue` service.
## Migrations and zero-downtime deploys
- `preDeployCommand` — runs **once**, before the new instance starts serving traffic. Put
migrations here, not in `startCommand` (a startCommand migration runs on every instance
and races under multiple replicas).
- `healthCheckPath` — Render polls it on the new instance and only shifts traffic once it
passes, giving zero-downtime rollout. Point it at a route that checks real readiness.
- `autoDeployTrigger` — `commit` (deploy every push), `checksPass` (wait for CI status),
or `off` (manual / deploy-hook only).
## Free-tier traps
Render's free tier is generous for hobby work and a landmine for anything you care about.
| Trap | What happens | Fix |
| --- | --- | --- |
| Free web spin-down | after **15 min** of no inbound traffic the instance sleeps; next request waits **30–60s** to wake | Starter at **$7/mo** per service |
| 750 free instance-hrs/mo | shared across the workspace; spun-down time doesn't count toward it | budget it, or pay Starter |
| Free Postgres expiry | **deleted 30 days after creation** (14-day grace to upgrade), all data gone; only **one** free Postgres per workspace | Starter Postgres ($7/mo) from day one for anything real |
| Free Key Value | no disk persistence — data is lost on restart | paid plan if you need durability |
## Cron specifics
- `schedule:` (standard cron expression) is **required** for `type: cron`.
- Render guarantees **at most one active run at a time** — runs don't overlap.
- **A run is killed after 12 hours.** Anything that can exceed that must be a `worker` with
its own scheduler/queue, not a cron. A long cron fails silently mid-job — partial work,
no clean error.
## Scaling and disk knobs
This is the concrete Render surface, not capacity strategy (that's `scaling`).
```yaml
services:
- type: web
name: api
runtime: node
scaling:
minInstances: 1
maxInstances: 4
targetCPUPercent: 70
targetMemoryPercent: 80 # autoscale between min/max on CPU/mem
disk:
name: data
mountPath: /var/data
sizeGB: 10 # a disk PINS the service to ONE instance — blocks horizontal scale
```
Use `numInstances` for a fixed replica count instead of `scaling` when you don't want
autoscaling. A persistent `disk` and horizontal scaling are mutually exclusive — pick one.
Regions: `oregon`, `ohio`, `virginia`, `frankfurt`, `singapore`. Plans: `free`, `starter`,
`standard`, `pro`, `pro plus`, `pro max`, `pro ultra`.
## Anti-patterns
| Anti-pattern | Why it bites | Do instead |
| --- | --- | --- |
| Hardcoding a port (`listen(3000)`) | "no open ports detected" → deploy fails / 502 | bind `0.0.0.0` and `$PORT` |
| Hardcoding `DATABASE_URL` | breaks on rotation / db recreate | `fromDatabase` reference |
| A long job as a `cron` | killed at 12h, silent partial work | `worker` + its own scheduler/queue |
| Free Postgres for production | deleted 30 days after creation | Starter ($7/mo) from day one |
| Secrets as literal `value:` in render.yaml | leaked in git history | `sync: false` or `generateValue: true` |
| Migrations in `startCommand` | runs on every instance, races under replicas | `preDeployCommand` |
| Free web service for a real API | cold-start 502s after idle | Starter, or accept it only for a hobby toy |
| Dashboard-only config | invisible drift, no preview envs, no review | declare in `render.yaml` |
## When to hand off
- Schema design, queries, indexing, tuning → `../postgresdb/SKILL.md`. This skill only
*provisions and connects* Render's managed Postgres.
- Writing the `Dockerfile` Render consumes via `runtime: docker` → `docker`.
- Registrar-side DNS records for a custom domain → `domains-dns`. (The Render `domains:`
block and verification stay here.)
- Cross-platform release strategy, promotion, rollback flow → `../deployment/SKILL.md` and
`ship`. This skill is Render config and platform mechanics.
- Build steps Render's native build doesn't run (custom CI) → `github-actions`. Render
auto-deploys on push; only reach for Actions when you genuinely need it.
- Capacity/load strategy beyond the `scaling:` knobs → `scaling`.
- A different PaaS → its own sibling: `../fly-io/SKILL.md`, `railway`, `vercel`,
`../netlify/SKILL.md`, `coolify`, `digitalocean`.
## Full key surface
The exhaustive `render.yaml` key tables per service type, all database keys
(`postgresMajorVersion`, `diskSizeGB`, `readReplicas`, `highAvailability`, `previewPlan`),
every env-var reference form, region/plan enums, and four complete copy-paste recipes
(Next.js web+pg; FastAPI web+worker+cron+keyvalue; Docker monorepo with `rootDir`; static
SPA with `routes` rewrites) live in `references/blueprint-reference.md` — pull it open when
you need a key this body didn't cover.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!