Every project I’ve touched in the last decade has a .env file. Sometimes it’s .env.local. Sometimes .env.development. Sometimes it’s the same file, committed to the repo, with a # DO NOT COMMIT comment at the top. That comment has never once stopped anyone.

.env files are a solved problem that everyone keeps solving badly. The file format is fine. The habit of scattering them across projects, laptops, and Docker volumes — and then treating them like source code — is where things go sideways. A committed .env is a leaked secret. A .env emailed to a new teammate is a secret with no single point of revocation. A .env that lives on your laptop for two years is a secret you’ve forgotten about.

Infisical fixes this without asking you to change how your app reads environment variables. The app still calls os.getenv() or process.env. The difference is where the values come from.

How runtime injection works

infisical run is a process wrapper. It fetches secrets from your Infisical project, stuffs them into the environment of a child process, and then gets out of the way. The subprocess — your dev server, your migration script, your build tool — sees them as ordinary environment variables. Nothing in your application code changes.

# instead of: source .env && npm start
infisical run --env=dev -- npm start

The secrets never land in a file. They exist in memory for the duration of that subprocess. When the process exits, they’re gone. If you run the command again tomorrow, they’re fetched fresh.

That’s it. That’s the core of the pitch.

Migrating a project

1. Push your existing secrets into Infisical

If you have a .env file today, Infisical can import it directly:

infisical secrets import --env=dev .env

Verify they arrived:

infisical secrets --env=dev

Then delete the local file. That’s the point of no return, and it’s good.

2. Swap the run command

Replace whatever you use to start your app with infisical run -- <your command>. Examples:

# Node / Next.js
infisical run --env=dev -- npm run dev

# Python / FastAPI
infisical run --env=dev -- uvicorn app.main:app --reload

# Go
infisical run --env=dev -- go run ./cmd/server

# Docker Compose (injects into the compose process, which passes env to containers)
infisical run --env=dev -- docker compose up

Your package.json, Makefile, or whatever you use to start things just gets infisical run --env=dev -- prepended. The app reads process.env / os.environ exactly as before.

3. Targeting secrets in folders

If you organise secrets into folders in Infisical (e.g. /apikeys, /database), use --path to specify which folder to inject from:

infisical run --env=dev --path="/apikeys" -- npm run dev

You can stack --path to pull from multiple folders in one command:

infisical run --env=dev --path="/common" --path="/apikeys" -- npm run dev

When the same secret name exists in more than one of the specified paths, the first --path wins. In the example above, if API_KEY appears in both /common and /apikeys, the value from /common is used.

The default behavior when you omit --path entirely isn’t explicitly documented — it likely fetches from the root, but whether that includes subfolders recursively is unclear. If you’re using folders to organise secrets, use --path explicitly rather than relying on the default.

4. Update your .env.example

You probably have an .env.example that documents which variables are expected. Keep it — it’s useful for onboarding and local tooling that reads it (some frameworks do). But it becomes documentation only, not a secret carrier. Strip all real values; leave only key names and comments:

# .env.example
DATABASE_URL=          # see Infisical project "blog" > dev
REDIS_URL=             # see Infisical project "blog" > dev
API_KEY=               # rotating 30-day token, see rotation runbook

Add .env and .env.local to .gitignore if they aren’t already. Belt and suspenders.

The environment promotion story

The reason to have --env=dev, --env=staging, and --env=prod in Infisical — rather than separate files — is that promotion becomes explicit and auditable. The secret DATABASE_URL has a different value per environment. You don’t hand-edit a file to promote; you just change the flag:

# Run database migrations against staging
infisical run --env=staging -- npm run db:migrate

# Same command, prod — different value injected, same code path
infisical run --env=prod -- npm run db:migrate

No cp .env.staging .env && vim .env, no diff to review before you run it, no .env.bak that someone forgot to clean up.

Team secrets without a shared file

The old workflow: create .env, add it to .gitignore, email it to new teammates, pray it doesn’t end up in Slack.

The Infisical workflow: the secret is in the vault. A new teammate logs in, is added to the project with the right access policy, and runs infisical run --env=dev -- npm start. Done. No file to share, no single teammate who has “the real one,” no revocation problem when someone leaves.

Access is managed through Infisical’s identity and access policies. You can give a developer read access to dev without exposing prod. You can audit who fetched what and when. You can revoke someone’s access from one place.

Docker Compose workflow

Docker Compose has its own .env habit: by default it reads a .env file in the project root and substitutes ${VAR} references throughout compose.yml. Convenient — and exactly the kind of implicit file that quietly accumulates secrets over time.

