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:

  1. Environment Variable Fundamentals - How Docker handles environment variables
  2. Configuration Methods - Different ways to pass configuration to containers
  3. Security Best Practices - Protecting sensitive configuration data
  4. Advanced Patterns - Configuration files, templates, and runtime configuration
  5. 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:

  1. Dockerfile ENV instructions
  2. Docker run -e flags
  3. Docker run –env-file
  4. Host environment (with –env flag)
  5. 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

  1. Misconception: “Environment variables are always the best way to pass configuration” Reality: For complex configurations, files or configuration services might be better

  2. Misconception: “Environment variables in Dockerfile are secure” Reality: Dockerfile ENV values are visible in image layers and should not contain secrets

  3. 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:

  1. Create three environment files: dev.env, staging.env, prod.env
  2. 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
  3. Implement configuration validation
  4. 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:

  1. Never stores secrets in Docker images
  2. Uses different methods for different types of secrets
  3. Implements secret rotation capabilities
  4. 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:

  1. Use configuration templates with variable substitution
  2. Support default values and conditional configuration
  3. Validate configuration after template processing
  4. 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

  1. 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
  1. 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
  };
}
  1. 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);
  1. 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

  1. Environment variables are the primary configuration method - Use them for all configurable aspects of your application
  2. Never put secrets in Docker images - Use runtime injection for sensitive data
  3. Validate configuration at startup - Fail fast if configuration is invalid

Quick Review Questions

  1. What’s the difference between ENV in Dockerfile and -e in docker run?
  2. How should you handle database passwords in production containers?
  3. Why shouldn’t you use environment variables for large configuration structures?

Answers:

  1. ENV sets default values in the image; -e overrides them at runtime
  2. Use external secret management or mounted secret files, never environment variables
  3. 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