Render Tutorials
Advanced render.yaml Blueprint patterns

Projects and environments

⏱ 8 min

The default Blueprint pattern is a flat list of services: and databases: at the root. It’s perfect for a single app - until you want a staging copy, or a second app that should live in the same Render project, or env groups scoped to one of those environments.

That’s when you reach for projects + environments.

The two shapes, side by side

flowchart LR
  subgraph flat [Flat layout]
    direction TB
    rs1[services]
    rd1[databases]
    rg1[envVarGroups]
  end

  subgraph nested [Projects + environments]
    direction TB
    p[project: my-app]
    e1[environment: production]
    e2[environment: staging]
    rs2[services]
    rd2[databases]
    p --> e1
    p --> e2
    e1 --> rs2
    e1 --> rd2
  end

Both shapes are valid YAML and both get a green light from render blueprints validate. The difference is what Render does with the resources after a sync.

Flat: one project, no environment story

render.yaml
databases:
- name: app-db
plan: basic-256mb
services:
- type: web
name: api
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString

The web service and database land in a Render project named after your repo. There’s no concept of a second environment - if you want staging, you have to fork the file or duplicate every resource with a -staging suffix.

Nested: one project, many environments

render.yaml
projects:
- name: my-app
environments:
- name: production
databases:
- name: app-db
plan: basic-256mb
services:
- type: web
name: api
runtime: node
plan: standard
buildCommand: npm ci && npm run build
startCommand: npm start
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString
- name: staging
databases:
- name: app-db
plan: basic-256mb
services:
- type: web
name: api
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString

Same names (api, app-db) appear in both environments - that’s intentional. fromDatabase and fromService resolve within the same environment, so production’s api wires to production’s app-db and staging’s api wires to staging’s app-db. No collisions, no duplicated bookkeeping.

When to migrate

Reach for the projects pattern when at least one of these is true:

  • You have two or more services that belong to the same logical app.
  • You want staging and production in the same Blueprint.
  • You want env groups scoped to a single environment (e.g. a staging-only LLM_API_KEY).
  • You want environment isolation so private network traffic can’t cross between staging and production. Available on Professional+ plans.

If your app is a single web service with no staging, the flat layout is fine. Don’t migrate for the sake of migrating.

Anatomy of an environment

Each environment block is a self-contained mini-Blueprint:

FieldWhat it holds
nameThe environment key. Convention: production, staging, preview-base.
servicesSame schema as top-level services. Web, worker, cron, pserv, keyvalue.
databasesSame schema as top-level databases.
envVarGroupsEnv groups scoped to this environment. Production secrets stay out of staging.

envVarGroups can also live one level up - under the project - when you want a group shared across every environment in the project.

render.yaml
projects:
- name: my-app
envVarGroups:
- name: shared-config
envVars:
- key: LOG_LEVEL
value: info
environments:
- name: production
envVarGroups:
- name: production-secrets
envVars:
- key: STRIPE_API_KEY

We dig into env group strategy in the next step. For now, the takeaway is that scoping is a real lever: workspace > project > environment.

The big footgun: don’t define a resource twice

flowchart LR
  topServices[root-level services]
  envServices[environment services]

  topServices -.->|"same name"| envServices
  envServices -.-> conflict["Reconcile churn or<br/>name collisions"]

The fix when you spot this in a real file is mechanical: keep the version inside the environment, delete the root-level copy, run render blueprints validate, then sync.

A worked migration

You already have a flat Blueprint in production. Here’s the diff to add a staging environment:

render.yaml
databases:
- name: app-db
plan: basic-256mb
services:
- type: web
name: api
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString
projects:
- name: my-app
environments:
- name: production
databases:
- name: app-db
plan: basic-256mb
services:
- type: web
name: api
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString
- name: staging
databases:
- name: app-db
plan: basic-256mb
services:
- type: web
name: api
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString

Render reconciles the existing production resources into the new layout - they don’t get destroyed and recreated, as long as the environment name (production) and resource names (api, app-db) match what’s already in the workspace. Staging spins up fresh.

In a projects + environments Blueprint, you have a service named `api` in both `production` and `staging`. Each environment also has a database named `app-db`. The `api` service in `staging` references `app-db` via `fromDatabase`. Which database does it connect to?

What you learned

  • Flat services/databases is fine for single-service apps; reach for projects + environments when you need staging or env-scoped configuration
  • Each environment is a self-contained mini-Blueprint with its own services, databases, and optional env groups
  • `fromDatabase` and `fromService` resolve within the same environment - same names across environments don't collide
  • Never define the same logical resource at both the root and inside an environment