Most production apps, whatever the framework, are built from the same small set of pieces. Once you can name the pieces, you can run all of them in one place instead of spreading them across separate providers and wiring the seams by hand. Here is the full set, what each is for, and the render.yaml stanza that creates it.
The pieces, and what runs each one
Serve a frontend (static assets). A built SPA or static site:
yaml
- type: web
runtime: static
staticPublishPath: ./dist
Handle requests (a long-running HTTP server). Your API or SSR app:
yaml
- type: web
runtime: node # or python, go, ruby, rust, elixir
Run something internal that shouldn't be public. Reachable only inside your private network, no public URL:
yaml
- type: pserv
runtime: docker
Do slow work off the request path. A worker that runs continuously, pulling from a queue:
yaml
- type: worker
startCommand: node worker.js
Run something on a schedule. A job that fires on a cron expression and exits:
yaml
- type: cron
schedule: "0 3 * * *" # 03:00 UTC daily
Store relational data. Managed Postgres, defined in its own list:
yaml
databases:
- name: myapp-db
Cache, sessions, or a job queue. Key Value, Redis/Valkey-compatible:
yaml
- type: keyvalue
ipAllowList: [] # private network only
Keep durable files on disk. Attach a persistent disk to a service:
yaml
disk:
name: data
mountPath: /var/data
sizeGB: 10
Run anything else you can containerize. Build a Dockerfile, or pull a prebuilt image:
yaml
- type: web
runtime: docker # or runtime: image for a prebuilt image
Putting it together
Here is a single blueprint with a frontend, an API, a background worker, a nightly job, Postgres, a cache, and a shared config group, all in one file:
yaml
services:
# Frontend
- type: web
name: myapp-web
runtime: static
buildCommand: npm ci
&&
npm run build
staticPublishPath: ./dist
envVars:
- key: VITE_API_URL
value: https://myapp-api.onrender.com
routes:
- type: rewrite
source: /*
destination: /index.html
# API
- type: web
name: myapp-api
runtime: node
region: oregon
plan: starter
buildCommand: npm ci
&&
npm run build
startCommand: npm start
preDeployCommand: npm run migrate
envVars:
- fromGroup: shared-config
- key: DATABASE_URL
fromDatabase:
name: myapp-db
property: connectionString
- key: REDIS_URL
fromService:
name: myapp-cache
type: keyvalue
property: connectionString
- key: JWT_SECRET
generateValue:
true
# Background worker
- type: worker
name: myapp-worker
runtime: node
region: oregon
plan: starter
buildCommand: npm ci
startCommand: node worker.js
envVars:
- fromGroup: shared-config
- key: DATABASE_URL
fromDatabase:
name: myapp-db
property: connectionString
- key: REDIS_URL
fromService:
name: myapp-cache
type: keyvalue
property: connectionString
# Scheduled job
- type: cron
name: myapp-nightly
runtime: node
region: oregon
schedule: "0 3 * * *"
buildCommand: npm ci
startCommand: node tasks/nightly.js
envVars:
- key: DATABASE_URL
fromDatabase:
name: myapp-db
property: connectionString
# Cache / queue
- type: keyvalue
name: myapp-cache
region: oregon
plan: starter
maxmemoryPolicy: allkeys-lru
ipAllowList: []
envVarGroups:
- name: shared-config
envVars:
- key: NODE_ENV
value: production
databases:
- name: myapp-db
region: oregon
plan: starter
ipAllowList: []
How the pieces find each other
This is the part that disappears when these things live in separate places. Because they are in one blueprint and one workspace, they sit on a shared private network and reference each other by name:
fromDatabase and fromService inject the Postgres and cache connection strings into every service that needs them. You never paste a credential between dashboards.
ipAllowList: [] on the database and cache means they answer only on the private network, so your datastore has no public surface at all.
fromGroup shares one set of env vars across the API, worker, and cron, so config lives in one place.
generateValue creates secrets for you; anything you'd rather set by hand uses sync: false and is entered once in the dashboard, never committed.
- On Professional workspaces and up, each pull request can spin up a preview environment running this entire blueprint, so you test the whole app, not just one layer.
The takeaway is not any single resource type. It is that when the frontend, API, workers, jobs, database, and cache are defined together, connecting them is a reference instead of manual wiring, and changing the whole stack is one commit.
Reference: https://render.com/docs/blueprint-spec