Why .env files don't belong in production
Short answer: a .env file on a developer laptop is a handy format for running
things locally, but a .env file on a production server is a secret stored in
plaintext on disk, whose copies you no longer control. It ends up in backups,
image layers, CI artifacts and home sync folders; any process on the host can
read it; and when it leaks, the file itself will not tell you. The fix is not
“hide the file better” but removing it from the deploy entirely: at startup the
service fetches its values from a secrets manager under its own machine
identity, and every such read stays in the log.
A file on disk cannot keep quiet
The problem with .env in production is not the format — it is the properties
of a file as a medium:
- Copies multiply on their own. The file lands in host backups, disk
snapshots, a Docker image layer if
.dockerignoremissed it, and CI artifact archives. Deleting the original does not delete the copies. - Any process on the host can read it. Mode
600protects against other users, not against a process running as the same one — which is exactly who runs next to your application. - A file does not record its readers. Nobody knows who opened it and when. Incidents get investigated from circumstantial evidence, if at all.
“We use environment variables, not a file” is not an answer
Variables set in an orchestrator or CI get rid of the file, not the problem. A
value in the process environment is visible through /proc/<pid>/environ to
any process with the right privileges, leaks into crash dumps and error
reports, and shows up in the orchestrator UI for everyone with project access.
Above all, it is still handed out by a human and stored by a third party (the
CI, the PaaS) in the clear: the secrets manager is replaced by deploy settings
that have neither read auditing nor rotation.
What delivery without a file looks like
In env-master a service is a first-class entity with its own machine identity. At startup the SDK authenticates with client credentials, gets a short-lived token and fetches its values — ciphertext plus a key wrapped for that specific identity. Decryption happens in the service's own memory: the machine path is honestly client-side, and the server never produces the plaintext in this scenario (where the boundary actually lies is written on the security page and in the threat model — without the words “zero-knowledge”).
From then on the values live only in process memory: not on disk, not in the
image, not in CI settings. Every read lands in the
audit log, an identity can be scoped down to a
list of keys (DB_*, not the whole environment), and a value marked
restricted is never shown to a person at all — only the service fetches it.
How to migrate without stopping the service
Moving off .env does not take a rewrite in one evening:
- Create a project and move the values.
envmaster import .env --dry-runshows the plan before writing anything and flags what looks like a secret (see the quickstart). - Issue the service an identity scoped to its environment and key set.
- Connect the SDK — it replaces reading
process.envwith a client that keeps values in memory and renews its token itself. - An intermediate step for legacy:
envmaster run -- <command>hands values to a child process through the environment without writing them to disk — the application needs no changes at all. - Delete the file from servers, CI and backups — nobody needs it anymore.
To make sure nothing was lost along the way, schema validation checks required keys against what the store actually holds and stops a deploy before runtime, not after.
The emergency exit always stays open: dumping an environment back into a plain
.env is one CLI command. A store you cannot leave is not a store — it is a
hostage situation.