Skip to content

Chapter 2: Environment Secrets

Progress Indicator

Module 3: Going Live
├─ Chapter 1: Custom Domains
├─ Chapter 2: Environment Secrets     ← YOU ARE HERE
└─ Chapter 3: Monitoring & Logs

Learning Objectives

By the end of this chapter, you will: - Understand different types of secrets and sensitive data - Properly create and manage .env files - Use environment variables in Docker containers - Follow security best practices for production - Never accidentally commit secrets to version control

Prerequisites

  • Completed Chapter 1 (Custom Domains)
  • Basic understanding of environment variables
  • Text editor for configuration files
  • Docker and Docker Compose installed

Critical Security Rules

Before we begin, remember these non-negotiable security principles:

NEVER DO THIS ❌

❌ Commit .env files to version control
❌ Store secrets in docker-compose.yml directly
❌ Use weak or simple passwords
❌ Share API keys or database passwords
❌ Keep secrets in image layers
❌ Log sensitive information

ALWAYS DO THIS ✓

✓ Use .env files for local development only
✓ Use environment variables for secrets
✓ Use strong, unique passwords (20+ characters)
✓ Rotate secrets regularly
✓ Use secret management tools in production
✓ Mask secrets in logs

Step 1: Understand Types of Secrets

What Are Secrets?

Secrets are sensitive pieces of information that your application needs but should never be exposed publicly.

Common Types of Secrets

Secret Type Example Risk Level
Database Password MySuperSecureDBPass123! CRITICAL
API Keys sk_live_51234567890abc CRITICAL
JWT Secrets your-jwt-secret-key-min-32-chars CRITICAL
OAuth Credentials client_id, client_secret CRITICAL
Email Passwords SMTP credentials HIGH
Third-party Keys Stripe, SendGrid, etc. HIGH
Private Keys SSL certificates, SSH keys CRITICAL

Step 2: Create a .env File

Create Local .env File for Development

Create a .env file in your project root (Docker Compose directory):

# Navigate to your project directory
cd ~/my-app

# Create .env file
touch .env

# Edit with your preferred editor
nano .env

Example .env File

# Database Configuration
DATABASE_URL=postgresql://user:password@postgres:5432/myapp_db
DB_USER=postgres
DB_PASSWORD=SuperSecurePassword123!
DB_NAME=myapp_db

# Application Settings
NODE_ENV=production
DEBUG=false
LOG_LEVEL=info

# API Keys & Credentials
JWT_SECRET=your-very-long-jwt-secret-minimum-32-characters-long-key
API_KEY=sk_prod_1234567890abcdefghijklmnop
STRIPE_API_KEY=sk_live_51234567890abc
STRIPE_WEBHOOK_SECRET=whsec_1234567890abc

# Third-party Services
SENDGRID_API_KEY=SG.1234567890abcdefghijklmnop
SENDGRID_FROM_EMAIL=noreply@myapp.com

# OAuth
GITHUB_CLIENT_ID=Iv1.1234567890ab
GITHUB_CLIENT_SECRET=ghp_1234567890abcdefghijklmnopqrst

# Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-specific-password

# URLs & Hostnames
APP_URL=https://myapp.com
API_URL=https://api.myapp.com

Step 3: Protect Your .env File

Add .env to .gitignore

CRITICAL: Ensure .env files are never committed to version control!

# Edit .gitignore
nano .gitignore

Add these lines:

# Environment variables - NEVER commit these!
.env
.env.local
.env.*.local
.env.production
.env.staging

# IDE
.vscode/
.idea/
*.swp
*.swo

# Dependencies
node_modules/
__pycache__/

# Build artifacts
dist/
build/

Verify .env is Ignored

# Check if .env would be committed
git status

# Should show: nothing to commit
# .env should NOT appear in the list

# Double-check
git check-ignore .env
# Should output: .env

Create .env.example for Documentation

Create a template file that shows what variables are needed (without real values):

touch .env.example
# Database Configuration
DATABASE_URL=postgresql://user:password@postgres:5432/database_name
DB_USER=postgres
DB_PASSWORD=your_secure_password_here
DB_NAME=database_name

# Application Settings
NODE_ENV=production
DEBUG=false
LOG_LEVEL=info

