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
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-configfromGroup 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.
| Scope | Where it goes | Visible to |
|---|---|---|
| Workspace | Created in the Render Dashboard, not in render.yaml | Every Blueprint and service in the workspace |
| Project | Under projects[].envVarGroups | Every environment inside that project |
| Environment | Under projects[].environments[].envVarGroups | Only services in that environment |
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-secretsThe 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.
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_WEBHOOKThe 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: falseworks 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.
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