Render Tutorials
Advanced render.yaml Blueprint patterns

envVarGroups patterns

⏱ 7 min

By the third service that needs LOG_LEVEL=info and DATADOG_API_KEY, you’re tired of pasting them into every envVars: block. envVarGroups is the fix: declare the bundle once, attach it everywhere with a single line.

What a group looks like

render.yaml
envVarGroups:
- name: shared-config
envVars:
- key: LOG_LEVEL
value: info
- key: NODE_ENV
value: production
- key: SENTRY_DSN
services:
- type: web
name: api
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
envVars:
- key: PORT
value: "3000"
- fromGroup: shared-config

fromGroup doesn’t take a key. It pulls every var in the named group into the consuming service. You can also list multiple groups on a service - they merge in order.

Three scopes, same syntax

Where you put the envVarGroups: block decides who can see it.

ScopeWhere it goesVisible to
WorkspaceCreated in the Render Dashboard, not in render.yamlEvery Blueprint and service in the workspace
ProjectUnder projects[].envVarGroupsEvery environment inside that project
EnvironmentUnder projects[].environments[].envVarGroupsOnly services in that environment
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
services:
- type: web
name: api
runtime: node
plan: standard
buildCommand: npm ci && npm run build
startCommand: npm start
envVars:
- fromGroup: shared-config
- fromGroup: production-secrets

The web service sees both groups: shared logging config from the project level, and production-only Stripe credentials scoped to the environment. Staging would see shared-config but not production-secrets.

Config vs secrets - keep them separate

A useful convention: one group for config, one group per secret tier.

render.yaml - recommended split
projects:
- name: my-app
envVarGroups:
- name: shared-config
envVars:
- key: LOG_LEVEL
value: info
- key: FEATURE_FLAGS_URL
value: https://flags.example.com
environments:
- name: production
envVarGroups:
- name: production-secrets
envVars:
- key: STRIPE_API_KEY
- key: SLACK_WEBHOOK
- name: staging
envVarGroups:
- name: staging-secrets
envVars:
- key: STRIPE_API_KEY
- key: SLACK_WEBHOOK

The win: editing a config value (say, raising LOG_LEVEL to debug while you chase a bug) doesn’t touch any group that holds secrets. PR diffs stay tight, and reviewers can wave through config-only changes without re-auditing every secret.

The sync: false rule that bites every first-time deploy

If you need a secret value that Render shouldn’t store in the Blueprint:

  • In a service’s own envVars:sync: false works as expected. Render prompts on initial sync.
  • In an envVarGroup → declare the group with the keys but no values, then fill them in via the Render Dashboard before you sync the Blueprint.

That second flow is the same one used by the sf-pulse-env group in the Deploy SF Pulse on Render tutorial - you create the group in the Render Dashboard first, fill in the secret, then run the Blueprint sync. Get the order wrong and the sync fails because the group doesn’t exist yet.

Lifecycle: the order matters

flowchart LR
  s1["1. Create env group<br/>(the Render Dashboard or Blueprint)"]
  s2["2. Fill in secret values<br/>(Render Dashboard)"]
  s3["3. Sync Blueprint<br/>(services attach via fromGroup)"]
  s4["4. Edit values later<br/>(Render Dashboard, no resync needed)"]

  s1 --> s2 --> s3 --> s4

Once the group exists, changes to its values propagate to attached services without a Blueprint resync. That’s why teams that reach the Professional plan often move all secrets into env groups - rotating a key takes one Render Dashboard edit instead of a redeploy of every consumer.

Resolution order when keys collide

What happens if shared-config and production-secrets both declare STRIPE_API_KEY, and the service also declares it directly?

The order, from least to most specific:

flowchart LR
  g1["fromGroup (declared first)"]
  g2["fromGroup (declared later)"]
  svc["service-level envVars[].key"]

  g1 -->|"overrides"| g2 -->|"overrides"| svc -->|"wins"| final["Final value"]

Service-level wins, then later groups override earlier ones. Practical advice: don’t rely on overrides. Pick one home for each key. Overrides are a debugging nightmare three months from now.

You have a service that lists `- fromGroup: shared-config` and `- fromGroup: production-secrets`, in that order. Both groups declare `LOG_LEVEL`. Which value does the service see?

What you learned

  • `envVarGroups` declare reusable bundles of env vars; services attach with `- fromGroup: <name>`
  • Three scopes - workspace (Render Dashboard), project, environment - with progressively narrower visibility
  • `sync: false` works on a service but is silently invalid inside a group; pre-fill those values in the Render Dashboard before syncing
  • Service-level envVars override later groups, which override earlier groups - don't rely on this; declare keys in one place