# API Keys & Credentials
JWT_SECRET=your-very-long-jwt-secret-minimum-32-characters-long
API_KEY=your_api_key_here
STRIPE_API_KEY=sk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx

# Third-party Services
SENDGRID_API_KEY=SG.xxxxx
SENDGRID_FROM_EMAIL=noreply@yourdomain.com

# OAuth
GITHUB_CLIENT_ID=xxxxx
GITHUB_CLIENT_SECRET=xxxxx

# Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password

# URLs & Hostnames
APP_URL=https://yourdomain.com
API_URL=https://api.yourdomain.com

Step 4: Use .env in Docker Compose

Reference Environment Variables

Update your docker-compose.yml to use variables from .env:

version: '3.8'

services:
  database:
    image: postgres:15
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app-network

  app:
    build: .
    environment:
      NODE_ENV: ${NODE_ENV}
      DATABASE_URL: ${DATABASE_URL}
      JWT_SECRET: ${JWT_SECRET}
      API_KEY: ${API_KEY}
      STRIPE_API_KEY: ${STRIPE_API_KEY}
      SENDGRID_API_KEY: ${SENDGRID_API_KEY}
      GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
      GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
      SMTP_HOST: ${SMTP_HOST}
      SMTP_PORT: ${SMTP_PORT}
      SMTP_USER: ${SMTP_USER}
      SMTP_PASSWORD: ${SMTP_PASSWORD}
      APP_URL: ${APP_URL}
    depends_on:
      - database
    networks:
      - app-network
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`${APP_DOMAIN}`)"

volumes:
  postgres_data:

networks:
  app-network:

Docker Compose Automatically Loads .env

When you run docker-compose up, Docker Compose automatically: 1. Looks for a .env file in the current directory 2. Loads all variables from that file 3. Makes them available to services via the ${VARIABLE_NAME} syntax

# Docker Compose automatically uses .env
docker-compose up -d

# Verify variables are loaded
docker-compose exec app env | grep DATABASE_URL

Step 5: Generate Strong Passwords & Keys

Create Strong Secrets

For passwords and secrets, use cryptographically secure generation:

# Generate a strong random password (32 characters)
openssl rand -base64 32

# Generate a JWT secret
openssl rand -hex 32

# Generate API keys (use this format for consistency)
head -c 32 /dev/urandom | base64

Password Best Practices

Requirement Example Why
Length 20+ characters Harder to crack
Complexity Mix case, numbers, symbols Increases entropy
Uniqueness Different for each secret Limits damage if one leaks
Rotation Change quarterly Reduces compromise window

Bad vs Good Examples

❌ BAD: password123
❌ BAD: admin@123
❌ BAD: Password123

✓ GOOD: aB9xK2$mP7qL@nW5vD3xF8tY
✓ GOOD: Tr0p!cal$un$et#2024%Secure
✓ GOOD: Base64EncodedRandomData==

Step 6: Security Best Practices for Production

Never Log Secrets

// ❌ BAD - Logs the secret!
console.log('Connecting with password:', dbPassword);

// ✓ GOOD - Only logs connection status
console.log('Database connection established');

// ✓ GOOD - Masks sensitive parts
console.log('Connecting to:', maskSecret(databaseUrl));

function maskSecret(str) {
  const parts = str.split(':');
  if (parts.length > 2) {
    parts[2] = '***';
  }
  return parts.join(':');
}

Rotate Secrets Regularly

Schedule for secret rotation: - Database passwords: Every 90 days - API keys: Every 180 days - JWT secrets: Every year (or when compromised) - Third-party keys: Check provider recommendations

Least Privilege Principle

# ❌ BAD - Database user has full access
DATABASE_URL=postgresql://admin:password@db:5432/myapp

# ✓ GOOD - Application user has limited permissions
DATABASE_URL=postgresql://app_user:password@db:5432/myapp
# app_user only has SELECT, INSERT, UPDATE permissions on application tables

Separate Secrets by Environment

.env              # Development (never commit)
.env.example      # Template (commit this)
.env.staging      # Staging (never commit, separate secure storage)
.env.production   # Production (never commit, use secret manager)

Production Secret Management

For production, instead of .env files, use:

Docker Secrets (Swarm mode):

# Create a secret
echo "SuperSecurePassword123!" | docker secret create db_password -

# Reference in docker-compose.yml
secrets:
  db_password:
    external: true

