Environment Configuration Best Practices
Introduction
Proper environment configuration is crucial for application security, portability, and maintainability. This guide outlines Kirana Labs' standards for managing environment variables, secrets, and configuration across different environments.
Core Principles
- Security First: Never expose sensitive information in code or version control
- Environment Separation: Maintain clear separation between development, staging, and production
- Configuration as Code: Treat configuration like code with proper versioning and review
- Developer Experience: Make local setup easy while maintaining security
- Infrastructure Independence: Allow applications to run consistently across different platforms
Environment Variables
Naming Conventions
Use the following conventions for environment variable names:
- All uppercase with underscores
- Prefix with application or service name when needed
- Group related variables with common prefixes
- Use descriptive names that clearly indicate purpose
Examples:
# Database configuration
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
DATABASE_MAX_CONNECTIONS=20
# API keys with service prefix
STRIPE_API_KEY=sk_test_123
SENDGRID_API_KEY=sg_123
# Feature flags
FEATURE_NEW_DASHBOARD=true
FEATURE_BETA_ANALYTICS=false
Required vs. Optional Variables
Clearly document which variables are required and which are optional:
// config/env.ts
import { z } from 'zod';
// Schema for environment variables
const envSchema = z.object({
// Required variables
NODE_ENV: z.enum(['development', 'test', 'production']),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
// Optional variables with defaults
PORT: z.string().default('3000'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
CACHE_TTL: z.string().transform(Number).default('300'),
});
// Validate and export environment variables
export const env = envSchema.parse(process.env);
Local Development
For local development, use .env files with the following guidelines:
- Never commit
.envfiles to version control - Provide
.env.examplefiles with dummy/safe values - Document the process for setting up local environments
Example .env.example file:
# Server configuration
PORT=3000
NODE_ENV=development
# Database configuration
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app_dev
# Authentication (replace with actual secrets for local dev)
JWT_SECRET=replace_with_min_32_char_random_string
JWT_EXPIRES_IN=1d
# Third-party services (replace with test account credentials)
STRIPE_API_KEY=sk_test_example
SENDGRID_API_KEY=sg_test_example
Environment-Specific Configuration
Environment Types
Define clear configuration for different environments:
- Development: Local developer environments
- Testing: Automated testing environments
- Staging: Pre-production testing environment
- Production: Live environment with real data
Configuration Strategy
Use a combination of:
.envfiles for local development- Environment variables from CI/CD systems for testing/deployment
- Secret management services for production secrets
NextJS Environment Configuration
For NextJS applications, distinguish between:
- Server-side only variables: Accessible only in Server Components, API routes, and server-side code
- Public variables: Exposed to the browser, prefixed with
NEXT_PUBLIC_
Example:
# Server-side only (not exposed to browser)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
API_SECRET_KEY=sk_test_123
# Public (exposed to browser)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=UA-123456
Secrets Management
Development Environment
- Use
.envfiles for local development - Never store real production secrets locally
- Use separate test accounts for development environments
Staging and Production
Use dedicated secrets management solutions based on the hosting platform:
- Railway: Environment variables in project settings
- Vercel: Environment variables in project configuration
- AWS: AWS Secrets Manager or Parameter Store
- GCP: Google Secret Manager
Rotating Secrets
Establish procedures for secret rotation:
- Generate new secret
- Deploy the new secret alongside the old one
- Update applications to use the new secret
- Monitor for any usage of the old secret
- Remove the old secret
Configuration Loading
Centralized Configuration
Create a centralized configuration module that:
- Loads and validates environment variables
- Provides type-safe access to configuration
- Sets sensible defaults where appropriate
Example:
// config/index.ts
import { z } from 'zod';
// Define the schema for environment variables
const envSchema = z.object({
// Server
NODE_ENV: z.enum(['development', 'test', 'staging', 'production']),
PORT: z.string().transform(Number).default('3000'),
// Database
DATABASE_URL: z.string().url(),
// Authentication
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default('1d'),
// Public URLs
NEXT_PUBLIC_API_URL: z.string().url(),
});
// Parse and validate environment variables
const parsedEnv = envSchema.safeParse(process.env);
if (!parsedEnv.success) {
console.error(
'❌ Invalid environment variables:',
JSON.stringify(parsedEnv.error.format(), null, 2)
);
process.exit(1);
}
export const config = {
isProduction: parsedEnv.data.NODE_ENV === 'production',
isTest: parsedEnv.data.NODE_ENV === 'test',
isDevelopment: parsedEnv.data.NODE_ENV === 'development',
server: {
port: parsedEnv.data.PORT,
nodeEnv: parsedEnv.data.NODE_ENV,
},
database: {
url: parsedEnv.data.DATABASE_URL,
},
auth: {
jwtSecret: parsedEnv.data.JWT_SECRET,
jwtExpiresIn: parsedEnv.data.JWT_EXPIRES_IN,
},
publicUrls: {
apiUrl: parsedEnv.data.NEXT_PUBLIC_API_URL,
},
};
Feature Flags
Use environment variables for simple feature flags:
FEATURE_NEW_DASHBOARD=true
FEATURE_BETA_ANALYTICS=false
For more complex feature flag requirements, consider using a dedicated service like LaunchDarkly or Unleash.
Configuration Validation
Validate configurations at startup to catch issues early:
// Validate database connection
async function validateDatabaseConnection() {
try {
await db.raw('SELECT 1');
console.log('✅ Database connection validated');
} catch (error) {
console.error('❌ Database connection failed:', error);
process.exit(1);
}
}
// Validate required third-party services
async function validateRequiredServices() {
try {
const stripeResponse = await stripe.account.retrieve();
console.log('✅ Stripe connection validated');
} catch (error) {
console.error('❌ Stripe connection failed:', error);
process.exit(1);
}
}
// Run validation at startup
async function validateConfig() {
await validateDatabaseConnection();
await validateRequiredServices();
console.log('✅ All configurations validated');
}
Environment Setup Automation
Provide scripts to automate environment setup:
#!/bin/bash
# setup-dev.sh - Setup development environment
# Check for .env file
if [ ! -f .env ]; then
echo "Creating .env file from .env.example..."
cp .env.example .env
echo "Please update the .env file with your local settings."
fi
# Install dependencies
echo "Installing dependencies..."
npm install
# Setup database
echo "Setting up database..."
npx prisma migrate dev
# Seed development data
echo "Seeding development data..."
npx prisma db seed
echo "✅ Development environment setup complete!"
Docker Environment Variables
When using Docker, follow these practices:
- Use
ARGfor build-time variables - Use
ENVfor runtime variables - Don't hardcode secrets in Dockerfiles
- Pass secrets and configuration through environment variables at runtime
Example Dockerfile:
# Build arguments
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine AS base
# Set default environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Don't run as root
USER node
WORKDIR /app
# Copy application files
COPY --chown=node:node . .
# Install dependencies (use ci for reproducible builds)
RUN npm ci --only=production
# Expose port
EXPOSE ${PORT}
# Start command
CMD ["node", "server.js"]
CI/CD Environment Configuration
GitHub Actions
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
# Public variables for build
NEXT_PUBLIC_API_URL: https://api.kiranalabs.com
- name: Deploy to Railway
uses: railway/railway-action@v3
env:
# Secrets are stored in GitHub Secrets
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
with:
command: up
Environment-Specific Deployments
Use different environment configurations for different deployment targets:
# For staging deployment
- name: Deploy to Staging
uses: railway/railway-action@v3
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
with:
command: up
environment: staging
# For production deployment
- name: Deploy to Production
uses: railway/railway-action@v3
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
with:
command: up
environment: production
Runtime Configuration
For settings that need to be changed without redeployment, consider:
- Database-stored configuration
- Remote configuration service
- Environment variable override with app restart
Example schema for database configuration:
// Prisma schema for configuration
model AppConfig {
id String @id @default(cuid())
key String @unique
value String
description String?
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
}
Logging and Monitoring
Configure logging based on the environment:
import pino from 'pino';
import { config } from './config';
// Configure logger based on environment
export const logger = pino({
level: config.isDevelopment ? 'debug' : 'info',
transport: config.isDevelopment
? { target: 'pino-pretty' }
: undefined,
redact: ['password', 'secret', 'token'], // Redact sensitive information
});
Documentation
Document all required environment variables:
## Environment Variables
### Required Variables
| Name | Description | Example |
|------|-------------|---------|
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/db` |
| `JWT_SECRET` | Secret for signing JWT tokens | `a-long-secure-random-string` |
### Optional Variables
| Name | Description | Default | Example |
|------|-------------|---------|---------|
| `PORT` | Port for the HTTP server | `3000` | `8080` |
| `LOG_LEVEL` | Application logging level | `info` | `debug` |
| `CACHE_TTL` | Cache time-to-live in seconds | `300` | `600` |
Security Considerations
- Encryption: Encrypt sensitive environment variables at rest
- Access Control: Limit access to production secrets
- Auditing: Log and monitor access to production configurations
- Least Privilege: Only include the minimum required privileges in service accounts
Checklist for New Projects
- Create
.env.examplewith all required variables - Implement environment validation at startup
- Document all environment variables
- Set up CI/CD with environment-specific configuration
- Configure secrets management for production
- Test application across all target environments
- Implement monitoring of configuration-related issues
Conclusion
Following these environment configuration best practices will help ensure that Kirana Labs applications are secure, maintainable, and can be deployed consistently across different environments.
Remember that environment configuration is a critical part of application security. Always review and audit your configuration practices as part of your regular security reviews.