Guides
.env vs .env.example: What Should You Upload to GitHub?
If you are not sure whether to commit .env or .env.example to GitHub, you are not alone — it is one of the most common sources of accidental API key leaks. The answer is clear once you understand what each file does. This guide explains both files, why they exist, and gives you real examples for Next.js and Django apps.
Updated July 2026 · 9 min read
Written for AI makers using tools like Cursor, Bolt, Lovable, Replit, Claude, and ChatGPT.
Quick answer
.env holds your real, secret values. It must never go to GitHub. .env.example is a safe template with placeholder values — it should be committed so teammates know what the app needs. The two files have the same variable names, but only .env.example travels to GitHub.
Who this is for
- You see both
.envand.env.examplein your project and are not sure which to commit - You want to share your project on GitHub without exposing API keys
- A teammate cloned the project and cannot figure out how to set it up
What you'll need
- Your project folder with a
.envfile (or the knowledge of what keys your app uses) - A GitHub account and basic understanding of committing files
- A text editor
The two-file system most apps need
Most modern apps that call external APIs need configuration that changes between environments: different API keys for development and production, different database URLs, different feature flags. The standard way to manage this is with a pair of files: .env for real values and .env.example as a documented template.
.env contains real keys: OPENAI_API_KEY=sk-proj-abc123. .env.example contains the same variable names with placeholder strings: OPENAI_API_KEY=your_openai_key_here. They look similar but serve opposite purposes.
Together, they solve two problems: keeping secrets off GitHub while also making it easy to understand what the project needs to run. You cannot solve both problems with a single file.
What .env is for
.env is your local secret store. It lives in your project root and holds the actual credentials your app needs to run on your development machine. Variable names and their real values, one per line.
Your app framework reads .env at startup: Next.js reads it automatically, Python apps using python-dotenv call load_dotenv(), Vite reads it via import.meta.env. The values become available in code as process.env.VARIABLE_NAME or os.environ['VARIABLE_NAME'].
.env is strictly local. It belongs only on machines where the app runs in development. It must never be committed to Git because Git history is permanent — even if you delete the file later, old commits still contain the value.
What .env.example is for
.env.example is a safe documentation file. It lists every environment variable your app needs, but instead of real values it uses descriptive placeholder strings: STRIPE_SECRET_KEY=your_stripe_secret_key or DATABASE_URL=postgresql://user:password@localhost:5432/dbname.
When someone clones your project for the first time, they look at .env.example to understand what the app expects. They copy it to .env: cp .env.example .env — then replace the placeholders with their own real values. This makes project setup self-documenting.
.env.example is safe to commit and safe to share publicly. It contains no real secrets. Its value is entirely in the variable names and the placeholder strings that hint at the format expected.
Why .env must never be committed to GitHub
Git stores the entire history of every file ever committed. Committing .env once — even if you immediately delete it in the next commit — leaves the real values permanently accessible in the repository's commit history. Anyone who clones the repo can access older commits.
GitHub is scanned continuously by automated bots looking for patterns that look like API keys. Even in private repositories, exposure risk exists through collaborators, forked repositories, and any future visibility changes.
The correct protection is .gitignore: a file in your project root that lists patterns Git should never track. Adding .env to .gitignore prevents the file from ever appearing in git status as a file to commit.
What placeholder values look like
A placeholder should tell whoever reads it what kind of value goes there — without being a real value itself. Descriptive placeholders are better than empty strings or generic text.
Good placeholders: OPENAI_API_KEY=your_openai_api_key_here, DATABASE_URL=postgresql://user:password@localhost:5432/yourdb, STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key, JWT_SECRET=your_strong_random_secret_here.
Avoid: empty values (OPENAI_API_KEY=), generic single words (OPENAI_API_KEY=xxx), or anything that looks like a real key format. The goal is clarity, not obfuscation.
Frontend and backend examples
Next.js .env (local only): OPENAI_API_KEY=sk-proj-... STRIPE_SECRET_KEY=sk_live_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_... DATABASE_URL=postgres://localhost:5432/myapp. Only NEXT_PUBLIC_ variables appear in browser code — never use this prefix for secret keys.
Next.js .env.example (safe to commit): OPENAI_API_KEY=your_openai_api_key STRIPE_SECRET_KEY=your_stripe_secret_key NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key DATABASE_URL=postgresql://user:password@localhost:5432/yourdb.
Django .env (local only): SECRET_KEY=your-long-random-secret-key OPENAI_API_KEY=sk-proj-... DATABASE_URL=postgres://localhost:5432/myapp DEBUG=True. Django .env.example: SECRET_KEY=your-secret-key-here OPENAI_API_KEY=your_openai_api_key DATABASE_URL=postgresql://user:password@localhost:5432/yourdb DEBUG=False.
How .gitignore protects .env
.gitignore is the enforcement mechanism. Without it, the two-file system does not protect you — .env will be committed the first time you run git add. Open .gitignore in your project root and verify .env appears as its own line.
If .gitignore did not exist before your first commit, .env may already be tracked. Check with git ls-files .env — if the command returns the filename, it is tracked. Untrack it with git rm --cached .env, then commit the removal. The key values in past commits remain in history; rotate those keys.
Framework starters usually include .env in .gitignore by default. Still verify it. AI-generated projects sometimes omit or misconfigure .gitignore, especially when the framework is non-standard or the project has unusual tooling.
Setting up the two-file pattern for the first time
If your project currently has only a .env with real values and no .env.example, follow these steps: check that .env is in .gitignore (add it if not), create .env.example by copying .env and replacing all real values with placeholders, commit .env.example, and verify .env does not appear in git status.
If your project has neither file, create both. Start with .env.example: list every variable name your app reads and give each a placeholder value. Use it as the specification. Then copy it to .env and fill in your real development values.
Once both files exist and .env is gitignored, maintain them together. When you add a new integration that needs a new variable, add the name with a placeholder to .env.example and the real value to .env. Commit .env.example; never commit .env.
Step-by-step
- 1
Step 1
Check your current state
Open your project folder. Identify which env files exist. Check whether
.envis in.gitignore. Run git status or look at GitHub Desktop to see if.envappears as a file Git is tracking. - 2
Step 2
Secure .env in .gitignore
Add
.envto.gitignoreif it is not already there. If.envwas previously committed, run git rm --cached.envand commit the change. Rotate any keys that may have been exposed in Git history. - 3
Step 3
Create .env.example
Copy
.envto.env.example. Go through every line and replace real values with descriptive placeholder strings. Make sure no real key values remain. Save the file. - 4
Step 4
Commit .env.example
Add
.env.exampleto Git and commit it. Verify that only.env.example(not.env) appears in the commit. Push to GitHub. The template is now in your repository for collaborators and documentation.
Common mistakes
Committing
.envinstead of.env.exampleHow to fix it: Check git status before committing. If
.envappears, add it to.gitignorefirst. Commit only.env.example.Putting real key values in
.env.exampleHow to fix it:
.env.examplemust contain only placeholder strings. Copy.envto.env.example, then replace every real value with a descriptive placeholder.Not creating
.env.exampleat allHow to fix it: Without
.env.example, new collaborators and hosted deployments have no way to know what variables the app needs. Create it from your.envwith placeholders.Using
NEXT_PUBLIC_on secret keys in.envHow to fix it:
NEXT_PUBLIC_exposes the value to the browser bundle. Secret keys (OpenAI, Stripe secret, etc.) must be used without the prefix, in server-side code only.Thinking a private GitHub repo makes
.envsafe to commitHow to fix it: Private repos share secrets with collaborators and are vulnerable to visibility changes. Use
.gitignoreand.env.exampleregardless of repository visibility.
Copy-ready prompt
Prompt: create safe .env.example from my project
Paste this into Cursor, Claude, ChatGPT, Bolt, Lovable, or your AI coding tool.
Ready to paste
Set up the two-file environment variable pattern for this project. Tasks: 1. Scan the codebase for all environment variables the app reads (process.env.X, os.environ['X'], import.meta.env.X, etc.). 2. Verify .env exists with real values for all required variables. Create it if missing. 3. Confirm .env is in .gitignore. Add it if missing. If .env was previously committed, run git rm --cached .env. 4. Create or update .env.example with descriptive placeholder values for every variable in .env. Do not copy real values. 5. Check that no variables with secret values use NEXT_PUBLIC_, VITE_, or other browser-exposure prefixes. 6. Summarize what you created or changed and list any variables that still need real values from me.
Paste this into Cursor, Claude, ChatGPT, Bolt, Lovable, or your AI coding tool.
Ready check
Beginner checklist
.envexists in project root with real values for all required variables.envis listed in.gitignore- git status confirms
.envis not in the tracked file list .env.exampleexists with placeholder values for every variable.env.examplecontains no real key values.env.exampleis committed to GitHub- No secret variables use
NEXT_PUBLIC_orVITE_prefixes - Teammates can set up the project by copying
.env.exampleto.env
Frequently asked questions
- Can I commit .env if my GitHub repo is private?
- No. Private repos can become public, gain new collaborators, or be forked. Use
.gitignoreand.env.exampleregardless of repository visibility. - What is the difference between .env and .env.local in Next.js?
- Next.js supports multiple env file names.
.env.localoverrides.envand is automatically gitignored in Next.js projects. Both serve the same purpose — storing local secrets. Check Next.js documentation for the full loading priority order. - My teammate cloned the project but the app does not work. Is this the fix?
- Likely. They need to copy
.env.exampleto.envand fill in their own real API keys and values. Without a.envfile (or with missing variables), the app will fail to connect to external services. - Do I need .env.example if it is just me working on the project?
- Yes, for two reasons: it documents required variables for your future self when setting up on a new machine, and most hosting platforms and deployment tools use it to validate that required variables are configured.
- What if my AI tool created .env with real keys but no .env.example?
- Create
.env.exampleby copying.envand replacing every real value with a placeholder. Check that.envis in.gitignorebefore doing anything else — if it is already tracked by Git, untrack it with git rm --cached.envand rotate the exposed keys.