Back to BlogOdoo · containerization · docker

Environment Variables in Compose

2025-12-22

Environment variables provide a powerful mechanism for configuring containerized applications without modifying code or configuration files. They allow you to inject runtime configuration, credentials, feature flags, and other dynamic values into your application containers, making your deployments flexible and adaptable across different contexts.

Understanding Environment Variables

Environment variables are key-value pairs that exist in the execution environment of a process. When a container starts, it inherits environment variables defined in its configuration, and applications running inside the container can read these variables to adjust their behavior. This mechanism separates configuration from code, following the twelve-factor app methodology.

The power of environment variables lies in their flexibility. The same container image can behave differently in development, staging, and production environments simply by receiving different environment variable values. This approach eliminates the need to rebuild images for different deployment contexts.

Basic Environment Variable Syntax

Environment variables in configuration files can be defined using two primary formats: the list format and the mapping format. Each format has specific use cases and advantages.

services:
  web:
    image: webapp:latest
    environment:
      - NODE_ENV=production
      - PORT=3000
      - DEBUG=false
    # List format: each variable on its own line
    # Uses KEY=VALUE syntax with dashes

  api:
    image: api-server:latest
    environment:
      DATABASE_HOST: db.example.com
      DATABASE_PORT: 5432
      API_KEY: abc123xyz
    # Mapping format: uses YAML key-value pairs
    # More readable for complex configurations

Both formats achieve the same result—they set environment variables inside the container. The list format is more compact, while the mapping format offers better readability for configurations with many variables.

Environment Variable Values and Types

Environment variable values are always treated as strings, regardless of how they appear in the configuration. This string-only nature affects how applications must parse and interpret these values.

services:
  app:
    image: myapp:latest
    environment:
      # All values become strings in the container
      PORT: 8080                    # String: "8080"
      ENABLE_FEATURE: true          # String: "true"
      MAX_CONNECTIONS: 100          # String: "100"
      RATIO: 0.75                   # String: "0.75"
      EMPTY_VALUE: ""               # Empty string
      
      # Explicit string values
      APP_NAME: "My Application"
      MESSAGE: 'Hello, World!'
      
      # Values with special characters need quoting
      PASSWORD: "p@ssw0rd!"
      PATH_VAR: "/usr/local/bin:/usr/bin"

Applications reading these variables must convert string values to appropriate types. A value of "true" must be parsed as a boolean, "8080" as an integer, and so forth.

Quoting and Special Characters

Values containing special characters require careful quoting to ensure they're interpreted correctly. YAML's quoting rules apply to environment variable values.

services:
  app:
    image: myapp:latest
    environment:
      # Simple values don't need quotes
      SIMPLE: value
      NUMBER: 123
      
      # Values with spaces need quotes
      MESSAGE: "Hello World"
      
      # Values with colons need quotes
      URL: "http://example.com:8080"
      TIME: "12:30:45"
      
      # Values with special YAML characters
      REGEX: "^[a-z]+$"
      MATH: "5+5=10"
      
      # Values with quotes inside
      QUOTED: 'He said "hello"'
      APOSTROPHE: "It's working"
      
      # Values with dollar signs (literal)
      BASH_VAR: "$$PATH"           # Results in: $PATH
      ESCAPED: "$${VARIABLE}"      # Results in: ${VARIABLE}

Proper quoting ensures that special characters are treated as literal values rather than being interpreted by YAML or variable substitution mechanisms.

Multi-line Environment Variable Values

Environment variables can contain multi-line values using YAML's multi-line string syntax. This capability is useful for configuration files, certificates, or long text values.

services:
  app:
    image: myapp:latest
    environment:
      # Literal multi-line (preserves newlines)
      CONFIG: |
        server {
          listen 80;
          server_name example.com;
        }
      
      # Folded multi-line (joins lines with spaces)
      DESCRIPTION: >
        This is a long description
        that spans multiple lines
        but will be joined into
        a single line with spaces.
      
      # Certificate or key
      CERTIFICATE: |
        -----BEGIN CERTIFICATE-----
        MIIDXTCCAkWgAwIBAgIJAKL0UG+mRUKNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
        BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
        -----END CERTIFICATE-----

Multi-line values maintain formatting and structure, allowing complex configuration data to be passed through environment variables.

Variable Substitution in Values

