Guide
How to share environment variables and secrets between services in a monorepo
Stop copy-pasting credentials per service. Separate plain config from secrets, generate service URLs, and set each secret once.
· 3 min read
In short
- Split config into three kinds: plain settings, service links and secrets
- Never hardcode another service's address, use {{ api.url }}
- Set each secret once, outside git, and rotate it in one place
In a single-app project, environment variables are a minor chore. In a monorepo they become a real source of bugs: the API and a background worker both need DATABASE_URL, the frontend needs the API’s address, and three people each have a slightly different .env on their laptops.
The fix is to stop treating “environment variables” as one bucket. There are three different kinds, and each one has a different home.
The three kinds of config
| Kind | Example | Where it lives |
|---|---|---|
| Plain settings | NODE_ENV=production, POSTGRES_DB=shop | Committed in your deploy config |
| Service links | the API’s address | Generated, never typed |
| Secrets | passwords, API keys, tokens | Encrypted store, set once |
1. Plain settings go in the config file
Anything you would be fine publishing belongs in version control next to the code, so changes are reviewed and reproducible:
services:
api:
build: ./apps/api
env:
NODE_ENV: production
2. Service links are generated, never hardcoded
The most common monorepo mistake is typing the API’s URL into the frontend’s config. That address differs between production, a preview of a feature branch, and your laptop. Use a reference and let the platform resolve it:
services:
web:
build: ./apps/web
depends_on: [api]
env:
API_URL: "{{ api.url }}"
Because web depends on api, the platform also knows to deploy api first, so the address exists by the time web starts. The details are in the config reference.
3. Secrets are set once, outside git
Passwords, API keys and database URLs with credentials must never be in the repo. Add them once in the dashboard under the project’s secrets. They are stored encrypted and injected into the service as an environment variable with the same name, so your code just reads process.env.DATABASE_URL.
Setting a secret once at project level, rather than per service, is what removes the drift: there is exactly one place to rotate it. The secrets docs cover how scoping works and the security page explains how they are stored.
A checklist you can apply today
- Keep an
.env.examplewith placeholders and no real values. - Add
.envto.gitignore, in the root and in every service folder. - If a value would hurt if it appeared on GitHub, it is a secret.
- Replace every hardcoded
localhost:PORTor internal hostname with a service reference. - Rotate a secret in one place and redeploy, rather than editing several services.
The mistake that leaks credentials
A frequent trap is putting DATABASE_URL: postgres://user:password@db/shop in the yaml “just to get it working”. It will be in git history forever, even after you delete it. If that has happened, rotate the credential rather than just removing the line.
Next step
If you are setting up a multi-service repo from scratch, read how to deploy a monorepo with multiple services for the whole flow end to end.
Frequently asked questions
How do I share environment variables between services in a monorepo?
Put non-sensitive settings in your deploy config, link services with generated URLs instead of hardcoded hosts, and store real secrets once in your platform's encrypted secret store so every service that needs them receives the same value.
Should I commit a .env file in a monorepo?
Commit an .env.example with placeholder values, never the real .env. Real values belong in an encrypted secret store or your platform's dashboard.
What is the difference between a secret and an environment variable?
Every secret is delivered to your app as an environment variable, but not every environment variable is secret. NODE_ENV=production is plain config. A database password is a secret and must be stored encrypted and kept out of git.
How should one service reference another service's address?
Never hardcode it. Use a generated reference such as {{ api.url }} so the address stays correct across production, previews and local environments.