Most teams start with .env files for everything, including production secrets, not because it's the right call but because it's already there — the same file that holds a local database URL is right next to the API keys the deployed service needs too. That works fine for a while, and then quietly stops being fine in ways that are hard to notice until something goes wrong.
What .env files get right
It's worth being honest about why this convention won in the first place: zero setup, no network dependency, works the same offline as online, human-readable, and every language has a library that reads it in about one line of code. For local development — where the worst case of a leaked value is your own test database — that's a genuinely good tradeoff, not a shortcut people should feel bad about taking.
Where a flat file stops scaling
The problems show up specifically once more than one person, one service, or one environment is involved:
- No access control. A
.envfile is all-or-nothing — anyone who can read the file reads every secret in it. There's no way to give a CI job access to the deploy key without also handing it the payment provider's secret key sitting three lines below. - No audit trail. There's no record of who read a value, from where, or when. If a secret turns out to have leaked, you can't look up who actually accessed it before rotating — you just have to assume the worst and rotate everything.
- No rotation mechanism. Rotating a credential means finding and editing every copy of the file by hand, then redeploying everywhere it lives. Nothing enforces that you actually got them all.
- Plaintext at rest. A
.envfile on a server's disk, or copied into a Docker image, sits there unencrypted. Baking one into an image is a specific, well-known trap: even if a later layer deletes it, the layer that added it is still part of the image's history and stays recoverable — the same underlying issue covered for buildARGs in Docker Best Practices. - Sprawl. Once a production
.envhas been emailed, Slacked, or copied to a few laptops to unblock someone, there's no realistic way to know where all the copies ended up, let alone revoke them individually.
What a secrets manager actually adds
Strip away the marketing and a dedicated secrets manager (Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, Doppler, and similar tools all fit this category) earns its place by directly addressing that list:
- Fine-grained access control — a service or role gets scoped access to exactly the secrets it needs, via IAM policy or an equivalent, not blanket access to a whole file.
- Audit logging — every read is attributable to a specific identity, so a suspected leak can actually be investigated instead of just assumed.
- Rotation — some integrations rotate credentials automatically (a database password on a schedule, with connected services picking up the new value without a manual edit-and-redeploy cycle).
- Versioning — a bad value can be rolled back to the previous version instead of requiring someone to remember what it used to be.
- Encryption at rest, with the encryption keys managed separately from the data they protect, rather than a file sitting in the clear on whatever disk it happens to be on.
Your platform's env var settings aren't a .env file
It's worth separating two things that get conflated: the specific risks above are about .env as a file — plaintext, copyable, no access control baked in. Setting environment variables through your hosting platform's own dashboard (Vercel, Heroku, Render, and similar all offer this) is a meaningfully different, generally reasonable middle ground — those values are typically encrypted at rest and gated behind the platform's own access control on your account, even though your application code still just reads process.env.WHATEVER exactly the same way either way. Plenty of teams never need to go further than that.
A practical decision guide
.env locally plus your platform's built-in environment variable settings in production is enough for a solo developer or small team without regulatory requirements. Reach for an actual secrets manager once any of these become true: multiple services need different, narrower access to overlapping sets of secrets; a compliance framework (SOC 2, HIPAA, PCI-DSS) requires an audit trail you can hand to an auditor; you need credentials that rotate on a schedule without a deploy; or you regularly need to grant and then fully revoke a contractor's or a CI job's access to a specific subset of secrets — something a shared file structurally can't do.
The migration doesn't have to be all-or-nothing
Adopting a secrets manager doesn't usually mean ripping process.env calls out of application code. The common pattern is the secrets manager becoming the source of truth, with a startup step that pulls the current values and exposes them as ordinary environment variables before the app runs — the application code doesn't change at all, only where the values originate from. .env often survives as the local development fallback even after production has moved on, which is a perfectly reasonable place for it to keep living.
Try it yourself
Whichever side of this you're on, keeping what you have correct still matters: .env Validator catches the dotenv parsing gotchas that fail silently, Environment Variable Diff catches a template that's drifted out of sync with what's actually deployed, and — specifically for the CI side of secrets handling — GitHub Actions Secrets Checker traces where a workflow's secrets can leak. All three run entirely in your browser.