services:
  app:
    secrets:
      - db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password

Environment Variables (Cloud Platforms):

AWS Lambda → Environment Variables
Heroku → Config Variables
DigitalOcean → Environment Variables

Secret Managers:

HashiCorp Vault
AWS Secrets Manager
Azure Key Vault
Google Secret Manager

Step 7: Verify Your Configuration

Check Environment Variables in Container

# See all environment variables in running container
docker-compose exec app env

# Filter for specific variables
docker-compose exec app env | grep DATABASE_URL
docker-compose exec app env | grep API_KEY

Test Database Connection

# If using PostgreSQL
docker-compose exec app psql -h database -U ${DB_USER} -d ${DB_NAME} -c "SELECT 1;"

# If using MySQL
docker-compose exec app mysql -h database -u ${DB_USER} -p${DB_PASSWORD} ${DB_NAME} -e "SELECT 1;"

Application Startup

# View application logs to see if all variables are loaded
docker-compose logs app

# Look for successful initialization messages
# Error: Missing environment variable → Check .env file
# Error: Connection refused → Check database credentials

Troubleshooting

Variables Not Loading

Problem: docker-compose exec app env shows empty variables

Solutions:

# Verify .env file exists in correct location
ls -la .env

# Rebuild container to load new variables
docker-compose down
docker-compose up -d

# Check for syntax errors in .env (no spaces around =)
cat .env | grep "DATABASE_URL="

Accidentally Committed Secret?

Problem: Realized .env file was committed to Git

Solutions:

# Remove the file from Git history (destructive!)
git rm --cached .env
git commit --amend --no-edit

# For already pushed commits, the secret is compromised
# Immediately: Rotate all secrets in the .env file
# Then: Force-push (only if not shared with team)

Authentication Failed

Problem: docker-compose exec app fails to authenticate with database

Solutions:

# Verify credentials in .env
cat .env | grep DB_

# Test with manual connection
docker-compose exec database psql -U ${DB_USER} -d ${DB_NAME}

# Check database service is running
docker-compose ps database

AI Prompts for This Lesson

Environment File Generation

Generate .env File for My Stack
I'm building a full-stack application with this tech stack:

Backend: [Node.js/Python/Ruby/etc]
Framework: [Express/Django/Rails/etc]
Database: [PostgreSQL/MySQL/MongoDB]
Additional services: [Redis, SendGrid, Stripe, etc]

Generate a complete .env.example file that includes:
1. Database connection strings
2. Authentication secrets (JWT, session, etc)
3. API keys for common services
4. Email/SMTP configuration
5. OAuth provider credentials
6. Application-specific settings

Include comments explaining what each variable does.
Migrate from Hardcoded Values
I have configuration hardcoded in my application and want to move to .env.

Current configuration file:
[paste your config.js, settings.py, etc]

Convert this to:
1. .env file format
2. .env.example template
3. Updated code to read from environment variables

Technology: [Node.js/Python/etc]

Secrets Generation

Generate Strong Secrets
I need to generate secure secrets for production deployment.

Generate cryptographically secure values for:
1. JWT_SECRET (minimum 32 characters)
2. DATABASE_PASSWORD (PostgreSQL)
3. SESSION_SECRET
4. API_KEY (custom format)
5. ENCRYPTION_KEY

Provide the openssl commands to generate each one.
Also include strength requirements and best practices.
Check If Secrets Are Strong Enough
Are these secrets secure enough for production?

JWT_SECRET=[your current secret]
DATABASE_PASSWORD=[your current password]
API_KEY=[your current key]

For each one, tell me:
1. Is it strong enough? (Yes/No)
2. What's the entropy/strength level?
3. How to improve it if weak
4. Command to generate a better one

Docker Compose Integration

Add .env Support to docker-compose.yml
I have this docker-compose.yml with hardcoded values:
[paste your docker-compose.yml]

Convert it to use .env variables:
1. Replace hardcoded values with ${VARIABLE} syntax
2. Generate the corresponding .env file
3. Generate .env.example template
4. Add .gitignore rules

Make sure to include all services and volumes.
Environment Variables Not Loading
My environment variables aren't being loaded in Docker.

My docker-compose.yml environment section:
[paste your environment config]

My .env file location: [path]

Output from `docker-compose config`:
[paste output]

Output from `docker-compose exec app env | grep DATABASE`:
[paste output]

