Configuration management across different environments—development, staging, production—requires a flexible approach that separates configuration from code. The .env file provides a standardized mechanism for defining configuration values that Docker Compose automatically loads and makes available throughout your setup.
Understanding .env Files
A .env file is a plain text file containing key-value pairs, one per line. Docker Compose reads this file automatically when present in the same directory as your configuration, making the defined values available for substitution.
Basic .env File Structure
Create a file named .env in your project directory:
DATABASE_HOST=postgres DATABASE_PORT=5432 API_KEY=abc123xyz DEBUG_MODE=true
Each line defines a variable. The format is simple: KEY=value with no spaces around the equals sign.
Automatic Loading
Docker Compose automatically loads .env files from:
- The directory containing your docker-compose.yml file
- Any parent directories (walking up the tree)
You don't need to explicitly tell Compose to load the file—it happens automatically. If Compose finds multiple .env files in the directory hierarchy, it uses the one closest to your configuration file.
Variable Substitution
Variables defined in .env files become available for substitution in your configuration.
Basic Substitution
Reference variables using ${VARIABLE_NAME} syntax:
services:
web:
image: nginx:${NGINX_VERSION}
ports:
- "${WEB_PORT}:80"
With this .env file:
NGINX_VERSION=1.21 WEB_PORT=8080
Compose substitutes the values when processing the configuration, resulting in:
services:
web:
image: nginx:1.21
ports:
- "8080:80"
Multiple Variables in One Value
Combine multiple variables in a single value:
services:
app:
image: myregistry.com/${APP_NAME}:${APP_VERSION}
With .env:
APP_NAME=myapp APP_VERSION=2.1.0
This resolves to image: myregistry.com/myapp:2.1.0.
Variables in Different Contexts
Use variables throughout your configuration:
services:
database:
image: postgres:${POSTGRES_VERSION}
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "${DB_PORT}:5432"
volumes:
- ${DATA_DIR}:/var/lib/postgresql/data
Variables work in image names, port mappings, environment values, volume paths, and most other configuration fields.
Default Values
Provide fallback values for undefined variables using ${VARIABLE:-default} syntax:
services:
web:
image: nginx:${NGINX_VERSION:-latest}
ports:
- "${WEB_PORT:-8080}:80"
If NGINX_VERSION is undefined in .env, Compose uses latest. If WEB_PORT is undefined, it uses 8080.
This pattern allows running without a .env file by providing sensible defaults while still enabling environment-specific overrides.
Empty vs Undefined Variables
The :- operator only provides defaults for undefined variables. If a variable is defined but empty, the empty value is used:
.env:
NGINX_VERSION=
Configuration:
image: nginx:${NGINX_VERSION:-latest}
Result: image: nginx: (empty, not latest)
To use defaults for both undefined and empty variables, use := to set the variable if undefined:
image: nginx:${NGINX_VERSION:=latest}
Alternative Default Syntax
Another syntax for defaults uses ${VARIABLE-default} (no colon):
image: nginx:${NGINX_VERSION-latest}
The single-hyphen version only provides defaults when variables are completely undefined (not set at all). The :- version (with colon) provides defaults for both undefined and empty variables.
In practice, :- is more commonly used because empty values typically indicate missing configuration.
Variable Naming Conventions
Following conventions improves readability and reduces errors.
Use Uppercase
Standard practice uses uppercase for variable names:
DATABASE_HOST=localhost API_PORT=3000 DEBUG_MODE=false
This clearly distinguishes environment variables from other text in your configuration.
Use Underscores for Word Separation
Separate words with underscores:
DATABASE_HOST=postgres API_BASE_URL=https://api.example.com MAX_CONNECTIONS=100
Avoid hyphens, spaces, or camelCase in variable names.
Prefix Related Variables
Group related variables with common prefixes:
DB_HOST=postgres DB_PORT=5432 DB_USER=admin DB_PASSWORD=secret DB_NAME=myapp REDIS_HOST=redis REDIS_PORT=6379 REDIS_PASSWORD=cachesecret
This organization makes relationships clear and simplifies variable management.
Comments in .env Files
Add comments using the # character:
# Database configuration DATABASE_HOST=postgres DATABASE_PORT=5432 # API settings API_KEY=abc123xyz # Production key DEBUG_MODE=false # Never enable in production
Comments can appear on their own lines or after variable definitions. Use comments to:
- Document variable purposes
- Provide example values
- Warn about sensitive settings
- Explain non-obvious values
Quotes in .env Files
Values don't require quotes in most cases:
DATABASE_HOST=localhost APP_NAME=myapp
Use quotes when values contain spaces or special characters:
APP_TITLE="My Application" DATABASE_URL="postgresql://user:pass@host/db"
Both single and double quotes work:
APP_TITLE='My Application' MESSAGE="Hello, World!"
Quotes become part of the value if used unnecessarily:
PORT="8080"
This sets PORT to the string "8080" (including quotes), which may cause issues. Use quotes only when needed.
Multi-Line Values
Multi-line values aren't directly supported in .env files. For complex multi-line content, use alternative approaches:
Option 1: Use literal \n in the value:
LONG_TEXT="First line\nSecond line\nThird line"
Option 2: Store complex content in separate files and reference file paths:
CONFIG_FILE=/path/to/config.json
Then read the file content in your application.
Special Characters
Special characters in values may require special handling:
Spaces
Enclose values with spaces in quotes:
APP_TITLE="My Application Name"
Quotes Inside Values
Escape quotes within quoted values:
MESSAGE="He said \"Hello\""
Or use single quotes to contain double quotes:
MESSAGE='He said "Hello"'
Dollar Signs
Dollar signs trigger variable expansion. Escape them to use literally:
PASSWORD=p\$\$word123
Or use single quotes to prevent expansion:
PASSWORD='p$$word123'
Multiple .env Files
While Docker Compose automatically loads .env by default, you can specify alternative files.
Specifying Alternative Files
Use the --env-file flag to load a different file:
docker compose --env-file .env.production up
This loads .env.production instead of .env. The file can have any name.
Loading Multiple Files
Load multiple environment files in sequence:
docker compose --env-file .env.common --env-file .env.production up
Variables in later files override those in earlier files. This pattern enables shared base configuration with environment-specific overrides.
Environment-Specific Files
Create separate files for each environment:
.env.development .env.staging .env.production
Load the appropriate file based on context:
# Development docker compose --env-file .env.development up # Production docker compose --env-file .env.production up
This approach clearly separates environment configurations and prevents accidental use of wrong settings.
Variable Precedence
When the same variable is defined in multiple places, Docker Compose follows a precedence order:
- Shell environment variables (highest priority)
- Values set with --env-file
- Values in the default .env file
- Default values specified in configuration (lowest priority)
Shell Environment Override
Shell variables override .env file values:
.env:
PORT=8080
Command:
PORT=9090 docker compose up
The container uses port 9090, not 8080. Shell variables take precedence.
This precedence enables temporary overrides without modifying files.
Explicit env-file Priority
Variables from --env-file override those in .env:
.env:
DEBUG=false
.env.development:
DEBUG=true
Command:
docker compose --env-file .env.development up
DEBUG is true because .env.development takes precedence over .env.
Security Considerations
.env files often contain sensitive information requiring careful handling.
Never Commit Secrets
Add .env to .gitignore to prevent committing secrets to version control:
.env .env.* !.env.example
This excludes all .env files except .env.example (a template with no real secrets).
Use .env.example Templates
Provide a template showing required variables without actual values:
.env.example:
# Database configuration DATABASE_HOST=localhost DATABASE_PORT=5432 DATABASE_USER=your_db_user DATABASE_PASSWORD=your_secure_password # API Keys API_KEY=your_api_key_here SECRET_KEY=your_secret_key_here
Commit .env.example to version control. Team members copy it to .env and fill in real values locally.
File Permissions
Restrict .env file access:
chmod 600 .env
This makes the file readable and writable only by the owner, preventing other users from viewing sensitive values.
Separate Secrets from Configuration
Not all variables are equally sensitive. Consider separating truly secret values (API keys, passwords) from general configuration (ports, feature flags):
.env:
# Non-sensitive configuration APP_PORT=8080 DEBUG_MODE=false
.env.secrets:
# Sensitive credentials DATABASE_PASSWORD=secret123 API_KEY=abc123xyz
Use different security measures for each file type.
Debugging .env Loading
When variables don't behave as expected, verify they're loading correctly.
Checking Variable Values
Use the config command to see resolved configuration:
docker compose config
This displays the final configuration after variable substitution. Check if variables resolved to expected values.
Verifying .env File Location
Ensure .env is in the correct directory—the same directory as your docker-compose.yml file or a parent directory.
List files to confirm:
ls -la
Verify .env exists and contains expected content:
cat .env
Testing Variable Substitution
Test individual variable substitution:
docker compose config | grep "image:"
This shows resolved image values, helping verify variable substitution worked correctly.
Common Issues
Variables not substituting: Check for typos in variable names. Variable names are case-sensitive.
Unexpected values: Remember precedence order. Shell environment may be overriding .env values.
Quotes appearing in values: Unnecessary quotes become part of the value. Remove quotes unless needed for spaces or special characters.
Empty values: Distinguish between undefined variables and variables defined with empty values. Use :- for defaults that apply to both cases.
Practical .env Patterns
Development Configuration
.env.development:
# Use local services DATABASE_HOST=localhost DATABASE_PORT=5432 REDIS_HOST=localhost REDIS_PORT=6379 # Enable debugging DEBUG=true LOG_LEVEL=debug # Use development API keys API_KEY=dev_key_123 # Development URLs APP_URL=http://localhost:8080 API_URL=http://localhost:3000
Production Configuration
.env.production:
# Use production services
DATABASE_HOST=prod-db.example.com
DATABASE_PORT=5432
REDIS_HOST=prod-redis.example.com
REDIS_PORT=6379
# Disable debugging
DEBUG=false
LOG_LEVEL=error
# Production API keys (use secrets management in real scenarios)
API_KEY=${PRODUCTION_API_KEY}
# Production URLs
APP_URL=https://app.example.com
API_URL=https://api.example.com
Shared Base Configuration
.env.common:
# Versions POSTGRES_VERSION=14 REDIS_VERSION=7 NODE_VERSION=18 # Common ports DATABASE_PORT=5432 REDIS_PORT=6379 # Feature flags ENABLE_ANALYTICS=true ENABLE_CACHING=true
Combine with environment-specific files:
docker compose --env-file .env.common --env-file .env.production up
Advanced Variable Usage
Computed Values
Use shell expansion for computed values:
TIMESTAMP=$(date +%Y%m%d)
BUILD_NUMBER=${CI_BUILD_NUMBER:-local}
VERSION=1.2.3-${BUILD_NUMBER}
Note: Shell expansion happens when the shell reads the file, not when Compose processes it. This works with export but not directly in .env files.
Conditional Values
While .env files don't support conditional logic, you can use shell conditionals when invoking Compose:
if [ "$ENVIRONMENT" = "production" ]; then docker compose --env-file .env.production up else docker compose --env-file .env.development up fi
Dynamic File Selection
Select .env files based on conditions:
ENV_FILE=".env.${ENVIRONMENT:-development}"
docker compose --env-file "$ENV_FILE" up
This loads .env.development by default or .env.production if ENVIRONMENT=production is set.
Integration with Configuration Management
Using .env with CI/CD
In CI/CD pipelines, generate .env files dynamically:
# CI/CD script
cat > .env << EOF
DATABASE_HOST=${CI_DATABASE_HOST}
DATABASE_PASSWORD=${CI_DATABASE_PASSWORD}
API_KEY=${CI_API_KEY}
BUILD_NUMBER=${CI_BUILD_NUMBER}
EOF
docker compose up -d
This creates .env from CI/CD environment variables, keeping secrets in the CI/CD system rather than in repository files.
Secrets Management Integration
For production environments, integrate with secrets management systems:
# Fetch secrets and create .env vault kv get -format=json secret/app | jq -r 'to_entries[] | "\(.key)=\(.value)"' > .env docker compose up -d
This retrieves secrets from HashiCorp Vault (or similar systems) and generates .env dynamically, avoiding secrets in files at rest.
Validation and Documentation
Validating Required Variables
Create a script to validate required variables exist:
#!/bin/bash
# validate-env.sh
REQUIRED_VARS=("DATABASE_HOST" "DATABASE_PASSWORD" "API_KEY")
for var in "${REQUIRED_VARS[@]}"; do
if [ -z "${!var}" ]; then
echo "Error: $var is not set"
exit 1
fi
done
echo "All required variables are set"
Run before starting services:
source .env ./validate-env.sh docker compose up
Self-Documenting .env Files
Use extensive comments to document variables:
# Database Configuration # The hostname or IP address of the PostgreSQL database server # In production, this should point to a managed database instance # Default: localhost DATABASE_HOST=localhost # The port PostgreSQL is listening on # Standard PostgreSQL port is 5432 # Only change if using a non-standard configuration # Default: 5432 DATABASE_PORT=5432 # Database authentication credentials # ⚠️ SECURITY: Never commit these values to version control # Use strong, unique passwords for production environments DATABASE_USER=myapp_user DATABASE_PASSWORD=change_this_in_production
Clear documentation helps team members understand configuration options and security implications.
Common Pitfalls
Trailing Whitespace
Invisible whitespace causes subtle issues:
DATABASE_HOST=localhost
The trailing space becomes part of the value. Most editors can highlight or automatically remove trailing whitespace.
Inconsistent Line Endings
Windows-style line endings (CRLF) can cause problems. Use Unix-style (LF) line endings:
dos2unix .env
Or configure your editor to use LF line endings for .env files.
Missing Variables
Referencing undefined variables without defaults causes empty values:
image: myapp:${VERSION}
If VERSION isn't defined, this becomes image: myapp: which fails. Always provide defaults or validate required variables.
Overusing .env Files
Not everything belongs in .env files. Keep them focused on environment-specific configuration. Don't use them for:
- Application logic
- Complex structured data
- Non-configuration constants
- Values that never change across environments
Ignoring Variable Precedence
Remember that shell environment variables override .env values. Mysterious behavior often results from shell variables you've forgotten about.
Check your shell environment:
env | grep DATABASE
Maintenance Strategies
Regular Audits
Periodically review .env files:
- Remove unused variables
- Update documentation
- Verify security practices
- Check for sensitive data leakage
- Ensure consistency across environments
Version .env.example
Keep .env.example synchronized with actual requirements:
# After adding new variables to .env # Update .env.example with placeholder values cp .env .env.example # Replace real values with placeholders sed -i 's/=.*/=your_value_here/' .env.example
Automated Consistency Checks
Create scripts that verify .env files contain required variables:
#!/bin/bash # check-env-consistency.sh TEMPLATE=.env.example ACTUAL=.env # Extract variable names from template TEMPLATE_VARS=$(grep -v '^#' "$TEMPLATE" | cut -d'=' -f1 | sort) # Extract variable names from actual file ACTUAL_VARS=$(grep -v '^#' "$ACTUAL" | cut -d'=' -f1 | sort) # Find differences MISSING=$(comm -23 <(echo "$TEMPLATE_VARS") <(echo "$ACTUAL_VARS")) if [ -n "$MISSING" ]; then echo "Missing variables in $ACTUAL:" echo "$MISSING" exit 1 fi echo "All template variables present in $ACTUAL"
Run this in development to catch missing configuration.
Best Practices Summary
Do's
✅ Use .env for environment-specific configuration
✅ Commit .env.example with placeholder values
✅ Add .env to .gitignore
✅ Use uppercase variable names with underscores
✅ Provide defaults for optional variables
✅ Document variables with comments
✅ Validate required variables before starting services
✅ Use separate files for different environments
✅ Set restrictive file permissions (600)
✅ Regularly audit and update .env files
Don'ts
❌ Don't commit real .env files to version control
❌ Don't store secrets in .env files in production (use secrets management)
❌ Don't use .env for complex structured data
❌ Don't include unnecessary quotes
❌ Don't forget about variable precedence
❌ Don't use spaces in variable names
❌ Don't reference undefined variables without defaults
❌ Don't share .env files via insecure channels
❌ Don't reuse the same .env across environments
❌ Don't ignore trailing whitespace and line endings
Conclusion
The .env file provides a simple, standardized approach to configuration management in Docker Compose. By centralizing environment-specific values in one place, separating configuration from code, and enabling easy environment switching, .env files streamline development and deployment workflows. Following security best practices, maintaining clear documentation, and understanding variable substitution mechanics ensures effective use of this powerful configuration mechanism.