← Back to DevBytes

Docker Compose Environment Variables: Best Practices and Common Pitfalls

What Are Docker Compose Environment Variables?

Environment variables in Docker Compose allow you to inject configuration into your containers at runtime and control the behavior of the Compose file itself. They provide a way to separate configuration from code, making your setups portable across different environments (development, staging, production) without modifying the Compose file.

In practice, you work with three distinct layers of environment variables:

Why They Matter

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Hardcoding values like database passwords, API keys, or host ports directly in docker-compose.yml is a security risk and a maintenance nightmare. Environment variables solve this by:

How to Use Environment Variables in Docker Compose

1. Passing Variables to Containers with environment

The most direct way is to list variables under the environment key. You can use either a dictionary (mapping) or an array format.

# docker-compose.yml
services:
  web:
    image: nginx:latest
    environment:
      APP_ENV: production
      APP_DEBUG: "false"
      APP_PORT: "8080"
    ports:
      - "${APP_PORT}:80"

The array syntax is useful when you need to pass variables without values (just names) or when values contain special characters:

services:
  web:
    environment:
      - APP_ENV=production
      - APP_DEBUG=false
      - EMPTY_VAR

2. Using an .env File for Service Variables

The env_file directive points to a file containing key-value pairs. This file is loaded into the container's environment.

# .env.web
APP_ENV=development
DB_HOST=mysql
DB_PASSWORD=secret
# docker-compose.yml
services:
  web:
    image: myapp:latest
    env_file:
      - .env.web

You can specify multiple env_file paths; they are merged in order. If the same variable appears in multiple files, the last one wins.

3. Compose File Substitution with .env

Docker Compose automatically looks for a file named .env in the same directory as the Compose file (or the project root). Variables defined there can be used directly in the Compose YAML using ${VARIABLE} or $VARIABLE.

# .env
IMAGE_TAG=1.2.3
HOST_PORT=8080
# docker-compose.yml
services:
  web:
    image: myapp:${IMAGE_TAG}
    ports:
      - "${HOST_PORT}:80"

You can also reference variables from the host shell environment. Compose will use shell values if a corresponding variable is not found in the .env file.

4. Precedence and Variable Sources

When Compose resolves a ${VAR} placeholder, it checks sources in this order:

This layered approach lets you override defaults safely.

Best Practices

1. Use a Project-level .env for Compose Substitutions Only

Keep your root .env focused on values that configure the Compose file itself (image tags, port mappings, resource limits). Service-specific runtime variables should go into dedicated files referenced by env_file.

# .env — compose‑level settings
TAG=latest
WEB_PORT=80
DEBUG=0

2. Never Commit Secrets to .env or env_file

Add .env and any *.env files to .gitignore. Distribute a template file instead:

# .env.example
DB_USER=app_user
DB_PASSWORD=replace_me

For production secrets, use Docker secrets (in Swarm) or a dedicated secrets manager, not plain environment variables.

3. Provide Defaults with ${VAR:-default} Syntax

Defaults keep your Compose file runnable even when no .env exists. This is essential for open‑source projects and quick demos.

services:
  web:
    image: myapp:${TAG:-latest}
    ports:
      - "${PORT:-3000}:80"

4. Use Explicit Variable Mapping in Production

For containers that need a curated set of variables, explicitly map them under environment rather than blindly passing an entire env_file. This avoids leaking unrelated host variables.

services:
  web:
    environment:
      DB_HOST: "${DB_HOST}"
      DB_PORT: "${DB_PORT}"
      # Only the needed vars

5. Leverage Multiple env_file Files Per Environment

Use separate files for different environments and load them conditionally via --env-file or by overriding the Compose file path.

# docker-compose.dev.yml
services:
  web:
    env_file:
      - .env.shared
      - .env.dev

Then run docker-compose -f docker-compose.yml -f docker-compose.dev.yml up.

6. Document Every Variable

Maintain a clear table in your README or a dedicated .env.example describing each variable, its purpose, and default value. This reduces onboarding friction.

7. Avoid Inline Hardcoded Values in Compose Files

Even for non‑secret settings like timeouts, use variables. It makes tuning across environments trivial.

Common Pitfalls and How to Avoid Them

Pitfall 1: Variable Expansion Inside Quotes

When you use quotes around a ${VAR} placeholder, Compose still expands it. However, if you need a literal $ character, you must escape it with $$.

environment:
  # Correctly expands the variable
  GREETING: "Hello, ${NAME}"
  # Escapes to a literal dollar sign
  COST_FORMAT: "Price: $$99.99"

If you want to pass an environment variable without expansion, use $$.

Pitfall 2: Missing Defaults Cause Startup Failures

A missing variable with no default will cause Compose to abort with an error. Always provide a fallback or document required variables clearly.

# This will fail if TAG is unset:
image: myapp:${TAG}
# Safer:
image: myapp:${TAG:-latest}

Pitfall 3: Confusing Shell Environment with .env File

The .env file is only for Compose‑level substitution. It does not automatically become available inside containers. For container runtime variables, use environment or env_file.

Pitfall 4: Overriding Variables at Runtime Unexpectedly

Running docker-compose up -e MY_VAR=override can override variables in the Compose file substitution, but it does not inject them into containers unless they are explicitly mapped. Be explicit about which variables you want to allow overriding.

Pitfall 5: Variable Precedence Confusion with env_file

When you define the same variable in environment and in an env_file, the inline environment value wins. This is intentional but can surprise you if you’re not aware.

services:
  web:
    environment:
      FOO: "inline_value"
    env_file:
      - .env.web   # contains FOO=file_value
  # Container sees FOO=inline_value

Pitfall 6: Using .env for Sensitive Values Without Gitignore

Accidentally committing an .env file with production credentials is one of the most common security breaches. Always add .env to .gitignore and use a secrets manager for real secrets.

Pitfall 7: Assuming .env Location Is Always the Compose File Directory

The default .env is looked up in the project directory (where the primary Compose file resides). If you run docker-compose from a different directory, you might get different values. Use --project-directory or the --env-file flag to be explicit.

Pitfall 8: Not Escaping Special Characters in env_file

An env_file line must be a simple KEY=value. If a value contains spaces or special characters, quote the whole value in the file.

# .env.web
PASSWORD="p@ss w0rd!"

Docker Compose processes this correctly, but be aware that the quotes are stripped when passed to the container.

Pitfall 9: Using List Format Under environment and Expecting Compose Substitution

The array syntax does not perform ${VAR} substitution on values. Only the dictionary style supports expansion. Use the dictionary style if you need Compose‑level variable interpolation.

# This does NOT expand HOST_PORT
environment:
  - PORT=${HOST_PORT}
# Use dictionary format instead:
environment:
  PORT: "${HOST_PORT}"

Conclusion

Docker Compose environment variables are a powerful mechanism for building flexible, secure, and environment‑agnostic container setups. By understanding the distinct roles of Compose‑level substitution, service‑level injection, and the host environment, you can avoid the common traps that lead to broken deployments or leaked secrets. Apply the practices covered here—using defaults, separating concerns, never committing real secrets, and documenting your variables—and your Compose projects will remain clean, maintainable, and ready to run anywhere.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles