Day 13: Environment Variables and Configuration
Learning Objectives: By the end of this lesson, you will be able to:
- Master environment variable management in Docker containers
- Implement secure configuration practices for sensitive data
- Use various methods for passing configuration to containers
- Design flexible, environment-specific container configurations
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Imagine you’re running the same application in three different environments: development on your laptop, testing on a staging server, and production in the cloud. Each needs different database connections, API keys, and settings. How do you manage this without rebuilding your container for each environment? The answer is smart configuration management!
Lesson Overview
- Duration: 50-60 minutes
- Prerequisites: Completion of Days 11-12, understanding of basic application configuration concepts
- Materials Needed: Docker installed, text editor, sample applications with configuration needs
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Environment Variable Fundamentals - How Docker handles environment variables
- Configuration Methods - Different ways to pass configuration to containers
- Security Best Practices - Protecting sensitive configuration data
- Advanced Patterns - Configuration files, templates, and runtime configuration
- Troubleshooting - Debugging configuration issues
Success Criteria:
By the end of this session, you should be able to design and implement robust configuration strategies for any containerized application, from simple web apps to complex distributed systems.
3. Foundation Knowledge (15-20 minutes)
Environment Variables in Containers
Environment variables are key-value pairs that provide configuration data to applications running inside containers. They’re the primary way to make containers configurable without modifying the image itself.
Real-World Analogy: Think of environment variables like the settings panel on your smartphone. The same phone model can behave differently based on your personal settings - language, timezone, notification preferences. Similarly, the same container image can behave differently based on environment variables.
Why Environment Variables Matter:
- Portability: Same image works across different environments
- Security: Sensitive data can be injected at runtime
- Flexibility: Configuration without rebuilding images
- 12-Factor App Compliance: Industry standard for cloud-native applications
Container Environment Inheritance
Containers inherit environment variables from:
- Dockerfile ENV instructions
- Docker run -e flags
- Docker run –env-file
- Host environment (with –env flag)
- Docker Compose environment sections
Configuration Hierarchy
Host Environment Variables
↓
Dockerfile ENV
↓
docker run -e / --env-file
↓
Application Defaults
Key Terminology
- Environment Variable: A key-value pair providing configuration data
- ENV Instruction: Dockerfile command that sets environment variables in the image
- Runtime Environment: Variables set when container starts
- Environment File: File containing multiple environment variable definitions
- Secret: Sensitive configuration data requiring special handling
4. Detailed Explanation and Examples (25-30 minutes)
Setting Environment Variables
Dockerfile ENV Instruction
# Set environment variables in image
FROM node:18-alpine
ENV NODE_ENV=production
ENV PORT=3000
ENV LOG_LEVEL=info
# Multiple variables in one instruction
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=info
WORKDIR /app
COPY . .
CMD ["npm", "start"]
Runtime Variable Setting
# Single variable
docker run -e NODE_ENV=development myapp
# Multiple variables
docker run \
-e NODE_ENV=development \
-e PORT=3000 \
-e DATABASE_URL=postgresql://localhost:5432/mydb \
myapp
# From host environment
export DATABASE_URL="postgresql://localhost:5432/mydb"
docker run -e DATABASE_URL myapp
# All host environment variables (dangerous!)
docker run --env HOST_VAR myapp
Environment Files
# Create environment file
cat > .env << EOF
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
REDIS_URL=redis://redis:6379
LOG_LEVEL=info
EOF
# Use environment file
docker run --env-file .env myapp
Configuration Patterns
Database Configuration
# Development environment
docker run \
-e NODE_ENV=development \
-e DATABASE_HOST=localhost \
-e DATABASE_PORT=5432 \
-e DATABASE_NAME=myapp_dev \
-e DATABASE_USER=developer \
-e DATABASE_PASSWORD=devpass \
myapp
# Production environment
docker run \
-e NODE_ENV=production \
-e DATABASE_HOST=prod-db.company.com \
-e DATABASE_PORT=5432 \
-e DATABASE_NAME=myapp_prod \
-e DATABASE_USER=app_user \
-e DATABASE_PASSWORD=secure_prod_password \
myapp
Application-Specific Configuration
// Example Node.js application reading environment variables
const config = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
database: {
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT) || 5432,
name: process.env.DATABASE_NAME || 'myapp',
user: process.env.DATABASE_USER || 'postgres',
password: process.env.DATABASE_PASSWORD || ''
},
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379'
},
logging: {
level: process.env.LOG_LEVEL || 'info'
},
features: {
enableAuth: process.env.ENABLE_AUTH === 'true',
maxUploadSize: parseInt(process.env.MAX_UPLOAD_SIZE) || 10485760
}
};
module.exports = config;
Advanced Configuration Methods
Configuration File Mounting
# Mount configuration file
docker run \
-v $(pwd)/config.json:/app/config/config.json:ro \
-e CONFIG_FILE=/app/config/config.json \
myapp
# Mount entire config directory
docker run \
-v $(pwd)/config:/app/config:ro \
myapp
Template-Based Configuration
# Dockerfile with configuration template
FROM node:18-alpine
# Install envsubst for template substitution
RUN apk add --no-cache gettext
# Copy configuration template
COPY config.template.json /app/config.template.json
# Copy startup script
COPY start.sh /app/start.sh
RUN chmod +x /app/start.sh
CMD ["/app/start.sh"]
# start.sh - substitute environment variables in template
#!/bin/sh
envsubst < /app/config.template.json > /app/config.json
exec node server.js
// config.template.json
{
"database": {
"host": "${DATABASE_HOST}",
"port": ${DATABASE_PORT},
"name": "${DATABASE_NAME}"
},
"features": {
"enableAuth": ${ENABLE_AUTH:-false},
"debugMode": ${DEBUG_MODE:-false}
}
}
Multi-Stage Configuration
# Build stage with build-time configuration
FROM node:18-alpine AS builder
ARG BUILD_ENV=production
ENV NODE_ENV=$BUILD_ENV
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Runtime stage with runtime configuration
FROM node:18-alpine AS runtime
WORKDIR /app
# Copy built application
COPY --from=builder /app/node_modules ./node_modules
COPY . .
# Runtime environment variables
ENV NODE_ENV=production
ENV PORT=3000
CMD ["npm", "start"]
Common Misconceptions
-
Misconception: “Environment variables are always the best way to pass configuration” Reality: For complex configurations, files or configuration services might be better
-
Misconception: “Environment variables in Dockerfile are secure” Reality: Dockerfile ENV values are visible in image layers and should not contain secrets
-
Misconception: “All configuration should use environment variables” Reality: Some configuration (like large JSON structures) is better handled through files
5. Live Coding Demonstration (20-25 minutes)
Example 1: Multi-Environment Web Application
// app.js - Environment-aware Express application
const express = require('express');
const app = express();
// Configuration from environment variables
const config = {
port: process.env.PORT || 3000,
env: process.env.NODE_ENV || 'development',
database: {
url: process.env.DATABASE_URL || 'sqlite://memory.db'
},
external_apis: {
weather_api_key: process.env.WEATHER_API_KEY || '',
payment_api_url: process.env.PAYMENT_API_URL || 'http://localhost:8080'
},
features: {
enable_caching: process.env.ENABLE_CACHING === 'true',
enable_logging: process.env.ENABLE_LOGGING !== 'false',
max_file_size: parseInt(process.env.MAX_FILE_SIZE) || 1048576
}
};
// Middleware for logging (if enabled)
if (config.features.enable_logging) {
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
});
}
// Routes
app.get('/', (req, res) => {
res.json({
message: 'Configuration Demo App',
environment: config.env,
config_summary: {
port: config.port,
database_configured: !!config.database.url,
caching_enabled: config.features.enable_caching,
logging_enabled: config.features.enable_logging,
max_file_size_mb: config.features.max_file_size / 1048576
}
});
});
app.get('/config', (req, res) => {
// Return configuration (hide sensitive data)
const safeConfig = JSON.parse(JSON.stringify(config));
if (safeConfig.external_apis.weather_api_key) {
safeConfig.external_apis.weather_api_key = '***';
}
res.json(safeConfig);
});
app.listen(config.port, () => {
console.log(`Server running on port ${config.port} in ${config.env} mode`);
});
# Dockerfile for the configuration demo
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy application
COPY app.js .
# Default environment variables (can be overridden at runtime)
ENV NODE_ENV=production
ENV PORT=3000
ENV ENABLE_LOGGING=true
ENV ENABLE_CACHING=false
ENV MAX_FILE_SIZE=1048576
EXPOSE 3000
CMD ["node", "app.js"]
# Build the image
docker build -t config-demo .
# Run in development mode
docker run -d --name dev-app \
-p 3001:3000 \
-e NODE_ENV=development \
-e ENABLE_CACHING=false \
-e ENABLE_LOGGING=true \
-e DATABASE_URL=postgresql://localhost:5432/myapp_dev \
config-demo
# Run in production mode
docker run -d --name prod-app \
-p 3002:3000 \
-e NODE_ENV=production \
-e ENABLE_CACHING=true \
-e ENABLE_LOGGING=false \
-e DATABASE_URL=postgresql://prod-server:5432/myapp_prod \
-e WEATHER_API_KEY=secret-api-key \
config-demo
# Test both environments
curl http://localhost:3001/config # Development config
curl http://localhost:3002/config # Production config
Explanation:
- Same image behaves differently based on environment variables
- Sensitive data (API keys) can be injected at runtime
- Configuration is validated and defaults are provided
- Different environments can have different feature flags
Example 2: Database-Driven Application with Secrets Management
# Create environment files for different stages
# development.env
cat > development.env << EOF
NODE_ENV=development
PORT=3000
DATABASE_HOST=dev-postgres
DATABASE_PORT=5432
DATABASE_NAME=myapp_dev
DATABASE_USER=developer
DATABASE_PASSWORD=devpass123
REDIS_URL=redis://dev-redis:6379
LOG_LEVEL=debug
ENABLE_DEBUG_ROUTES=true
EOF
# staging.env
cat > staging.env << EOF
NODE_ENV=staging
PORT=3000
DATABASE_HOST=staging-postgres
DATABASE_PORT=5432
DATABASE_NAME=myapp_staging
DATABASE_USER=staging_user
DATABASE_PASSWORD=staging_secure_pass
REDIS_URL=redis://staging-redis:6379
LOG_LEVEL=info
ENABLE_DEBUG_ROUTES=false
EOF
# production.env (without sensitive data)
cat > production.env << EOF
NODE_ENV=production
PORT=3000
DATABASE_HOST=prod-postgres
DATABASE_PORT=5432
DATABASE_NAME=myapp_prod
DATABASE_USER=app_user
LOG_LEVEL=warn
ENABLE_DEBUG_ROUTES=false
EOF
# Create network for services
docker network create app-network
# Run PostgreSQL for development
docker run -d --name dev-postgres \
--network app-network \
-e POSTGRES_PASSWORD=devpass123 \
-e POSTGRES_DB=myapp_dev \
-e POSTGRES_USER=developer \
postgres:13
# Run Redis for development
docker run -d --name dev-redis \
--network app-network \
redis:alpine
# Run application with development config
docker run -d --name dev-app \
--network app-network \
-p 3000:3000 \
--env-file development.env \
config-demo
# Test the setup
curl http://localhost:3000/config
Example 3: Configuration Validation and Error Handling
// config-validator.js
class ConfigValidator {
static validate(config) {
const errors = [];
// Required fields
if (!config.database.url && (!config.database.host || !config.database.name)) {
errors.push('Database configuration incomplete');
}
// Port validation
if (config.port < 1 || config.port > 65535) {
errors.push('Port must be between 1 and 65535');
}
// Environment validation
const validEnvs = ['development', 'staging', 'production'];
if (!validEnvs.includes(config.env)) {
errors.push(`NODE_ENV must be one of: ${validEnvs.join(', ')}`);
}
// API key validation for production
if (config.env === 'production' && !config.external_apis.weather_api_key) {
errors.push('WEATHER_API_KEY is required in production');
}
return {
isValid: errors.length === 0,
errors
};
}
static logConfiguration(config) {
console.log('=== Application Configuration ===');
console.log(`Environment: ${config.env}`);
console.log(`Port: ${config.port}`);
console.log(`Database: ${config.database.url ? 'Configured' : 'Not configured'}`);
console.log(`Caching: ${config.features.enable_caching ? 'Enabled' : 'Disabled'}`);
console.log(`Logging: ${config.features.enable_logging ? 'Enabled' : 'Disabled'}`);
console.log('================================');
}
}
// Add to app.js
const validation = ConfigValidator.validate(config);
if (!validation.isValid) {
console.error('Configuration validation failed:');
validation.errors.forEach(error => console.error(`- ${error}`));
process.exit(1);
}
ConfigValidator.logConfiguration(config);
Debugging Configuration Issues
# Check environment variables in running container
docker exec <container> env
# Check specific variable
docker exec <container> echo $NODE_ENV
# Run container with interactive shell to debug
docker run -it --env-file .env config-demo sh
# Inside container, check variables
env | grep NODE
echo $DATABASE_URL
6. Hands-On Practice (30-40 minutes)
Exercise 1: Multi-Tier Application Configuration (20 minutes)
Objective: Configure a complete three-tier application with environment-specific settings
Requirements: Create environment configurations for a web application stack:
- Frontend: React app with API endpoint configuration
- Backend: Express API with database and external service configuration
- Database: PostgreSQL with different settings per environment
Instructions:
- Create three environment files: dev.env, staging.env, prod.env
- Each environment should have different:
- Database credentials and connection strings
- API rate limits and timeout values
- Feature flags (enable/disable features per environment)
- Logging levels and debug settings
- Implement configuration validation
- Test each environment setup
Frontend Configuration Template:
// frontend/src/config.js
const config = {
API_BASE_URL: process.env.REACT_APP_API_URL || 'http://localhost:3000',
ENABLE_ANALYTICS: process.env.REACT_APP_ENABLE_ANALYTICS === 'true',
DEBUG_MODE: process.env.REACT_APP_DEBUG_MODE === 'true',
VERSION: process.env.REACT_APP_VERSION || '1.0.0'
};
export default config;
Backend Configuration Template:
// backend/config.js
module.exports = {
server: {
port: parseInt(process.env.PORT) || 3000,
cors_origin: process.env.CORS_ORIGIN || '*'
},
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
name: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || '',
pool_min: parseInt(process.env.DB_POOL_MIN) || 2,
pool_max: parseInt(process.env.DB_POOL_MAX) || 10
},
external_services: {
payment_api_url: process.env.PAYMENT_API_URL,
payment_api_key: process.env.PAYMENT_API_KEY,
email_service_key: process.env.EMAIL_SERVICE_KEY
},
features: {
enable_caching: process.env.ENABLE_CACHING === 'true',
enable_rate_limiting: process.env.ENABLE_RATE_LIMITING !== 'false',
max_upload_size: parseInt(process.env.MAX_UPLOAD_SIZE) || 5242880
},
logging: {
level: process.env.LOG_LEVEL || 'info',
enable_request_logging: process.env.ENABLE_REQUEST_LOGGING !== 'false'
}
};
Exercise 2: Secrets Management Challenge (10 minutes)
Objective: Implement secure handling of sensitive configuration data
Challenge: Design a system that:
- Never stores secrets in Docker images
- Uses different methods for different types of secrets
- Implements secret rotation capabilities
- Provides audit logging for secret access
Methods to implement:
- Environment variables for non-sensitive config
- Docker secrets for sensitive data (simulated with bind mounts)
- External secret management integration (vault, AWS Secrets Manager simulation)
Starter Configuration:
# Create secrets directory
mkdir -p secrets
# Create secret files
echo "super-secret-api-key" > secrets/api_key
echo "database-password-123" > secrets/db_password
echo "jwt-signing-secret" > secrets/jwt_secret
# Run container with secret mounts
docker run -d --name secure-app \
-v $(pwd)/secrets:/run/secrets:ro \
-e SECRET_API_KEY_FILE=/run/secrets/api_key \
-e SECRET_DB_PASSWORD_FILE=/run/secrets/db_password \
-e SECRET_JWT_SECRET_FILE=/run/secrets/jwt_secret \
myapp
Exercise 3: Configuration Template System (10 minutes)
Objective: Create a flexible configuration template system
Requirements:
- Use configuration templates with variable substitution
- Support default values and conditional configuration
- Validate configuration after template processing
- Generate different configurations for different deployment targets
Template Example:
# config.template.yaml
server:
port: ${PORT:-3000}
host: ${HOST:-0.0.0.0}
database:
host: ${DB_HOST:-localhost}
port: ${DB_PORT:-5432}
name: ${DB_NAME:-myapp_${NODE_ENV:-development}}
ssl: ${DB_SSL:-false}
features:
analytics: ${ENABLE_ANALYTICS:-false}
debug: ${DEBUG_MODE:-${NODE_ENV:-development == "development"}}
rate_limiting:
enabled: ${RATE_LIMITING_ENABLED:-true}
requests_per_minute: ${RATE_LIMIT_RPM:-100}
Processing Script:
#!/bin/bash
# process-config.sh
# Set default values
export NODE_ENV=${NODE_ENV:-development}
export PORT=${PORT:-3000}
export DB_HOST=${DB_HOST:-localhost}
# Process template
envsubst < config.template.yaml > config.yaml
# Validate configuration
if ! yaml-validator config.yaml; then
echo "Configuration validation failed"
exit 1
fi
echo "Configuration processed successfully"
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
- Problem: Secrets visible in Docker history or logs Cause: Using ENV in Dockerfile or -e for sensitive data Solution: Use runtime secrets management
# ❌ Secret in image layer
ENV API_KEY=secret-key-123
# ✅ Secret file mounted at runtime
COPY load-secrets.sh /app/
CMD ["/app/load-secrets.sh"]
# ❌ Secret in command line (visible in ps, history)
docker run -e PASSWORD=secret123 myapp
# ✅ Secret from file
echo "secret123" | docker run -i -e PASSWORD_STDIN=true myapp
- Problem: Configuration not updating when environment changes Cause: Configuration cached or not reading environment variables properly Solution: Ensure configuration is read at runtime, not build time
// ❌ Configuration read once at module load
const config = {
port: process.env.PORT || 3000
};
// ✅ Configuration function read when needed
function getConfig() {
return {
port: process.env.PORT || 3000
};
}
- Problem: Type conversion issues with environment variables Cause: All environment variables are strings Solution: Proper type conversion and validation
// ❌ String comparison
if (process.env.ENABLE_FEATURE === true) // Always false!
// ✅ Proper boolean conversion
if (process.env.ENABLE_FEATURE === 'true')
// ✅ Better boolean conversion with default
function parseBool(value, defaultValue = false) {
if (value === undefined) return defaultValue;
return value.toLowerCase() === 'true';
}
const enableFeature = parseBool(process.env.ENABLE_FEATURE, false);
- Problem: Configuration not working in Docker Compose vs docker run Cause: Different environment variable precedence and file handling Solution: Understand Docker Compose environment variable handling
# docker-compose.yml
services:
app:
image: myapp
environment:
- NODE_ENV=production # Highest precedence
env_file:
- .env # Lower precedence
Debugging Configuration
# Check all environment variables in container
docker exec <container> env | sort
# Check specific variable and its source
docker exec <container> printenv NODE_ENV
# Compare host vs container environment
env | grep NODE_ENV
docker exec <container> env | grep NODE_ENV
# Test configuration loading in container
docker exec <container> node -e "console.log(require('./config'))"
# Check file-based configuration
docker exec <container> cat /app/config.json
# Validate configuration format
docker exec <container> node -e "
try {
const config = require('./config');
console.log('Configuration valid');
console.log(JSON.stringify(config, null, 2));
} catch (err) {
console.error('Configuration error:', err.message);
process.exit(1);
}
"
Environment Variable Precedence Issues
# Check precedence in Docker Compose
docker-compose config # Shows final configuration
# Override environment file with specific variables
docker run --env-file .env -e NODE_ENV=override myapp
# Check what Docker sees
docker inspect <container> | grep -A 20 "Env"
8. Best Practices and Professional Tips (10 minutes)
Industry Standards
12-Factor App Configuration
// Follow 12-factor app principles
const config = {
// Store config in environment
database_url: process.env.DATABASE_URL,
// Strictly separate config from code
stripe_api_key: process.env.STRIPE_API_KEY,
// Treat backing services as attached resources
redis_url: process.env.REDIS_URL,
// No config files in version control
jwt_secret: process.env.JWT_SECRET
};
Configuration Validation
// config-validator.js
const Joi = require('joi');
const configSchema = Joi.object({
NODE_ENV: Joi.string().valid('development', 'staging', 'production').required(),
PORT: Joi.number().port().default(3000),
DATABASE_URL: Joi.string().uri().required(),
API_KEY: Joi.string().min(32).when('NODE_ENV', {
is: 'production',
then: Joi.required()
}),
ENABLE_FEATURE_X: Joi.boolean().default(false)
});
function validateConfig() {
const { error, value } = configSchema.validate(process.env, {
allowUnknown: true,
abortEarly: false
});
if (error) {
console.error('Configuration validation failed:');
error.details.forEach(detail => {
console.error(`- ${detail.path}: ${detail.message}`);
});
process.exit(1);
}
return value;
}
module.exports = { validateConfig };
Secrets Management
# Use init containers for secret fetching
docker run --name secret-fetcher \
-v secrets-volume:/secrets \
secret-fetcher:latest
# Main application uses mounted secrets
docker run --name app \
-v secrets-volume:/run/secrets:ro \
myapp
Configuration Documentation
# Document all configuration options in Dockerfile
FROM node:18-alpine
# Configuration documentation
LABEL config.NODE_ENV="Environment: development|staging|production"
LABEL config.PORT="Server port (default: 3000)"
LABEL config.DATABASE_URL="Database connection string"
LABEL config.ENABLE_CACHING="Enable caching (true|false)"
# Default values
ENV NODE_ENV=production
ENV PORT=3000
WORKDIR /app
COPY . .
CMD ["npm", "start"]
Production Configuration Patterns
# Immutable configuration with init containers
docker run --name config-init \
-v app-config:/config \
config-generator:latest
docker run --name app \
-v app-config:/app/config:ro \
--read-only \
myapp
# Configuration hot-reload (for specific use cases)
docker run --name app \
-v $(pwd)/config:/app/config:ro \
-e CONFIG_WATCH=true \
myapp
Security Best Practices
# Use least privilege for configuration access
docker run --user 1001:1001 \
-v $(pwd)/config:/app/config:ro \
myapp
# Audit configuration access
docker run \
--log-driver=syslog \
--log-opt syslog-address=tcp://log-server:514 \
--log-opt tag="app-config" \
myapp
Next Steps in Your Learning Journey
- Learn Docker Compose environment management
- Explore Kubernetes ConfigMaps and Secrets
- Study external configuration management (Consul, etcd)
- Practice with CI/CD configuration pipelines
9. Summary and Review (5 minutes)
Key Takeaways
- Environment variables are the primary configuration method - Use them for all configurable aspects of your application
- Never put secrets in Docker images - Use runtime injection for sensitive data
- Validate configuration at startup - Fail fast if configuration is invalid
Quick Review Questions
- What’s the difference between ENV in Dockerfile and -e in docker run?
- How should you handle database passwords in production containers?
- Why shouldn’t you use environment variables for large configuration structures?
Answers:
- ENV sets default values in the image; -e overrides them at runtime
- Use external secret management or mounted secret files, never environment variables
- Environment variables are limited in size and complexity; use configuration files for large structures
For Instructors
Teaching Tips
- Start with simple examples and gradually add complexity
- Show both insecure and secure configuration patterns
- Demonstrate configuration debugging techniques live
- Use real-world examples from popular applications
Assessment Ideas
- Configuration design exercises for different application types
- Security audit assignments for configuration practices
- Troubleshooting scenarios with broken configurations
- Environment promotion pipelines (dev → staging → prod)
Adaptation Notes
- For beginners: Focus on basic environment variable usage and security awareness
- For advanced students: Introduce external configuration management and advanced patterns
- Online delivery: Use screen sharing to show environment variable inspection
- Time management: Configuration debugging can be time-consuming - plan accordingly