Environment variable values can reference other environment variables using substitution syntax. This feature enables building complex values from simpler components.

services:
  app:
    image: myapp:latest
    environment:
      # Basic variable reference
      HOST: example.com
      PORT: 8080
      URL: "http://${HOST}:${PORT}"
      # URL becomes: http://example.com:8080
      
      # Nested substitution
      PROTOCOL: https
      DOMAIN: api.example.com
      ENDPOINT: "${PROTOCOL}://${DOMAIN}/v1"
      # ENDPOINT becomes: https://api.example.com/v1
      
      # Building paths
      BASE_DIR: /opt/app
      LOG_DIR: "${BASE_DIR}/logs"
      DATA_DIR: "${BASE_DIR}/data"

Substitution allows defining base values once and deriving related values, reducing duplication and making configurations more maintainable.

Default Values in Substitution

Variable substitution supports default values that are used when the referenced variable is not set. This pattern provides fallback behavior for optional configuration.

services:
  app:
    image: myapp:latest
    environment:
      # Default value syntax: ${VAR:-default}
      PORT: "${APP_PORT:-3000}"
      # Uses APP_PORT if set, otherwise 3000
      
      HOST: "${APP_HOST:-localhost}"
      # Uses APP_HOST if set, otherwise localhost
      
      LOG_LEVEL: "${LOG_LEVEL:-info}"
      # Uses LOG_LEVEL if set, otherwise info
      
      # Empty string as default
      OPTIONAL: "${OPTIONAL_VAR:-}"
      
      # Complex default values
      DATABASE_URL: "${DB_URL:-postgresql://localhost:5432/mydb}"

Default values make configurations more robust by ensuring variables always have meaningful values even when external configuration is missing.

Required Variables

Variable substitution can enforce that certain variables must be set, failing if they're missing. This validation ensures critical configuration is provided.

services:
  app:
    image: myapp:latest
    environment:
      # Required variable syntax: ${VAR:?error message}
      API_KEY: "${API_KEY:?API_KEY must be set}"
      # Fails with error if API_KEY is not set
      
      DATABASE_PASSWORD: "${DB_PASSWORD:?Database password required}"
      # Fails with custom error message
      
      # Required with no custom message
      SECRET_KEY: "${SECRET_KEY:?}"
      # Fails with generic error if not set
      
      # Can combine with other variables
      ADMIN_EMAIL: "${ADMIN_EMAIL:?Admin email must be configured}"

Required variable syntax provides validation at configuration parse time, catching missing configuration before containers start.

Alternative Value Syntax

Substitution supports alternative values that are used when variables are set, regardless of their value. This less common pattern has specific use cases.

services:
  app:
    image: myapp:latest
    environment:
      # Alternative value syntax: ${VAR:+alternative}
      DEBUG_MODE: "${ENABLE_DEBUG:+true}"
      # If ENABLE_DEBUG is set (to any value), DEBUG_MODE becomes "true"
      # If ENABLE_DEBUG is not set, DEBUG_MODE is empty
      
      FEATURE_FLAG: "${FEATURE_ENABLED:+enabled}"
      # Sets "enabled" when FEATURE_ENABLED exists
      
      # Useful for conditional configuration
      VERBOSE: "${VERBOSE:+--verbose}"
      # Adds --verbose flag only if VERBOSE is set

Alternative value syntax allows presence-based rather than value-based configuration decisions.

Combining Substitution Patterns

Multiple substitution patterns can be combined to create sophisticated configuration logic.

services:
  app:
    image: myapp:latest
    environment:
      # Nested substitution with defaults
      PRIMARY_HOST: "${HOST:-localhost}"
      BACKUP_HOST: "${BACKUP:-${PRIMARY_HOST}}"
      # BACKUP_HOST uses BACKUP if set, otherwise falls back to PRIMARY_HOST
      
      # Building complex URLs
      PROTOCOL: "${PROTOCOL:-https}"
      DOMAIN: "${DOMAIN:?Domain must be set}"
      PORT: "${PORT:-443}"
      API_ENDPOINT: "${PROTOCOL}://${DOMAIN}:${PORT}/api"
      
      # Conditional flags
      DEBUG: "${DEBUG:-false}"
      VERBOSE_FLAG: "${DEBUG:+--verbose}"

