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.