Configuration Management
Overview
Comprehensive guide to managing application configuration across environments, including environment variables, configuration files, secrets, feature flags, and following 12-factor app methodology.
When to Use
- Setting up configuration for different environments
- Managing secrets and credentials
- Implementing feature flags
- Creating configuration hierarchies
- Following 12-factor app principles
- Migrating configuration to cloud services
- Implementing dynamic configuration
- Managing multi-tenant configurations
Instructions
1. Environment Variables
Basic Setup (.env files)
# .env.development
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379
LOG_LEVEL=debug
API_KEY=dev-api-key-12345
# .env.production
NODE_ENV=production
PORT=8080
DATABASE_URL=${DATABASE_URL} # From environment
REDIS_URL=${REDIS_URL}
LOG_LEVEL=info
API_KEY=${API_KEY} # From secret manager
# .env.test
NODE_ENV=test
DATABASE_URL=postgresql://localhost:5432/myapp_test
LOG_LEVEL=error
Loading Environment Variables
// config/env.ts
import dotenv from "dotenv";
import path from "path";
// Load environment-specific .env file
const envFile = `.env.${process.env.NODE_ENV || "development"}`;
dotenv.config({ path: path.resolve(process.cwd(), envFile) });
// Validate required variables
const required = ["DATABASE_URL", "PORT", "API_KEY"];
const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Missing required environment variables: ${missing.join(", ")}`,
);
}
// Export typed configuration
export const config = {
env: process.env.NODE_ENV || "development",
port: parseInt(process.env.PORT || "3000", 10),
database: {
url: process.env.DATABASE_URL!,
poolSize: parseInt(process.env.DB_POOL_SIZE || "10", 10),
},
redis: {
url: process.env.REDIS_URL || "redis://localhost:6379",
},
logging: {
level: process.env.LOG_LEVEL || "info",
},
api: {
key: process.env.API_KEY!,
timeout: parseInt(process.env.API_TIMEOUT || "5000", 10),
},
} as const;
Python Configuration
# config/settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env file
env_file = f'.env.{os.getenv("ENVIRONMENT", "development")}'
load_dotenv(Path(__file__).parent.parent / env_file)
class Config:
"""Base configuration"""
ENV = os.getenv('ENVIRONMENT', 'development')
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
SECRET_KEY = os.getenv('SECRET_KEY')
# Database
DATABASE_URL = os.getenv('DATABASE_URL')
DB_POOL_SIZE = int(os.getenv('DB_POOL_SIZE', 10))
# Redis
REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379')
# API
API_KEY = os.getenv('API_KEY')
API_TIMEOUT = int(os.getenv('API_TIMEOUT', 5000))
class DevelopmentConfig(Config):
"""Development configuration"""
DEBUG = True
LOG_LEVEL = 'DEBUG'
class ProductionConfig(Config):
"""Production configuration"""
DEBUG = False
LOG_LEVEL = 'INFO'
class TestConfig(Config):
"""Test configuration"""
TESTING = True
DATABASE_URL = 'sqlite:///:memory:'
# Configuration dictionary
config_by_name = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'test': TestConfig
}
# Get active config
config = config_by_name[Config.ENV]()
2. Configuration Hierarchies
// config/config.ts
import deepmerge from "deepmerge";
// Base configuration (shared across all environments)
const baseConfig = {
app: {
name: "MyApp",
version: "1.0.0",
},
server: {
timeout: 30000,
bodyLimit: "100kb",
},
database: {
poolSize: 10,
idleTimeout: 30000,
},
logging: {
format: "json",
destination: "stdout",
},
};
// Environment-specific overrides
const developmentConfig = {
server: {
port: 3000,
},
database: {
url: "postgresql://localhost:5432/myapp_dev",
logging: true,
},
logging: {
level: "debug",
prettyPrint: true,
},
};
const productionConfig = {
server: {
port: 8080,
trustProxy: true,
},
database: {
url: process.env.DATABASE_URL,
ssl: true,
logging: false,
},
logging: {
level: "info",
prettyPrint: false,
},
};
// Merge configurations
const configs = {
development: deepmerge(baseConfig, developmentConfig),
production: deepmerge(baseConfig, productionConfig),
test: deepmerge(baseConfig, {
database: { url: "postgresql://localhost:5432/myapp_test" },
}),
};
export const config = configs[process.env.NODE_ENV || "development"];
YAML Configuration Files
# config/default.yml
app:
name: MyApp
version: 1.0.0
server:
timeout: 30000
bodyLimit: 100kb
database:
poolSize: 10
idleTimeout: 30000
# config/development.yml
server:
port: 3000
database:
url: postgresql://localhost:5432/myapp_dev
logging: true
logging:
level: debug
prettyPrint: true
# config/production.yml
server:
port: 8080
trustProxy: true
database:
url: ${DATABASE_URL}
ssl: true
logging: false
logging:
level: info
prettyPrint: false
// Load YAML config
import yaml from "js-yaml";
import fs from "fs";
import path from "path";
function loadYamlConfig(env: string) {
const defaultConfig = yaml.load(
fs.readFileSync(path.join(__dirname, "config/default.yml"), "utf8"),
);
const envConfig = yaml.load(
fs.readFileSync(path.join(__dirname, `config/${env}.yml`), "utf8"),
);
return deepmerge(defaultConfig, envConfig);
}
export const config = loadYamlConfig(process.env.NODE_ENV || "development");
3. Secret Management
AWS Secrets Manager
// secrets/aws-secrets-manager.ts
import {
SecretsManagerClient,
GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
export class SecretManager {
private client: SecretsManagerClient;
private cache = new Map<string, { value: any; expiry: number }>();
private cacheTtl = 300000; // 5 minutes
constructor() {
this.client = new SecretsManagerClient({ region: process.env.AWS_REGION });
}
async getSecret(secretName: string): Promise<any> {
// Check cache
const cached = this.cache.get(secretName);
if (cached && cached.expiry > Date.now()) {
return cached.value;
}
try {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await this.client.send(command);
const secret = JSON.parse(response.SecretString || "{}");
// Cache the secret
this.cache.set(secretName, {
value: secret,
expiry: Date.now() + this.cacheTtl,
});
return secret;
} catch (error) {
throw new Error(
`Failed to retrieve secret ${secretName}: ${error.message}`,
);
}
}
async getDatabaseCredentials(): Promise<DatabaseCredentials> {
return this.getSecret("prod/database/credentials");
}
async getApiKey(service: string): Promise<string> {
const secrets = await this.getSecret("prod/api-keys");
return secrets[service];
}
}
// Usage
const secretManager = new SecretManager();
async function connectDatabase() {
const credentials = await secretManager.getDatabaseCredentials();
return createConnection({
host: credentials.host,
port: credentials.port,
username: credentials.username,
password: credentials.password,
database: credentials.database,
});
}
HashiCorp Vault
// secrets/vault.ts
import vault from "node-vault";
export class VaultClient {
private client: any;
constructor() {
this.client = vault({
apiVersion: "v1",
endpoint: process.env.VAULT_ADDR || "http://localhost:8200",
token: process.env.VAULT_TOKEN,
});
}
async getSecret(path: string): Promise<any> {
try {
const result = await this.client.read(path);
return result.data.data;
} catch (error) {
throw new Error(`Failed to read secret from ${path}: ${error.message}`);
}
}
async getDatabaseConfig(): Promise<DatabaseConfig> {
return this.getSecret("secret/data/database");
}
async getApiKeys(): Promise<Record<string, string>> {
return this.getSecret("secret/data/api-keys");
}
// Dynamic database credentials (rotated automatically)
async getDynamicDBCredentials(): Promise<Credentials> {
const result = await this.client.read("database/creds/readonly");
return {
username: result.data.username,
password: result.data.password,
leaseId: result.lease_id,
leaseDuration: result.lease_duration,
};
}
}
Environment-Specific Secrets
// secrets/secret-provider.ts
export interface SecretProvider {
getSecret(key: string): Promise<string>;
}
// Development: Use .env file
export class EnvFileSecretProvider implements SecretProvider {
async getSecret(key: string): Promise<string> {
const value = process.env[key];
if (!value) {
throw new Error(`Secret ${key} not found in environment`);
}
return value;
}
}
// Production: Use AWS Secrets Manager
export class AWSSecretProvider implements SecretProvider {
private secretManager: SecretManager;
constructor() {
this.secretManager = new SecretManager();
}
async getSecret(key: string): Promise<string> {
const secrets = await this.secretManager.getSecret("prod/secrets");
return secrets[key];
}
}
// Factory
export function createSecretProvider(): SecretProvider {
if (process.env.NODE_ENV === "production") {
return new AWSSecretProvider();
}
return new EnvFileSecretProvider();
}
4. Feature Flags
Simple Feature Flag Implementation
// feature-flags/feature-flag.ts
export interface FeatureFlag {
enabled: boolean;
rolloutPercentage?: number;
allowedUsers?: string[];
allowedEnvironments?: string[];
}
export class FeatureFlagManager {
private flags: Map<string, FeatureFlag>;
constructor(flags: Record<string, FeatureFlag>) {
this.flags = new Map(Object.entries(flags));
}
isEnabled(
flagName: string,
context?: { userId?: string; environment?: string },
): boolean {
const flag = this.flags.get(flagName);
if (!flag) return false;
// Check if disabled globally
if (!flag.enabled) return false;
// Check environment restriction
if (flag.allowedEnvironments && context?.environment) {
if (!flag.allowedEnvironments.includes(context.environment)) {
return false;
}
}
// Check user whitelist
if (flag.allowedUsers && context?.userId) {
if (flag.allowedUsers.includes(context.userId)) {
return true;
}
}
// Check rollout percentage
if (flag.rolloutPercentage !== undefined && context?.userId) {
const hash = this.hashUserId(context.userId);
return hash % 100 < flag.rolloutPercentage;
}
return true;
}
private hashUserId(userId: string): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = (hash << 5) - hash + userId.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
}
// Configuration
const featureFlags = {
"new-dashboard": {
enabled: true,
rolloutPercentage: 50, // 50% of users
},
"experimental-feature": {
enabled: true,
allowedUsers: ["user-123", "user-456"],
allowedEnvironments: ["development", "staging"],
},
"beta-api": {
enabled: true,
rolloutPercentage: 10,
},
};
const flagManager = new FeatureFlagManager(featureFlags);
// Usage
app.get("/api/dashboard", (req, res) => {
if (
flagManager.isEnabled("new-dashboard", {
userId: req.user.id,
environment: process.env.NODE_ENV,
})
) {
return res.json(getNewDashboard());
}
return res.json(getOldDashboard());
});
LaunchDarkly Integration
// feature-flags/launchdarkly.ts
import LaunchDarkly from "launchdarkly-node-server-sdk";
export class LaunchDarklyClient {
private client: LaunchDarkly.LDClient;
async initialize() {
this.client = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!);
await this.client.waitForInitialization();
}
async isEnabled(
flagKey: string,
user: LaunchDarkly.LDUser,
): Promise<boolean> {
return this.client.variation(flagKey, user, false);
}
async getVariation<T>(
flagKey: string,
user: LaunchDarkly.LDUser,
defaultValue: T,
): Promise<T> {
return this.client.variation(flagKey, user, defaultValue);
}
close() {
this.client.close();
}
}
// Usage
const ldClient = new LaunchDarklyClient();
await ldClient.initialize();
app.get("/api/dashboard", async (req, res) => {
const user = {
key: req.user.id,
email: req.user.email,
custom: {
groups: req.user.groups,
},
};
const showNewDashboard = await ldClient.isEnabled("new-dashboard", user);
if (showNewDashboard) {
return res.json(getNewDashboard());
}
return res.json(getOldDashboard());
});
5. 12-Factor App Configuration
// config/twelve-factor.ts
/**
* 12-Factor App Configuration Principles
*
* III. Config - Store config in the environment
* - Strict separation of config from code
* - Config varies between deploys, code does not
* - Store in environment variables
*/
// ✅ Good: Configuration from environment
export const config = {
database: {
url: process.env.DATABASE_URL!,
poolMin: parseInt(process.env.DB_POOL_MIN || "2", 10),
poolMax: parseInt(process.env.DB_POOL_MAX || "10", 10),
},
redis: {
url: process.env.REDIS_URL!,
},
s3: {
bucket: process.env.S3_BUCKET!,
region: process.env.AWS_REGION!,
},
sendgrid: {
apiKey: process.env.SENDGRID_API_KEY!,
},
};
// ❌ Bad: Hardcoded configuration
const badConfig = {
database: {
host: "prod-db.example.com", // Hardcoded!
password: "secretpassword", // Secret in code!
},
};
/**
* Backing Services - Treat backing services as attached resources
* - Database, cache, message queue, etc. are accessed via URLs
* - Should be swappable without code changes
*/
// ✅ Good: Backing service as URL
const db = createConnection(process.env.DATABASE_URL);
const cache = createClient(process.env.REDIS_URL);
// Can swap services by changing environment variable
// DATABASE_URL=postgresql://localhost/dev (local dev)
// DATABASE_URL=postgresql://prod-db/app (production)
/**
* Disposability - Fast startup and graceful shutdown
*/
function startServer() {
const server = app.listen(config.port, () => {
console.log(`Server started on port ${config.port}`);
});
// Graceful shutdown
process.on("SIGTERM", async () => {
console.log("SIGTERM received, shutting down gracefully");
server.close(() => {
console.log("HTTP server closed");
});
await db.close();
await cache.quit();
process.exit(0);
});
}
6. Configuration Validation
// config/validation.ts
import Joi from "joi";
const configSchema = Joi.object({
NODE_ENV: Joi.string()
.valid("development", "production", "test")
.default("development"),
PORT: Joi.number().port().default(3000),
DATABASE_URL: Joi.string().uri().required(),
REDIS_URL: Joi.string().uri().default("redis://localhost:6379"),
LOG_LEVEL: Joi.string()
.valid("debug", "info", "warn", "error")
.default("info"),
API_KEY: Joi.string().min(32).required(),
API_TIMEOUT: Joi.number().min(1000).max(30000).default(5000),
ENABLE_METRICS: Joi.boolean().default(false),
});
export function validateConfig() {
const { error, value } = configSchema.validate(process.env, {
allowUnknown: true, // Allow other env vars
stripUnknown: true, // Remove unknown vars
});
if (error) {
throw new Error(`Configuration validation error: ${error.message}`);
}
return value;
}
// Usage
const validatedConfig = validateConfig();
7. Dynamic Configuration (Remote Config)
// config/remote-config.ts
export class RemoteConfigService {
private config: Map<string, any> = new Map();
private pollInterval: NodeJS.Timeout | null = null;
constructor(private configServiceUrl: string) {}
async initialize() {
await this.fetchConfig();
this.startPolling();
}
private async fetchConfig() {
try {
const response = await fetch(`${this.configServiceUrl}/config`);
const config = await response.json();
for (const [key, value] of Object.entries(config)) {
const oldValue = this.config.get(key);
if (oldValue !== value) {
console.log(`Config changed: ${key} = ${value}`);
this.config.set(key, value);
}
}
} catch (error) {
console.error("Failed to fetch remote config:", error);
}
}
private startPolling() {
// Poll every 60 seconds
this.pollInterval = setInterval(() => {
this.fetchConfig();
}, 60000);
}
get(key: string, defaultValue?: any): any {
return this.config.get(key) ?? defaultValue;
}
stop() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
}
}
// Usage
const remoteConfig = new RemoteConfigService(
"https://config-service.example.com",
);
await remoteConfig.initialize();
app.get("/api/users", (req, res) => {
const pageSize = remoteConfig.get("api.users.pageSize", 20);
const enableCache = remoteConfig.get("api.users.enableCache", false);
// Use dynamic config values
});
Best Practices
✅ DO
- Store configuration in environment variables
- Use different config files per environment
- Validate configuration on startup
- Use secret managers for sensitive data
- Never commit secrets to version control
- Provide sensible defaults
- Document all configuration options
- Use type-safe configuration objects
- Implement configuration hierarchy (base + overrides)
- Use feature flags for gradual rollouts
- Follow 12-factor app principles
- Implement graceful degradation for missing config
- Cache secrets to reduce API calls
❌ DON'T
- Hardcode configuration in source code
- Commit .env files with real secrets
- Use different config formats across services
- Store secrets in plain text
- Expose configuration through APIs
- Use production credentials in development
- Ignore configuration validation errors
- Access process.env directly everywhere
- Store configuration in databases (circular dependency)
- Mix configuration with business logic
Common Patterns
Pattern 1: Config Service
export class ConfigService {
private static instance: ConfigService;
private config: Config;
private constructor() {
this.config = loadAndValidateConfig();
}
static getInstance(): ConfigService {
if (!ConfigService.instance) {
ConfigService.instance = new ConfigService();
}
return ConfigService.instance;
}
get<K extends keyof Config>(key: K): Config[K] {
return this.config[key];
}
}
Pattern 2: Configuration Builder
export class ConfigBuilder {
private config: Partial<Config> = {};
withDatabase(url: string): this {
this.config.database = { url };
return this;
}
withRedis(url: string): this {
this.config.redis = { url };
return this;
}
build(): Config {
return this.config as Config;
}
}
Tools & Resources
- dotenv: Load environment variables from .env files
- convict: Configuration management with validation
- config: Hierarchical configurations for Node.js
- AWS Secrets Manager: Cloud-based secret storage
- HashiCorp Vault: Secret and encryption management
- LaunchDarkly: Feature flag management
- ConfigCat: Feature flag and configuration service
- Consul: Service configuration and discovery