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`.
npx skills add https://github.com/ericrisco/rsc-harness --skill render
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.
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.
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.
**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.
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.
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 }
**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.
// 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");
# 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.
**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.
preDeployCommand — runs once, before the new instance starts serving traffic. Putmigrations 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 itpasses, 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).
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 |
schedule: (standard cron expression) is required for type: cron.worker withits own scheduler/queue, not a cron. A long cron fails silently mid-job — partial work,
no clean error.
This is the concrete Render surface, not capacity strategy (that's scaling).
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-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 |
../postgresdb/SKILL.md. This skill only*provisions and connects* Render's managed Postgres.
Dockerfile Render consumes via runtime: docker → docker.domains-dns. (The Render domains:block and verification stay here.)
../deployment/SKILL.md andship. This skill is Render config and platform mechanics.
github-actions. Renderauto-deploys on push; only reach for Actions when you genuinely need it.
scaling: knobs → scaling.../fly-io/SKILL.md, railway, vercel,../netlify/SKILL.md, coolify, digitalocean.
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.
Take ericrisco/render from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip.
Without those the skill loads but fails at the first command.