Combining patterns enables expressing complex configuration relationships declaratively.

Boolean Environment Variables

Boolean values in environment variables require careful handling since all values are strings. Establishing conventions for boolean representation ensures consistency.

services:
  app:
    image: myapp:latest
    environment:
      # Common boolean representations
      FEATURE_ENABLED: "true"       # String "true"
      DEBUG_MODE: "false"            # String "false"
      
      # Numeric booleans
      USE_CACHE: "1"                 # 1 for true
      STRICT_MODE: "0"               # 0 for false
      
      # Yes/No
      ENABLE_LOGGING: "yes"
      COMPRESS_OUTPUT: "no"
      
      # Presence-based booleans
      DEVELOPMENT: "1"               # Any value means true
      # Absence means false
      
      # Applications must parse these strings appropriately
      # Most languages provide utilities for this

Application code must parse these string representations into actual boolean values based on your chosen convention.

Numeric Environment Variables

Numeric values also require string representation and subsequent parsing by applications.

services:
  app:
    image: myapp:latest
    environment:
      # Integer values
      PORT: "8080"
      MAX_CONNECTIONS: "100"
      TIMEOUT_SECONDS: "30"
      
      # Floating point values
      RATIO: "0.75"
      THRESHOLD: "99.9"
      
      # Large numbers
      MAX_FILE_SIZE: "1048576"       # 1MB in bytes
      
      # Scientific notation (as string)
      CONSTANT: "6.022e23"
      
      # Negative numbers
      OFFSET: "-5"
      TEMPERATURE: "-40"

Applications reading numeric environment variables must parse them from strings, handling potential parsing errors appropriately.

Array and List Values

Environment variables don't natively support arrays or lists, but you can represent them as delimited strings that applications parse.

services:
  app:
    image: myapp:latest
    environment:
      # Comma-separated values
      ALLOWED_HOSTS: "localhost,example.com,*.example.org"
      
      # Colon-separated (PATH-style)
      SEARCH_PATH: "/usr/local/bin:/usr/bin:/bin"
      
      # Space-separated
      FEATURES: "auth cache logging monitoring"
      
      # Semicolon-separated
      MODULES: "core;database;api;web"
      
      # Applications must split these strings
      # Choice of delimiter depends on data characteristics

Choose delimiters that don't appear in the actual values, or implement escaping mechanisms in your application code.

JSON in Environment Variables

Complex structured data can be passed as JSON strings in environment variables.

services:
  app:
    image: myapp:latest
    environment:
      # Simple JSON object
      CONFIG: '{"timeout": 30, "retry": 3}'
      
      # JSON array
      ENDPOINTS: '["api.example.com", "backup.example.com"]'
      
      # Nested JSON
      DATABASE: |
        {
          "host": "db.example.com",
          "port": 5432,
          "credentials": {
            "username": "admin",
            "password": "secret"
          }
        }
      
      # JSON with escaping
      SETTINGS: "{\"feature\": true, \"level\": \"high\"}"

Applications must parse these JSON strings into structured data. This approach works well for complex configuration that doesn't fit simple key-value pairs.

Environment Variable Precedence

When environment variables can come from multiple sources, understanding precedence rules determines which value takes effect.

services:
  app:
    image: myapp:latest
    environment:
      # Variables defined here have specific precedence
      DATABASE_HOST: "localhost"
      PORT: "3000"
      
      # Variables can be overridden by:
      # 1. Command-line arguments (highest precedence)
      # 2. Shell environment variables
      # 3. Variables defined in this configuration
      # 4. Image defaults (lowest precedence)

Understanding precedence helps predict which value will be active when multiple sources define the same variable.

Overriding Image Defaults

Container images often define default environment variables. Configuration-level variables override these defaults.

services:
  app:
    image: myapp:latest
    # Image might define: NODE_ENV=development
    environment:
      NODE_ENV: "production"
      # Overrides image default
      
      # Image might define: PORT=8080
      PORT: "3000"
      # Overrides image default
      
      # Variables not overridden keep image defaults

This override mechanism allows customizing image behavior without rebuilding images.

Empty and Null Values

Empty strings and null values behave differently in environment variable handling.

