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

  1. Git. One git add . and it is in history, and removing it later doesn’t undo the exposure. What to do when it happens.
  2. 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).
  3. 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).
  4. 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 run fetches 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 .env has no such dependency.
  • Local dev convenience. Many teams keep a tiny, secret-free .env.example, or use dynv run locally 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_*, Vite import.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.

New here? Start here for the guides in a sensible order.