What is a .env file, and how do you keep secrets out of git?
A .env file keeps settings and secrets out of your code. It only protects you if it never reaches git, and the syntax has a few traps.
What it looks like
# Database
DATABASE_URL=postgres://app:s3cret@localhost:5432/app
PORT=3000
DEBUG=false
GREETING="Hello, world" # quotes keep the comma and space
PRIVATE_KEY="-----BEGIN KEY-----\nMIIB...\n-----END KEY-----"
Your code then reads process.env.PORT, os.environ["PORT"] and so on. The point is that the same code runs on your laptop, a test server and production, each with its own values, and no password sits in the source.
Syntax rules that trip people up
| Situation | Write | Why |
|---|---|---|
Spaces around = | PORT=3000 | Some loaders and shells treat PORT = 3000 as an error or keep the spaces |
Value with spaces or # | TITLE="My app #1" | Unquoted, # may start a comment |
Value with $ | PASS='pa$$word' | Single quotes stop $ being expanded as a variable in most loaders |
| Multi-line value | KEY="line1\nline2" | Escaped newlines in double quotes work almost everywhere; real line breaks only in some loaders |
| Numbers and booleans | DEBUG=false | Everything arrives as text: "false" is truthy in JavaScript, so compare strings |
export prefix | export PORT=3000 | Allowed by dotenv and python-dotenv, handy for source .env in a shell |
There is no single official standard, so the exact rules depend on the loader. Keep values simple and quote anything unusual.
Keeping it out of git
# .gitignore
.env
.env.*
!.env.example
Then commit a .env.example listing every variable with a blank or fake value, so the next person knows what to fill in. Add the ignore line before the first commit: once a file is tracked, .gitignore no longer affects it. To stop tracking one that slipped in, run git rm --cached .env and commit, then treat everything in it as leaked.
Loading it
- Node.js 20.6+:
node --env-file=.env app.js, no package needed. Older versions: thedotenvpackage. - Python:
python-dotenv, thenload_dotenv()at start-up. - Docker Compose: reads
.envnext tocompose.yamlfor${VAR}substitution; pass values into containers withenv_file:. NeverCOPY .envinto an image: anyone who pulls the image gets it. Add.envto.dockerignoretoo. - Front-end bundlers (Vite, Next.js, Create React App): variables with a public prefix such as
VITE_orNEXT_PUBLIC_are pasted into the JavaScript every visitor downloads. Never put a secret key behind those prefixes.
If you already pushed a secret
- Rotate it first. Create a new key or password and revoke the old one. Bots scan public repositories for keys within minutes, so assume it has been seen.
- Remove the file from the repo and add it to
.gitignore. - Optionally rewrite history with
git filter-repoor BFG, and force-push. That tidies up, but forks, clones and caches may still hold the old commit, which is why step 1 comes first.
Note that Base64 is not encryption: a Base64-encoded secret in a file is still a plain secret.
Making good secrets
For session secrets, API tokens and database passwords, use long random values rather than words: 32 or more random characters, or 32 random bytes as hex. The password generator makes them in your browser with the system's secure random generator, and nothing is sent anywhere. Want to share one with a teammate without leaving it in a chat log? A one-time link from Fusenote burns itself after reading.
Secrets in the browser are a different problem: localStorage vs cookies.
Questions
Should I commit my .env file?
No. Commit a .env.example with the variable names and dummy values, and keep the real .env in .gitignore.
Does .gitignore remove a .env file I already committed?
No. .gitignore only affects untracked files. Run git rm --cached .env, commit, and rotate any secrets that were in it, because they remain in the history.
Do I need quotes in a .env file?
Only when the value contains spaces, #, quotes, $ or newlines. Double quotes allow escapes like \n; single quotes keep the value exactly as written in most loaders.
Are environment variables secure?
They keep secrets out of source code, which is the main goal. They can still leak through error pages, logs, child processes or front-end bundles, so never print them and never expose them with public prefixes like VITE_ or NEXT_PUBLIC_.
Published .