Organising Environment Files
Environment files can become a pain to maintain over time, particularly as the number of apps, services and environments grow over time. The tips below might seem overkill at first, but at scale, they should make your environment configurations files more manageable.
This article builds upon the ideas presented in Environment Configuration Tips, which gives advice on authoring and using .env files.
Separate out your .env files
I personally like to have separate .env files for:
- each environment
- each app
- running vs deploying
Put together this results in the following folder structure:
├── packages
│ ├── api
│ │ ├── .env.example
│ │ ├── .env.local # API connecting to localhost database
│ │ ├── .env.dev # API connecting to deployed dev database
│ │ └── .env.prod # API connecting to production
│ └── web
│ ├── .env.example
│ ├── .env.local # Web app pointing to localhost API
│ └── .env.dev # Web app pointing to dev API
└── infra
├── .env.example
├── .env.dev
└── .env.prod
Accept different .env files
Ensure every service, app and command accepts an explicit environment name or path. This might need to be explicitly added and should support values like deploy-dev (see the next section). Furthermore:
- Avoid defaulting to
.envfiles in code - Avoid loading fallback/multiple
.envfiles (see below about Vite)
Clear commands
Once the different files are established, I like to create a very clear set of environment specific commands in my codebase:
{
"scripts": {
"watch": "...", // Default to local
"watch:local": "...", // Explicit local
"watch:dev": "...", // Connect to deployed dev
"watch:prod": "...", // Connect to prod (read-only)
"deploy:dev": "...", // Deploy to dev
"deploy:prod": "..." // Deploy to production
}
}
Implementation: Vite
Vite’s default behavior loads multiple .env files with fallback chains. Override this to load exactly one file.
// vite.config.ts
import { defineConfig, loadEnv } from "vite";
import fs from "fs";
export default defineConfig(({ mode }) => {
// Map 'development' to 'local'
const envMode = mode === "development" ? "local" : mode;
const envFile = `.env.${envMode}`;
// Fail fast if file doesn't exist
if (!fs.existsSync(envFile)) {
throw new Error(`Missing required environment file: ${envFile}`);
}
// Load ONLY the specific file - no fallbacks
const env = loadEnv(envMode, process.cwd(), "");
return {
envPrefix: [], // Disable Vite's automatic multi-file loading
define: {
"import.meta.env": env,
},
};
});
Deployment flow
When deploying, the infrastructure deployment script:
- Reads
infra/.env.{environment} - Generates
packages/*/.env.deploy-{environment}for each package - Runs
vite build --mode deploy-{environment}which loads the generated file
This will result in the creation of the files:
packages/api/.env.deploy-dev
packages/api/.env.deploy-staging
packages/api/.env.deploy-prod
This separates “build for production” from “production environment configuration”.