A .env file is a plain text file that holds the configuration values your application needs to run, one per line, in the form KEY=VALUE. Database URLs, API keys, feature flags, the port to listen on. Your code reads them as environment variables at startup instead of having them written into the source.
That is the whole idea. It is deliberately simple. The complications come from how you load it, how you keep it out of git, and what happens when more than one person needs a copy.
What a .env file looks like
DATABASE_URL=postgres://app:s3cret@localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_51Hx...
JWT_SECRET=change-me-in-production
PORT=3000
DEBUG=true
No quotes needed for simple values. No spaces around the equals sign. Lines starting with # are comments. The file sits in the root of your project and is named .env, with the leading dot, which makes it hidden by default on macOS and Linux.
Why it exists
Before .env files, developers hard coded credentials into source files and committed them. When the code was shared, so were the keys. The .env convention came out of Heroku's twelve factor methodology in 2012 and was popularised by the dotenv library for Node in 2013. The principle is that configuration that changes between environments should live in the environment, not in the code.
That separation gives you three things:
The same code runs everywhere. Your app reads DATABASE_URL and does not care whether it points at a local Postgres, a staging database, or production. You change the file, not the code.
Secrets stay out of version control. Add .env to .gitignore and the credentials never enter git history. This is the single most important property of the whole approach.
Config is visible in one place. A new developer can open one file and see every setting the app depends on, rather than hunting through the codebase for hard coded values.
How to load it
The file does nothing on its own. Something has to read it and put the values into the process environment. Every language has a library for this, and most are called some variation of dotenv.
Node.js
npm install dotenv
import 'dotenv/config';
const db = process.env.DATABASE_URL;
Node 20 and later can also load a .env file without a library, using the --env-file flag:
node --env-file=.env server.js
Python
pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv()
db = os.getenv("DATABASE_URL")
Docker and Docker Compose
Docker Compose reads a .env file in the project directory automatically and substitutes the values into your compose.yml:
services:
api:
image: myapp
environment:
- DATABASE_URL=${DATABASE_URL}
You can also pass a file straight into a container with docker run --env-file .env myapp.
Everything else
Ruby has dotenv, PHP has vlucas/phpdotenv, Go has godotenv, Rust has dotenvy. The pattern is identical: call load early, then read from the environment as normal.
Multiple environments and file precedence
Most projects end up with more than one file:
| File | Purpose | Commit it? |
|---|---|---|
.env | Default values for every environment | No |
.env.local | Your personal overrides on your machine | No |
.env.development | Settings specific to local development | Sometimes |
.env.production | Settings specific to production | No |
.env.example | A template with keys but no values | Yes |
Which file wins when they conflict depends on the tool. Next.js, Vite and Create React App each have their own precedence order, and they do not all agree. The general rule is that more specific files override less specific ones, and .env.local overrides everything, but check the docs for whatever framework you are using rather than assuming.
The .env.example file is worth calling out. It is the one file you should commit. It lists every variable the app needs with placeholder values, so a new developer knows what to fill in:
DATABASE_URL=
STRIPE_SECRET_KEY=
JWT_SECRET=
PORT=3000
Is a .env file secure?
Honest answer: not by itself. It is plain text. Anyone with read access to the file has every secret in it.
The security comes from three things around the file, not the file:
It is not in git. This is what stops the keys spreading with the code. If you have ever committed a .env file, treat every secret in it as compromised, rotate them, and then remove the file from history. Deleting it in a later commit is not enough, because git keeps the old version.
File permissions. On a shared server, the file should be readable only by the user the app runs as. chmod 600 .env does that on Linux and macOS.
It never leaves your machine. The moment you email it, paste it into Slack, or put it in a shared drive, every one of those places now holds your production credentials with whatever retention and access controls they happen to have.
That last one is where most teams go wrong, and it is covered below.
Format gotchas
The format looks too simple to get wrong. It is not.
Spaces. KEY = value fails in most parsers. Use KEY=value.
Quotes. Values with spaces or special characters need quotes: MESSAGE="hello world". Single and double quotes behave differently in some parsers. Double quotes usually expand \n into a newline; single quotes do not.
Multiline values. Private keys and certificates span many lines. Most modern dotenv libraries support them inside double quotes, but older versions do not. If your RSA key is coming through as the first line only, this is why.
Variable expansion. Some parsers expand ${OTHER_VAR} inside values and some do not. python-dotenv does. The original Node dotenv does not unless you add dotenv-expand.
The export prefix. Shell style export KEY=value is accepted by some loaders and rejected by others. Leave it off.
Trailing whitespace. A space after the value is part of the value in many parsers. If a key that looks right is being rejected by an API, check for this.
Common mistakes
Committing it. The big one. Check .gitignore before the first commit, not after.
Using one file for everything. If the same .env holds your test Stripe key and your live one, someone will eventually run the wrong command against the wrong environment. Separate the files, or separate the environments in whatever tool manages them.
Loading it in production. Production servers should get their environment from the platform (Railway, Vercel, Heroku, a Kubernetes secret, whatever you deploy to), not from a file on disk. The .env file is a local development convenience.
No .env.example. Without one, every new team member has to ask what variables exist. With one, they copy it to .env and fill in the blanks.
Putting non secrets in it. PORT=3000 is fine. But if a value is the same everywhere and not sensitive, it can just be in the code or a regular config file.
The problem .env files do not solve
Everything above works for one developer on one machine. It stops working the moment there is a team.
Three developers each need the same twenty variables. One of them rotates the database password. Now the other two are running against a broken connection until someone remembers to tell them, and the update travels over Slack, which means the new production credential is now in a chat log forever.
This is the gap that secrets managers exist to fill. Doppler, Infisical, Vault and others, including Krypt, which we build, keep the secrets in one place and let each developer pull them down with a command. The .env file still exists on your machine, it is just generated from a shared source instead of copied around by hand.
If you are a solo developer, you do not need one of these. If you are sharing a .env with anyone, it is worth reading how they compare, because the right pick depends on team size, budget and whether you want to run your own infrastructure.
FAQ
Should I commit my .env file? No. Commit .env.example with placeholder values instead, and add .env to .gitignore.
What is the difference between .env and environment variables? Environment variables are values in the process environment that any program can read. A .env file is a convenient way to set them for local development. In production, the platform usually sets them directly and no file is involved.
Can I have comments in a .env file? Yes, lines starting with # are ignored by every common parser.
Why does my app not see my .env variables? The most common causes: the file is not in the directory the app runs from, the loader is called after the variable is read, or the framework expects a different filename such as .env.local. Add a console.log(process.env.YOUR_KEY) right after loading to check.
Is a .env file the same as a config file? They overlap. A config file usually holds structured settings that are the same everywhere. A .env file holds the values that change between environments, especially secrets. Many projects use both.
How do I share a .env file with my team safely? Not over chat or email. Use a secrets manager, or at minimum an encrypted file with a key you distribute separately. Krypt's free tier covers three people if you want to try the managed approach.