.env Files — Multiple Environments
The core concept
.env files store environment-specific config — API keys, database URLs, feature flags — outside of your code. You never commit them. The environment (local, staging, prod) loads its own values.
File hierarchy
Most frameworks follow this load order (later files override earlier):
1
2
3
4
.env # base defaults, committed (no secrets)
.env.local # local overrides, gitignored
.env.[environment] # e.g. .env.production, committed (no secrets)
.env.[environment].local # local overrides per env, gitignored
What to commit:
.env— safe defaults only, no secrets.env.example— template showing all required keys with blank/fake values
What to gitignore:
.env.local.env.*.local.env.productionif it contains real secrets
Next.js env hierarchy
Next.js loads these in order, later wins:
1
2
3
4
.env
.env.local
.env.development / .env.production / .env.test
.env.development.local / .env.production.local / .env.test.local
NEXT_PUBLIC_ prefix exposes the var to the browser. Without it, server-side only.
1
2
3
4
5
6
# .env.local (gitignored)
DRUPAL_GRAPHQL_URL=http://mysite.ddev.site/graphql
# exposed to browser
NEXT_PUBLIC_ALGOLIA_APP_ID=your_app_id
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY=your_search_key
Use NEXT_PUBLIC_ENVIRONMENT to switch index names or API targets per env:
1
export const indexName = (process.env.NEXT_PUBLIC_ENVIRONMENT || "prod") + "_search"
Do .env files relate to git branches?
Yes — indirectly but directly in practice.
The .env file itself never leaves your machine (gitignored). But your git branch determines which deployment environment is triggered, and that environment injects its own set of variables.
1
2
3
main branch → production deployment → production env vars injected
develop branch → staging deployment → staging env vars injected
feature/x branch → preview deployment → preview env vars injected
Vercel
Vercel has three env scopes tied to branches:
| Scope | Branch |
|---|---|
| Production | main (or custom) |
| Preview | all other branches |
| Development | local vercel dev |
Set vars per scope in the Vercel dashboard. The correct set is automatically injected when that branch deploys.
GitHub Actions
Use GitHub Environments to scope secrets per branch:
1
2
3
4
5
6
# .github/workflows/deploy.yml
jobs:
deploy:
environment: production # or staging
steps:
- run: echo $
Branch protection rules control which branches can access which environment’s secrets.
Netlify
Same pattern — set env vars per deploy context (production, deploy-preview, branch-deploy) in site settings.
DDEV (local)
DDEV has its own env layer. Add vars to .ddev/config.yaml:
1
2
web_environment:
- MY_VAR=value
Or use a .env file in your project root — DDEV picks it up automatically for ddev exec and web container context.
WordPress
WordPress doesn’t use .env natively, but wp-config.php can be made env-aware:
1
2
3
4
5
// wp-config.php
define('DB_NAME', getenv('DB_NAME') ?: 'local_db');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASSWORD', getenv('DB_PASSWORD') ?: '');
define('WP_DEBUG', getenv('WP_ENV') === 'production' ? false : true);
With DDEV, set the vars in .ddev/config.yaml. On the server, set them in the Apache/Nginx vhost or via a .env file loaded by a package like vlucas/phpdotenv.
.env.example — always commit this
Document every required key with a blank or fake value:
1
2
3
4
5
6
7
8
# .env.example
DRUPAL_GRAPHQL_URL=
NEXT_PUBLIC_ALGOLIA_APP_ID=
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY=
NEXT_PUBLIC_ENVIRONMENT=local
DB_NAME=
DB_USER=
DB_PASSWORD=
New developers copy this, fill in real values, and never commit the result.
Summary
| File | Committed | Purpose |
|---|---|---|
.env | Yes | Safe base defaults |
.env.example | Yes | Template for onboarding |
.env.local | No | Local secrets/overrides |
.env.production | Depends | Prod config (no secrets) |
.env.production.local | No | Local prod overrides |
CI/CD (Vercel, Netlify, GitHub Actions) bridges the gap between branches and env vars — your branch signals the environment, the platform injects the right vars.