services:
  app:
    image: myapp:latest
    environment:
      # Empty string value
      EMPTY: ""
      # Variable exists with empty string value
      
      # Null value (YAML)
      NULL_VAR: null
      # Variable may not be set or may be empty
      
      # No value specified
      NO_VALUE:
      # Behavior depends on implementation
      
      # Explicitly unset
      UNSET_VAR:
      # Variable typically not set in container

The distinction between empty and unset variables matters when applications check for variable existence versus checking values.

Variable Expansion Timing

Understanding when variable substitution occurs helps predict behavior and troubleshoot issues.

services:
  app:
    image: myapp:latest
    environment:
      # Substitution happens at configuration parse time
      IMMEDIATE: "${HOST:-localhost}"
      # Value resolved when configuration is read
      
      # Variables referencing shell environment
      FROM_SHELL: "${USER}"
      # Resolved from shell environment at parse time
      
      # Build-time vs runtime distinction
      BUILD_VAR: "${BUILD_ENV:-production}"
      # Value fixed at configuration time

Variable expansion occurs during configuration processing, not during container runtime. Values are resolved once and remain constant.

Escaping Variable Substitution

Sometimes you need literal dollar signs or curly braces in values without triggering substitution.

services:
  app:
    image: myapp:latest
    environment:
      # Double dollar sign for literal $
      BASH_SCRIPT: "echo $$HOME"
      # Results in: echo $HOME
      
      # Escaped variable syntax
      LITERAL: "$${NOT_SUBSTITUTED}"
      # Results in: ${NOT_SUBSTITUTED}
      
      # Single quotes prevent substitution in YAML
      LITERAL_QUOTE: '${STAYS_LITERAL}'
      # Results in: ${STAYS_LITERAL}
      
      # Mixed literal and substitution
      MIXED: "Actual: ${REAL_VAR}, Literal: $$FAKE"

Escaping mechanisms allow including dollar signs and variable-like syntax as literal text.

Environment Variable Naming Conventions

Following naming conventions makes environment variables easier to understand and manage.

services:
  app:
    image: myapp:latest
    environment:
      # UPPER_CASE with underscores (most common)
      DATABASE_HOST: "localhost"
      API_KEY: "secret"
      MAX_CONNECTIONS: "100"
      
      # Prefixed for namespacing
      MYAPP_DATABASE_HOST: "db.example.com"
      MYAPP_CACHE_TTL: "3600"
      
      # Hierarchical naming
      DB_PRIMARY_HOST: "db1.example.com"
      DB_REPLICA_HOST: "db2.example.com"
      
      # Feature flags
      FEATURE_NEW_UI: "true"
      FEATURE_BETA_API: "false"

Consistent naming conventions improve configuration readability and reduce errors.

Grouping Related Variables

Organizing related variables together improves configuration maintainability.

services:
  app:
    image: myapp:latest
    environment:
      # Database configuration group
      DB_HOST: "database.example.com"
      DB_PORT: "5432"
      DB_NAME: "myapp"
      DB_USER: "appuser"
      DB_PASSWORD: "secret"
      
      # Redis configuration group
      REDIS_HOST: "cache.example.com"
      REDIS_PORT: "6379"
      REDIS_DB: "0"
      
      # Application configuration group
      APP_ENV: "production"
      APP_DEBUG: "false"
      APP_LOG_LEVEL: "warning"

Grouping related variables with consistent prefixes makes configurations self-documenting.

Sensitive Information Handling

Environment variables often contain sensitive information that requires careful handling.

services:
  app:
    image: myapp:latest
    environment:
      # Sensitive variables should be clearly marked
      DATABASE_PASSWORD: "${DB_PASSWORD:?Password required}"
      API_SECRET: "${API_SECRET:?Secret required}"
      ENCRYPTION_KEY: "${ENCRYPTION_KEY:?Key required}"
      
      # Use substitution from external sources
      AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
      AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}"
      
      # Never hardcode secrets
      # BAD: API_KEY: "hardcoded-secret-123"
      # GOOD: API_KEY: "${API_KEY:?Must be provided}"

Sensitive values should come from external sources rather than being hardcoded in configuration files.

Variable Documentation

Documenting environment variables helps teams understand configuration requirements.

