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!
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):
# 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):
Secret Managers:
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
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 32for 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.