The replacement is the same pattern as everything else: wrap docker compose up with infisical run. Compose inherits the environment of its parent process, so anything Infisical injects is available for ${VAR} substitution in your compose file.

infisical run --env=dev -- docker compose up

What changes in compose.yml

Most projects start from one of two places. The migration is different depending on which you’re in.

If you use env_file: — the common pattern where Compose reads a .env directly:

# before
services:
  api:
    image: myapp:latest
    env_file:
      - .env

Remove env_file: and replace it with explicit environment: references. Compose will substitute the values from the process environment Infisical populated:

# after
services:
  api:
    image: myapp:latest
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - API_KEY=${API_KEY}

If you already use ${VAR} substitution — no compose file changes needed at all. Delete the .env file, run via infisical run, done.

Either way, the end state is the same: your compose file has no env_file: stanza, no hardcoded values:

services:
  api:
    image: myapp:latest
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - API_KEY=${API_KEY}

  worker:
    image: myapp:latest
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - API_KEY=${API_KEY}

Compose substitutes the values at startup from the process environment Infisical populated. The containers receive the values as normal environment variables. Nothing changes inside the containers.

If multiple services share a large set of variables, YAML anchors keep the compose file tidy without duplicating the list:

x-common-env: &common-env
  DATABASE_URL: ${DATABASE_URL}
  REDIS_URL: ${REDIS_URL}
  API_KEY: ${API_KEY}

services:
  api:
    image: myapp:latest
    environment:
      <<: *common-env

  worker:
    image: myapp:latest
    environment:
      <<: *common-env
      QUEUE_NAME: ${QUEUE_NAME}

One thing to watch: if you have an existing .env file in the project root, Compose will still load it automatically — even when you’re using infisical run. Remove the file (or add it to .gitignore and delete the local copy) to avoid stale values leaking in alongside the Infisical-injected ones. You can also pass --env-file /dev/null as a one-off to suppress the auto-load while you migrate:

infisical run --env=dev -- docker compose --env-file /dev/null up

Authentication for local development

For interactive development on your own machine, the normal infisical login (browser-based OAuth) is fine:

infisical login

This creates a session that lasts a few hours to a few days. When it lapses, log in again. For automation and headless machines — CI, scripts, agents — use a machine identity instead. I covered that in detail in Infisical Machine Identities: Non-Interactive Secrets for CLI Auth.

The division I use:

  • Local dev: interactive login, session-based, human in the loop.
  • Automation: machine identity, Universal Auth, short-lived tokens.

Same secrets vault, different auth paths depending on who (or what) is calling.

Things I ran into

infisical run passes the whole environment through. Secrets are injected on top of your current shell environment, not instead of it. If you have DATABASE_URL already set in your shell, the Infisical value wins — but it can be surprising if you’re not expecting the merge. Use infisical secrets to confirm what’s actually being injected.

EU region flag. If your account is on Infisical’s EU instance, you need --domain="https://eu.infisical.com/api" on every command. The CLI defaults to the US endpoint and the 401s are not helpful:

infisical run --domain="https://eu.infisical.com/api" --env=dev -- npm run dev

I alias this:

alias irun='infisical run --domain="https://eu.infisical.com/api"'
irun --env=dev -- npm run dev

Machine identities need --projectId explicitly. Interactive sessions pick it up from .infisical.json. Machine identities don’t. If you’re scripting infisical run without a human session, pass --projectId or you’ll get a cryptic error. Interactive use in a project directory works without it.

Some frameworks bootstrap before your start command runs. Next.js, for instance, has a next.config.js that runs before next dev fully starts. If that config file tries to read env vars, they’re available — Infisical injects before the process starts. But if a build step outside your infisical run wrapper reads env vars, it won’t see them. Wrap everything you need injected.

The honest take

infisical run is a thin wrapper. It adds one command prefix to your dev workflow, and in exchange you get secrets that are centrally managed, per-environment, auditable, and revocable. The app doesn’t know or care. The secret never hits disk.

The alternative — .env files — also works. Until it doesn’t: the accidental commit, the old employee who still has a copy, the file that lived on a stolen laptop. That risk compounds every year the file exists.

This isn’t a rewrite. It’s a three-line migration. The hardest part is deleting the .env file.

For the full picture on why a secrets manager is worth the overhead, see Understanding Secrets Manager Architecture. For non-interactive auth in automation, see Infisical Machine Identities.