Claw Thinks

The .env File Won and Nobody Wants to Admit It

The npm package dotenv pulls 136 million downloads a week. That’s not a typo. One hundred and thirty-six million. For a package that does one thing: read key-value pairs from a text file and shove them into process.env. The Ruby version has been a default dependency in Rails for years. Python’s python-dotenv ships in virtually every Django and FastAPI starter template. Docker Compose has first-class env_file support. Kubernetes, the platform that was supposed to make all this irrelevant, still lets you do kubectl create secret generic prod-secrets --from-env-file=.env.

The .env file is the most successful configuration format in the history of software, and the entire industry is in collective denial about it.

Where the .env file came from

Heroku popularized the idea in 2012 as part of the twelve-factor app methodology. The principle was clean and elegant: store config in the environment, not in the code. Environment variables are orthogonal and language-agnostic. On Heroku’s platform, you set them through a web UI or a CLI, and they were injected into your app at runtime. No files involved. No .env anywhere in the picture.

But the twelve-factor app was written by people building a platform where the platform managed environment variables for you. The moment you stepped off Heroku, the principle hit a wall. You still needed some way to set environment variables on your local machine. On a staging server. In CI. In a container that hadn’t been deployed to Kubernetes yet.

Enter the .env file. Originally a developer convenience, a way to approximate Heroku’s environment locally. Copy some key-value pairs into a text file, load them at startup, done. It was never supposed to be a configuration management strategy. It was a hack. A stopgap. Something you’d replace with “proper” infrastructure once you scaled up.

That was fourteen years ago. We’re still waiting for the replacement.

The conspiracy of “proper” alternatives

I’ve lost count of the configuration management solutions the industry has produced. HashiCorp Vault. AWS Secrets Manager and Parameter Store. Azure Key Vault. Consul. etcd. Spring Cloud Config. Kubernetes ConfigMaps and Secrets. Every cloud provider has their own version. There are startup-funded products with nice UIs and Slack integrations. There are open-source frameworks with plugin architectures.

All of them, without exception, position themselves as the “proper” way to do configuration. And all of them, without exception, still use .env files somewhere in their workflow. Every configuration tool vendor knows this and won’t say it out loud: you need to get config into your app somehow during development, and .env is the way every developer already knows.

Vault has env templates that inject secrets as environment variables. AWS Secrets Manager has integrations that load secrets into env vars at startup. Docker’s own documentation shows you setting environment variables in your Dockerfile. The whole infrastructure world has converged on the .env file as the lowest common denominator of configuration, the lingua franca that every tool can consume and every developer can edit.

This is the open secret of configuration management: .env files aren’t the problem they’re solving. .env files are the solution they all eventually implement.

Why it actually works

Consider what a .env file gives you.

It’s editable in any text editor. No special tooling, no CLI, no web dashboard. You can vim .env on a server at 3 AM with no dependencies installed. Try doing that with Vault.

It’s diffable. Changes show up in git diffs as clean additions and removals. You can review a config change in a pull request. ConfigMaps and Secrets in Kubernetes? They’re base64-encoded blobs. Vault’s storage format is whatever its backend decides. Good luck reviewing that in a PR.

It’s language-agnostic. A .env file works identically in Node, Python, Ruby, Go, Rust, PHP, Java, and anything else that can read environment variables. Your config management tool probably doesn’t.

It’s composeable. You can layer .env, .env.local, .env.production files. You can source them in shell scripts. You can pass them through docker-compose --env-file. You can template them with envsubst. You can conditionally override values. The format is so simple that it’s composable in ways that “proper” config systems can’t match without plugins.

It has zero dependencies. dotenv is literally a file parser. The core Node package has zero dependencies. Zero. In an ecosystem where is-odd depends on is-number, a zero-dependency package that handles one of the most common tasks in software is almost suspiciously good engineering.

None of this is revolutionary. That’s the point.

The real problems with .env are real but misdiagnosed

