In this guide
Your app needs a database password. Your first instinct is to type it directly into the code, right where the connection string is built. It works — until you push to GitHub and the password is now public, or you deploy to a server and the code still points at your laptop's database.
Environment variables are the standard answer, and .env files are how you feed them to your app during development. The concept is small but the mistakes around it are the most common way student projects leak secrets. This guide covers how environment variables actually work, how to load them in Python and Node, the six mistakes that leak keys, and how config changes between your laptop and a production server.
The problem environment variables solve
Code should be identical in development, testing, and production. Secrets and settings cannot be: the database password on your laptop is not the password on the server, the API key for testing is not the production key, and the debug flag must never be on in production. If settings live inside the code, every environment needs a different code copy — which is how people end up editing live code at 1 AM.
Environment variables solve this by keeping settings outside the code. The code reads a named value from the operating system's environment at runtime; each machine provides its own values. Nothing secret is written in the source files, so the code can be committed, shared, and deployed safely.
What a .env file is (and isn't)
A .env file is a plain text file, kept in the project root during development, that lists key-value pairs:
DATABASE_URL=postgres://devuser@localhost:5432/myapp
API_KEY=REPLACE_WITH_YOUR_OWN_KEY
DEBUG=true
Two rules govern it:
- It is never committed to git. The moment a
.envfile enters version control, every clone of the repository contains your secrets. Add.envto.gitignorebefore you create the file, not after. - It is development-only convenience. On a real server you set environment variables in the hosting platform's dashboard or shell, not by uploading your
.envfile. The.envfile is a local stand-in for the real environment.
Ship a .env.example file instead — same keys, placeholder values — so teammates know what to configure without ever seeing your secrets.
Loading them in Python
The python-dotenv library reads .env into the process environment:
from dotenv import load_dotenv
import os
load_dotenv() # reads .env into os.environ
db_url = os.environ.get("DATABASE_URL")
if not db_url:
raise RuntimeError("DATABASE_URL is not set")
Call load_dotenv() at the very top of your entry point, before any module that reads config. Failing fast with a clear error when a variable is missing is far better than the app booting with half its config empty and failing mysteriously later.
Loading them in Node.js
The dotenv package does the equivalent:
import "dotenv/config";
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
throw new Error("DATABASE_URL is not set");
}
The import must come before anything that reads process.env. Note: dotenv does not override variables that are already set in the real environment — the real environment always wins, which is exactly the behaviour you want in production.
How production config actually works
On a deployment platform (a VPS, Render, Railway, Vercel, a college server — anywhere), you do not upload .env. You enter the same keys in the platform's environment-variable settings, and the platform injects them into the running process. Your code reads os.environ / process.env exactly as it did locally, and cannot tell the difference. This is why the code never references a file path: it reads the environment, and .env was only ever a way to fake that environment on your laptop.
The six mistakes that leak secrets
- Committing
.envto git. The classic. Check.gitignorebefore the first commit, and if it ever happens, assume the key is compromised — rotate it, don't just delete the file (git history remembers). - Hardcoding a fallback key. Writing
API_KEY = os.environ.get("API_KEY") or "hardcoded-real-key"defeats the whole mechanism. Fall back to nothing; fail loudly instead. - Printing config in logs or error pages. Debug output that dumps all environment variables will eventually be screenshared, screenshotted, or logged to a file someone reads. Never log secrets.
- Sending the key to the browser. Anything in frontend JavaScript is public. Keys the browser needs must be safe-to-expose public keys; anything else stays on the server.
- Sharing
.envover chat or email. Send a.env.examplewith blanks; let each person fill in their own credentials from the service's dashboard. - Using one key everywhere. Development, staging, and production should have separate keys so that a leak in one environment doesn't compromise the others — and so you can rotate one without breaking everything.
What to check before you deploy
.envis in.gitignoreand no secret is in the repo (search the codebase for the actual key values to be sure).- Every required variable is set on the production platform, and the app fails fast with a clear message if one is missing.
DEBUG/ verbose error pages are off in production — detailed stack traces handed to strangers are a vulnerability.- The
.env.exampleis up to date, so the next person can set the project up without asking you for values.
FAQ
Is a .env file encrypted or secure by itself?
No. It's a plain text file — its entire security model is "it stays on your machine and out of git." Anyone with your laptop can read it. The protection comes from never publishing it, not from the format.
My teammate needs the same keys. How do I share them safely?
The safe pattern is that each person generates their own credentials from the service's dashboard (most APIs let you create multiple keys). Sharing one key over chat means it now lives in a chat history, a notification preview, and possibly a cloud backup.
Why does my app work locally but fail on the server?
In nine cases out of ten, a variable is missing or misspelled in the production environment, or the code reads process.env before the platform injects values (wrong startup order). Add a startup check that lists which required variables are missing — never their values — and the answer appears in the first log line.
Do I need different variables for frontend and backend?
Yes, and this is a common source of leaks: build tools embed frontend variables into the JavaScript bundle at build time, making them public. Keep secrets in backend-only variables and expose to the frontend only what the browser genuinely needs.
Limitations
- Environment variables are a configuration mechanism, not a secrets vault: on shared or compromised machines they are readable by anyone with process access. High-sensitivity systems use dedicated secret managers instead.
- Values are strings — numbers, booleans and lists need parsing in code, which is a small but real source of bugs ("false" the string is truthy in most languages).
- This guide covers the standard pattern for student web apps; regulated environments (payments, health data) impose stricter secret-handling rules that go beyond
.envdiscipline.