Incident timeline

    ← All articles

    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

    Plain settingsprongo.yaml, in git Service linksgenerated: {{ api.url }} Secretsencrypted store, set once your service all arrive as process.env
    Three different homes, one place your code reads them from.
    KindExampleWhere it lives
    Plain settingsNODE_ENV=production, POSTGRES_DB=shopCommitted in your deploy config
    Service linksthe API’s addressGenerated, never typed
    Secretspasswords, API keys, tokensEncrypted 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

    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.example with placeholders and no real values.
    • Add .env to .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:PORT or 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.