services:
  app:
    image: myapp:latest
    environment:
      # Database connection string
      # Format: postgresql://user:pass@host:port/dbname
      DATABASE_URL: "${DATABASE_URL:?Database URL required}"
      
      # API timeout in seconds (default: 30)
      API_TIMEOUT: "${API_TIMEOUT:-30}"
      
      # Log level: debug, info, warning, error
      LOG_LEVEL: "${LOG_LEVEL:-info}"
      
      # Feature flag: enables new authentication system
      # Set to "true" to enable
      NEW_AUTH: "${NEW_AUTH:-false}"
      
      # Maximum concurrent connections (1-1000)
      MAX_CONNECTIONS: "${MAX_CONNECTIONS:-100}"

Inline comments document each variable's purpose, format, and valid values.

Environment Variable Validation

While configuration syntax validates structure, applications must validate environment variable values.

services:
  app:
    image: myapp:latest
    environment:
      # Configuration provides values
      PORT: "${PORT:-3000}"
      TIMEOUT: "${TIMEOUT:-30}"
      
      # Application must validate:
      # - PORT is a valid port number (1-65535)
      # - TIMEOUT is a positive integer
      # - Required variables are not empty
      # - Enumerated values are valid choices
      # - URLs are properly formatted
      # - File paths exist or are writable

Application code should validate environment variables at startup, failing fast with clear error messages if configuration is invalid.

Case Sensitivity

Environment variable names are case-sensitive in most systems, requiring consistent naming.

services:
  app:
    image: myapp:latest
    environment:
      # These are different variables
      port: "3000"
      PORT: "8080"
      Port: "9000"
      
      # Convention: use UPPER_CASE consistently
      DATABASE_HOST: "localhost"
      DATABASE_PORT: "5432"
      
      # Avoid mixing cases
      # GOOD: API_KEY and API_SECRET
      # BAD: API_KEY and api_secret

Stick to uppercase naming to avoid case-related confusion and errors.

Dynamic Configuration Patterns

Environment variables enable dynamic configuration patterns that adapt to runtime context.

services:
  app:
    image: myapp:latest
    environment:
      # Environment detection
      APP_ENV: "${APP_ENV:-development}"
      
      # Environment-specific defaults
      LOG_LEVEL: "${LOG_LEVEL:-${APP_ENV:+production}}"
      
      # Feature flags based on environment
      ENABLE_DEBUG: "${ENABLE_DEBUG:-${APP_ENV:+false}}"
      
      # Conditional configuration
      DATABASE_POOL_SIZE: "${DB_POOL_SIZE:-${APP_ENV:+20}}"
      
      # Dynamic endpoint construction
      API_URL: "${API_PROTOCOL:-https}://${API_HOST:?Host required}:${API_PORT:-443}"

Dynamic patterns reduce configuration duplication across environments while maintaining flexibility.

Environment Variable Length Limits

While uncommon, very long environment variable values may encounter system limits.

services:
  app:
    image: myapp:latest
    environment:
      # Most systems support values up to several KB
      # Very long values work but may have limits
      
      LONG_CONFIG: |
        This is a very long configuration value
        that spans many lines and could potentially
        reach system limits if it becomes too large.
        Typical limits are 32KB-128KB per variable.
      
      # For extremely large configuration:
      # - Consider using files instead
      # - Split into multiple variables
      # - Use external configuration sources

For very large configuration data, consider alternatives to environment variables like configuration files or external configuration services.

Best Practices Summary

Effective environment variable usage follows established patterns that improve reliability and maintainability.

services:
  app:
    image: myapp:latest
    environment:
      # Use descriptive names
      DATABASE_CONNECTION_TIMEOUT: "30"
      
      # Provide defaults when sensible
      LOG_LEVEL: "${LOG_LEVEL:-info}"
      
      # Require critical variables
      API_KEY: "${API_KEY:?Required}"
      
      # Document complex variables
      # Format: scheme://host:port/path
      SERVICE_URL: "${SERVICE_URL:?Service URL required}"
      
      # Group related variables with prefixes
      AWS_REGION: "us-east-1"
      AWS_BUCKET: "my-bucket"
      
      # Use consistent naming conventions
      FEATURE_NEW_UI: "true"
      FEATURE_BETA_API: "false"
      
      # Never hardcode secrets
      SECRET_KEY: "${SECRET_KEY:?Must be provided securely}"

Following these practices creates maintainable, secure, and reliable configuration.

We use cookies to improve your experience and analyse site traffic. See our Privacy Policy.