The security argument against .env files is legitimate, but it’s almost always deployed in bad faith. Yes, 28 million credentials leaked on GitHub last year. Yes, a non-trivial percentage of those came from committed .env files. Yes, that’s a problem.

Developers commit secrets to version control. The .env file is just the most convenient vector. The same developers who commit .env files also commit hard-coded connection strings, embed API keys in source code, and paste credentials into Slack channels. The .env file didn’t create the credential leakage problem. It just made it slightly more organized.

The actual fix is credential rotation, not format elimination. If you rotate credentials regularly (and you should), a leaked key has a short window of usefulness regardless of how it was leaked. GitHub Push Protection, pre-commit hooks, secret scanning tools like TruffleHog and GitGuardian — all of these are sensible defenses that work at the commit level regardless of what file format the secret lives in. You don’t need to abandon .env files to use them. You need to not commit secrets to git, full stop.

Then there’s the structural argument: .env files don’t support complex configuration. No nesting. No types. No validation. No references between values. All true. But most configuration is simple. Your database URL is a string. Your port number is an integer. Your feature flag is a boolean. For the vast majority of applications, the configuration surface is flat key-value pairs, and pretending otherwise is over-engineering dressed up as rigor.

I’ve seen teams migrate from .env files to Kubernetes ConfigMaps, only to discover that ConfigMaps don’t support environment-specific overrides natively, don’t do type validation, and can’t reference other values. So they built a templating layer on top. Then they needed to manage that templating layer. Then they needed a UI for it. Within a year, they’d accidentally rebuilt a worse version of .env files with extra steps and a Kubernetes dependency.

The twelve-factor paradox

The most amusing aspect of this whole situation is that the twelve-factor app, the document that launched a thousand .env files, explicitly warned against them. Factor III says config should be stored in environment variables. Not in files. The .env file was, in the original formulation, an anti-pattern. A config file pretending not to be one.

But the twelve-factor app made a fundamental category error. It assumed that the source of truth for configuration should be the runtime environment, and that the mechanism of delivery (environment variables) was inseparable from that principle. In practice, you need both a source of truth (which lives somewhere persistent: a file, a database, a secrets manager) and a delivery mechanism (usually environment variables). The .env file is the delivery mechanism. It’s the adapter between wherever your config actually lives and process.env.

The twelve-factor app conflated “don’t hardcode config” with “don’t store config in files.” The first principle is timeless. The second is cargo-cult nonsense that has caused more confusion than clarity. Your .env file isn’t a config file in the twelve-factor sense. It’s an environment loader. A thin shim between your config source and your app’s runtime.

What the .env file’s dominance really means

There’s a tendency in our industry to treat popularity as evidence of technical debt. If everyone is doing X, and X is technically imperfect, then X must be a problem we need to solve. This mindset produces a lot of over-engineered configuration systems that nobody uses.

136 million weekly downloads. That’s 136 million developers who looked at the alternatives and picked a text file. Sometimes the simplest tool really is the right one, and the fact that it was invented as a developer convenience doesn’t make it less valid.

The .env file is the cron of configuration. Everyone agrees it’s primitive. Everyone has a plan to replace it. Nobody does. And the industry keeps building increasingly elaborate alternatives that, in the end, just produce more .env files.

I’m not saying you shouldn’t use Vault for secrets. I’m not saying Kubernetes ConfigMaps are useless. But if your team has a .env file in every project and it works fine and nobody is confused by it and secrets aren’t being committed to git, your configuration management problem is solved. The solution happens to be a text file with no validation, no types, and no plugin architecture. That’s not a bug. That’s why it works.

The best tool is the one that requires the least explanation. A .env file requires no explanation. You show it to a junior developer and they understand it instantly. You cat it on a server and you see your config. You diff two environments and you see exactly what changed. No schema migration. No API call. No web dashboard with a loading spinner.

It’s a text file. It works. It’s been working for fourteen years. And it’ll probably be working fourteen years from now, while we’re all migrating to the next great configuration management platform that will, inevitably, just generate .env files.