Environment Variables & Secrets Management
Proper secrets management is critical for security and compliance. This guide covers how to handle sensitive configuration data across development and production environments.
Core Principles
- Never commit secrets to version control
- Use environment-specific configuration
- Implement least-privilege access
- Rotate secrets regularly
Local Development
Environment Configuration Files
We use Docker Compose with environment variables for local development. The configuration is structured as follows:
docker-compose.yml (Template)
services:
api:
build:
context: ./api
dockerfile: dev.Dockerfile
environment:
# External API Keys
API_KEY_1: your-api-key
API_KEY_2: your-api-key-2
# Authentication
OAUTH_KEY: your-oauth-key
CLIENT_ID: your-client-id
# Database
DATABASE_URL: connection-string
# Redis
REDIS_HOST: cache
REDIS_PORT: 6379
# Environment
NODE_ENV: development
LOG_LEVEL: debug
Git Configuration
Never commit these files:
docker-compose.yml(with actual secrets).env.env.local.env.production- Any file containing actual secret values
Always commit these files:
docker-compose.dist.yml- Template showing required variables.env.example- Template showing required variables.env.dist- Distribution template (same as .env.example)
.gitignore Configuration
# Environment files
.env
.env.local
.env.production
.env.staging
# Docker Compose with secrets
docker-compose.yml
# Keep templates
!.env.example
!.env.dist
!docker-compose.dist.yml
Project Setup Instructions
1. Initial Setup
When setting up a new project:
# Copy the docker-compose template
cp docker-compose.dist.yml docker-compose.yml
# Edit with your actual values
nano docker-compose.yml
# Start the application with Docker Compose
docker compose up -d
2. README Documentation
Include a section in your README.md:
## Environment Setup
1. Copy `docker-compose.dist.yml` to `docker-compose.yml`
2. Fill in your local development values
3. Never commit `docker-compose.yml` to version control
### Required Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@postgres/db` |
| `API_KEY` | API key | `your-api-key` |
| `OAUTH_KEY` | OAuth authentication key | `your-oauth-key` |
| `CLIENT_ID` | Client ID for authentication | `your-client-id` |
### Optional Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `LOG_LEVEL` | Logging verbosity | `debug` |
| `NODE_ENV` | Environment mode | `development` |
Production Deployment
Google Cloud Platform (GCP)
Secret Manager
# Create secrets via CLI
gcloud secrets create DATABASE_URL --data-file=./database-url.txt
gcloud secrets create JWT_SECRET --data-file=./jwt-secret.txt
# Grant access to service account
gcloud secrets add-iam-policy-binding DATABASE_URL \
--member="serviceAccount:my-app@my-project.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
Note: You can also create and manage secrets through the Google Cloud Console UI at https://console.cloud.google.com/security/secret-manager
Cloud Run / App Engine
# app.yaml
env_variables:
NODE_ENV: production
# Other non-sensitive variables
# Sensitive variables are injected via Secret Manager
Digital Ocean
App Platform Environment Variables
# .do/app.yaml
name: my-app
services:
- name: web
environment_slug: node-js
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
scope: RUN_AND_BUILD_TIME
type: SECRET
value: ${DATABASE_URL_SECRET}
Security Best Practices
1. Secret Rotation
Rotate secrets on a defined cadence — shorter intervals for high-impact, machine-issued credentials (JWT signing keys, service account keys), longer for stable credentials (database passwords, long-lived integration keys). The exact schedule per credential type is documented in our internal Ops runbook.
Always rotate immediately when:
- A team member with access to the secret leaves
- A leak or suspected leak is detected
- A dependency or third party that handled the secret is compromised
2. Access Control
# Example: GCP IAM roles for secret access
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:app@my-project.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
Common Patterns
1. Configuration Validation
// config/validation.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'production', 'test']),
});
export const validateEnv = () => {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('❌ Invalid environment variables:', result.error.flatten().fieldErrors);
process.exit(1);
}
return result.data;
};
2. Development vs Production
// Example: Different database URLs
const getDatabaseUrl = () => {
if (process.env.NODE_ENV === 'production') {
return process.env.DATABASE_URL;
}
// Development: Use local database
return process.env.DATABASE_URL || 'postgresql://localhost:5432/dev_db';
};