Back to BlogDocker Container · docker · docker-compose-env

Docker Compose .env Files: Complete Guide to Environment Configuration

2025-12-22

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:

  1. The directory containing your docker-compose.yml file
  2. 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:

  1. Shell environment variables (highest priority)
  2. Values set with --env-file
  3. Values in the default .env file
  4. 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:

  1. Remove unused variables
  2. Update documentation
  3. Verify security practices
  4. Check for sensitive data leakage
  5. 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.

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