What it is
An environment variable is a name and a string in the environment of a process. Child processes inherit a copy. On a laptop you might set PORT=3000 in a shell. In production the platform sets it when it starts the app. Code reads process.env.PORT in Node or os.environ in Python. A .env file is a convenience that a library loads into the environment during local development. The file is not magical and is not automatically present in production.
The twelve-factor idea is: configuration that changes between deploys (dev, staging, production) belongs in the environment, not in code branches. Secrets are configuration too, but they are configuration you must not scatter.
Why and when it is used
Use environment variables for the database URL, the listen port, the log level, the model name, and the base URL of an API. Use them so the same image runs in two environments. Use a secret manager (cloud secret stores, CI secret settings) to supply the sensitive ones at the start of the process.
Do not use environment variables for data that is not configuration, and do not use them as a database. Do not put secrets in a .env that is committed, in a Dockerfile ENV for a real key, or in a frontend build argument that gets baked into JavaScript. Frontend and Backend is why: the browser is the user's machine.
How it works
The parent process (your shell, systemd, a container runtime, a CI runner) passes a block of strings. The app reads them at startup. A missing variable should fail startup if the app cannot run without it. A default is appropriate for a port in local dev and dangerous for a "default" production password. .env loaders do not override variables already set, or they do, depending on the library; know which, or you will debug the wrong value.
Variables are strings. "false" is true in some boolean checks because a non-empty string is truthy. Parse deliberately. They are visible to the process and often to anything that can read /proc on the same host or inspect the orchestrator's pod spec. They are not encrypted at rest inside the process. A crash dump or a naive debug endpoint that prints the environment is a leak.
Technologies and dependencies
The runtime's environment API, a local .env that is gitignored, and the platform's config screen or secret store. Docker can pass variables at run time. CI and CD injects them into deploy jobs and must mask them in logs. GitHub Actions uses encrypted secrets. API Keys explains the kind of value that most often ends up here by mistake in a screenshot.
How to get started
- List what differs between your laptop and production. Those names are your configuration.
- Read them in one module that crashes with a clear message if a required name is missing. The rest of the app takes a config object, not scattered
process.envreads. - Add
.envto.gitignore. Add.env.examplewith empty values and comments so teammates know the names. - Set the real values in your shell or a local
.envyou do not commit. Run the app. - In CI, set them in the secret store. Print only whether a variable is present, never the value.
- Rotate anything that was ever committed, even "just for a second".
If a framework prefixes variables with NEXT_PUBLIC_ or similar, assume that prefix means "this will ship to the browser".
Cautions and trade-offs
.env files get pasted into chat. Treat chat as public. Multi-line secrets and quotes break naive parsers. A variable set in the systemd unit and a different one in the shell you used for debugging means you tested the wrong config.
Environment variables are a poor fit for values that change every minute or that are larger than a secret. They are also inherited by subprocesses. A library that shells out may leak them into a child that logs its environment. And do not invent your own crypto in a config file when the platform already has a secret store. The goal is one obvious place a new teammate looks, and a guarantee that place is not the git history.