The .env file is over: what replaces it, and how to migrate in ten minutes
Short answer: .env is fine for a throwaway local project and a liability for anything real, because it is a plaintext file that sits next to your code, gets copied, committed, read by tools and shipped by mistake. The replacement is to keep values in a store and hand them to the process when it starts. Migrating takes about ten minutes; below is a tested run, and what you give up.
This is opinionated, and we sell a product that does this, so we state the trade-offs on both sides. The migration commands were run on 2026-11-01 against a local Dynv server, on a deliberately awkward .env (quotes, an export, a # inside a value, an empty value and a multi-line key).
Four ways a .env file leaks
- Git. One
git add .and it is in history, and removing it later doesn’t undo the exposure. What to do when it happens. - AI coding agents. Anything an agent running as you can read, it can read. Deny rules on a file-reading tool don’t cover every shell command (details and sources).
- Packaging and builds. Dotfiles ride along in npm tarballs and Docker images unless explicitly excluded. GitGuardian counted 29 million new hardcoded secrets on public GitHub in 2025 (report).
- Laptops and chat. The file gets emailed, pasted into a ticket, synced to a cloud drive, and every copy is a place to rotate later.
Plus a slower failure: it does not change. Edit the file and nothing running notices; each environment has its own drifting copy.
What replaces it
| Option | How values reach the app | What you get | What you give up |
|---|---|---|---|
| Platform env (your host’s settings page) | Injected by the platform at start | No file; zero setup | Per-platform; no audit trail, rotation or sharing across services; changing a value often means a redeploy |
| A secrets manager (Vault, a cloud provider’s, Doppler, Infisical, and others) | Fetched at deploy or start by an agent, SDK or CI step | Central store, access control, history | Another system to run or pay for; how much the provider can read depends on its design |
| A delivery agent (what Dynv is) | A wrapper starts your app with its variables and updates it when they change | Values never sit in a file; live updates; rotation across services; zero-knowledge storage | You wrap your start command; one more binary in the image |
| Encrypted files in Git (SOPS and similar) | Decrypted at deploy | Versioned with code; no server | You manage keys and rotation; no live updates; teams share the decrypt key |
None is “correct”; it depends on team size and what you must prove to an auditor. The rest of this page uses the third row.
The ten-minute migration
You need the CLI and an account (quickstart, steps 0 to 2). Writing needs your master token from one machine. We assume a service named shop and a .env like this (all values fake):
# app config
PORT=3000
DATABASE_URL="postgres://app:[email protected]:5432/app"
export STRIPE_SECRET_KEY=sk_test_demo_1234567890
SESSION_SECRET='a b # not a comment'
EMPTY=
PRIVATE_KEY="-----BEGIN KEY-----
line1
line2
-----END KEY-----"
1. Preview the import. Nothing is written, and values stay masked:
$ dynv admin import shop .env --dry-run
+ DATABASE_URL (•••• (45 chars))
+ EMPTY (•••• (0 chars))
+ PORT (•••• (4 chars))
+ PRIVATE_KEY (•••• (49 chars))
+ SESSION_SECRET (•••• (19 chars))
+ STRIPE_SECRET_KEY (•••• (23 chars))
diff: 6 added, 0 changed, 0 unchanged, 0 removed
The same dotenv parser handles export, single and double quotes, escapes, inline comments, multi-line quoted values, CRLF and a BOM. In our run SESSION_SECRET='a b # not a comment' kept its #, EMPTY imported as an empty string, and the multi-line key survived as one value of 49 characters.
2. Write it. Without --yes the command refuses to write non-interactively and asks you to confirm in a terminal:
dynv admin import shop .env --yes
# shop -> v3
3. Run the app from the store, with no .env present. Create a service token in the dashboard (it reads this one service and nothing else), then wrap your start command:
export DYNV_URL=https://api.dynv.dev DYNV_TOKEN=dynv_pt_... DYNV_PROCESS=shop
dynv run -- node app.js
We removed .env first and printed what the app saw (values shortened here by the test script):
PORT "3000"
DATABASE_URL "postgres://a"… (45 chars)
STRIPE_SECRET_KEY "sk_test_demo"… (23 chars)
SESSION_SECRET "a b # not a "… (19 chars)
EMPTY ""
PRIVATE_KEY "-----BEGIN K"… (49 chars)
All six variables arrive as normal environment variables, byte-identical in length to the originals. Your code doesn’t change. If DYNV_URL and DYNV_TOKEN are unset, dynv run just starts your command as-is, so you can ship the wrapper first and turn it on later.
4. Only now move .env out of the repository. Keep a copy somewhere safe until you have run the app in every environment. Add .env to .gitignore and your npm files or .dockerignore, and rotate anything that was ever in a committed .env (how): moving the file does not un-leak history.
Coming from another tool? dynv admin import shop --from doppler (also infisical and heroku) reads through that vendor’s locally installed, already-authenticated CLI; per the CLI docs, nothing is sent to Dynv’s server in plaintext. For several environments, import into separate services (shop-staging, shop-prod), and use --shared for a group of variables several services use. --prune removes variables that are absent from the file; in our run it listed five removals in a dry run, so always preview it.
What you gain
- No plaintext file for a commit, an agent, a tarball or a screenshot to carry.
- One place to change and rotate a value across services, with a preview.
- Variables marked hot update without a restart, through the SDK (how); others restart cleanly.
- Dynv’s servers hold only ciphertext; encryption and decryption happen on your machine.
What you give up, honestly
- A dependency at start.
dynv runfetches variables when your app starts. If Dynv is unreachable, a running service keeps its last good values; a restart needs a local cache (--cache) or it fails by default (concepts). A.envhas no such dependency. - Local dev convenience. Many teams keep a tiny, secret-free
.env.example, or usedynv runlocally with a dev service. Don’t put production values on laptops. - Another binary in the image and a start command to wrap.
- The app process still holds its secrets. A dependency or an attacker who runs code inside the process can read its environment. This removes the file, not that exposure.
- Build-time variables stay build-time. Values bundled into browser assets (
NEXT_PUBLIC_*, Viteimport.meta.env) are fixed when the build runs and cannot update live. - A writer on one machine. Writes need the master token from one writer machine, by design; see the CLI docs.
Facts last verified . Spotted an error? Tell us.