Skip to content

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)

version: '3.7'

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

  • JWT Secrets: Rotate every 90 days
  • API Keys: Rotate every 6 months
  • Database Passwords: Rotate every 12 months
  • Service Account Keys: Rotate every 90 days

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';
};