Why aren't the variables being loaded?

Database Security

Create Limited Database User
I need to create a database user with minimal permissions.

Database: PostgreSQL 15
Database name: myapp_production
Tables: users, posts, comments, sessions

Create a user that can:
- SELECT, INSERT, UPDATE, DELETE on application tables
- Cannot DROP tables or ALTER schema
- Cannot access other databases

Provide:
1. SQL commands to create the user
2. Grant appropriate permissions
3. Connection string for .env
4. How to verify permissions are correct
Secure MySQL Configuration
Generate secure MySQL user setup for production.

Database: myapp_db
Required access: Read/Write on app tables only

Generate:
1. MySQL commands to create limited user
2. Grant statements with minimum permissions
3. .env file configuration
4. Verification commands

Also include security best practices for MySQL in production.

OAuth & API Keys

Setup OAuth Environment Variables
I'm integrating OAuth authentication.

Providers: [GitHub, Google, Facebook, etc]
Callback URL: https://myapp.com/auth/callback

Generate:
1. Required .env variables for each provider
2. .env.example template
3. Explanation of each variable (client_id, client_secret, etc)
4. Security notes for OAuth secrets

Framework: [Next.js/Express/Django/etc]
API Key Best Practices
I have these API keys to manage:

Services:
- Stripe (payment processing)
- SendGrid (email)
- AWS S3 (file storage)
- OpenAI (AI features)
- Google Maps (location)

Help me:
1. Organize them in .env properly
2. Use test vs production keys correctly
3. Set up key rotation schedule
4. Implement secure key storage
5. Add monitoring for key usage

Generate complete .env structure and best practices.

Security Verification

Check for Leaked Secrets
I'm worried I might have committed secrets to git.

Repository: [your repo]

Help me:
1. Search git history for potential secrets
2. Commands to check for exposed .env files
3. What to do if secrets are found in history
4. How to rotate compromised credentials
5. Prevent this in the future

Provide git commands and security checklist.
Production Security Checklist
I'm about to deploy to production.

Generate a security checklist for environment variables:
1. .env file is in .gitignore
2. All secrets are strong enough
3. No default/example values in production
4. Secrets are different from development
5. Backup/recovery plan exists

Include verification commands for each item.
Technology: Docker Compose with [stack]

Secret Rotation

Rotate Production Secrets
I need to rotate my production secrets.

Current setup:
- Database: PostgreSQL
- Application: Node.js running in Docker
- Services: 3 containers (app, db, redis)
- Secrets to rotate: DB password, JWT secret, API keys

Provide step-by-step instructions:
1. Generate new secrets
2. Update .env file
3. Restart services without downtime
4. Verify everything still works
5. Deactivate old secrets

Zero-downtime approach preferred.

Troubleshooting

Application Can't Read Environment Variables
My app can't read environment variables from .env.

Technology: [Node.js/Python/etc]
Error message:
[paste error]

My .env file:
[paste relevant lines, REMOVE actual secret values]

My code trying to read variables:
[paste code snippet]

Output from `docker-compose exec app env`:
[paste output]

What's wrong with my setup?
Different Values in Dev vs Production
I want to use different .env values for development and production.

Setup:
- Local development (Docker Compose)
- Production server (Docker Compose)

How do I:
1. Manage multiple .env files
2. Prevent mixing up environments
3. Keep .env.production secure
4. Switch between environments easily
5. Document which values go where

Provide file structure and best practices.

Important Reminders

Action Frequency Why
Review .gitignore Every release Ensure secrets aren't committed
Rotate secrets Every 90-180 days Reduce compromise window
Review logs Weekly Catch any secret leaks
Update dependencies Monthly Security patches
Audit access Quarterly Remove old credentials

What's Next

Excellent! Your secrets are now secure. Next, you need to:

Chapter 3: Monitoring & Logs

Learn how to monitor your application's health, access logs, debug issues, and set up alerts for production.

Help & Support

  • Secret Generation? Use openssl rand -base64 32 for strong random values
  • Password Requirements? Aim for 20+ characters with mixed case, numbers, and symbols
  • Already Leaked a Secret? Rotate it immediately, even if in development
  • Production Secrets? Use Docker Secrets or cloud-native secret managers

Never commit secrets. When in doubt, use a secret manager.