Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Pass environment variables in Compose

Learn how to pass environment variables in Docker Compose for flexible configuration. This lesson covers .env files, inline variables, and host environment access with practical examples.

Focus: pass environment variables in Compose

Sponsored

Every real-world application needs configuration that changes between environments – database URLs, API keys, or log levels. Hardcoding these into your Docker Compose file is a recipe for disaster: you'd either commit secrets to version control or manually edit files before every deployment. The solution is to pass environment variables in Docker Compose, keeping your services portable and your secrets safe.

This lesson shows you the three main ways to inject environment variables into your containers: the environment key, the env_file directive, and direct access to the host's shell variables. By the end, you'll be able to configure any Compose service without touching the image itself.

The problem this lesson solves

Imagine you have a Python web app that connects to a PostgreSQL database. The database host, user, and password are different on your laptop (localhost) vs. staging (staging.db.com) vs. production (prod-db-01.internal). If you bake those values into the Docker image, you'd need a separate image per environment – defeating the purpose of containers.

Even if you use a generic image, editing docker-compose.yml before every deployment is error-prone and breaks automation. Environment variables decouple configuration from code, allowing you to change behavior without modifying the image or compose file. This is one of the Twelve-Factor App principles, and Docker Compose supports it out of the box.

Pro tip: Never store secrets (passwords, tokens) directly in docker-compose.yml. Use .env files or secrets management tools instead.

Core concept / mental model

Think of Docker Compose as a recipe for running multiple containers. Each service in the compose file has an environment namespace – a set of key-value pairs that the container's processes can read. You have three sources for those values:

  1. Inline environment block – values written directly in the compose file.
  2. env_file directive – a path to a file containing key=value lines.
  3. Host shell variables – values from the machine running docker compose up, accessed via ${VAR} syntax.
Source Use case Security Portability
environment block Defaults, non‑secret config Low – values visible in compose file High – self‑contained
env_file Secrets, per‑env overrides Medium – file can be gitignored Medium – file must exist
Host shell variables (interpolated) CI/CD, dynamic values High – values never stored in repo Low – depends on caller's env

The mental model is layering: Compose first loads the .env file (if present) from the project directory, then substitutes any ${VAR} references in the compose file, and finally passes the resulting environment to each container.

How it works step by step

1. Using environment (inline)

This is the simplest method – list key=value pairs directly under the service.

services:
  web:
    image: my-python-app
    environment:
      - DB_HOST=localhost
      - DB_PORT=5432

What happens: When the container starts, any process inside (e.g., Python's os.environ.get('DB_HOST')) sees DB_HOST=localhost. This is fine for development defaults but not for per‑deployment differences.

2. Using a .env file

Create a file named .env in the same directory as your docker-compose.yml:

DB_HOST=staging.db.com
DB_USER=app_user
DB_PASSWORD=supersecret

Compose automatically reads this file when you run docker compose up. You can then reference the variables in your compose file:

services:
  web:
    image: my-python-app
    environment:
      - DB_HOST=${DB_HOST}
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}

Pro tip: Add .env and *.env to your .gitignore to prevent accidental commits.

3. Using env_file directive

If you prefer keeping the variable list outside the compose file altogether, use the env_file key:

services:
  web:
    image: my-python-app
    env_file:
      - production.env

This passes every variable from production.env into the container. The file must exist at the specified path when Compose runs.

4. Host shell interpolation

You can also pass variables directly from your terminal – no file needed:

export DB_USER=admin DB_PASSWORD=pass123
docker compose up

In your compose file, reference them with ${VAR:-default} (the :-default provides a fallback):

services:
  web:
    image: my-python-app
    environment:
      - DB_USER=${DB_USER:-default_user}

Caveat: This only works for the shell that runs docker compose. If you switch terminals, the variables are gone.

Hands-on walkthrough

Let's build a concrete example. You'll create an Nginx service that uses environment variables to set the server name and worker processes.

Step 1: Project structure

my_project/
├── docker-compose.yml
└── .env

Step 2: .env file

NGINX_HOST=my-app.local
NGINX_WORKERS=4

Step 3: Compose file with environment block

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    environment:
      - NGINX_HOST=${NGINX_HOST:-localhost}
      - NGINX_WORKERS=${NGINX_WORKERS:-1}

Step 4: Run it

cd my_project
docker compose up

Now inspect the container's environment:

docker compose exec web env | grep NGINX

Expected output:

NGINX_HOST=my-app.local
NGINX_WORKERS=4

The values from .env were injected into the container.

Step 5: Override from the shell

export NGINX_HOST=my-other-app.com
docker compose up

The container now sees NGINX_HOST=my-other-app.com because shell variables override .env file variables.

Important: The order of precedence is: shell environment > .env file > env_file directive (last read wins).

Compare options / when to choose what

Method Best for Downside
environment inline Defaults, non‑secret config Clutters compose file if many vars
.env file Per‑environment defaults Only one file; must be in project root
Multiple env_file entries Different configs for different services Merge order matters
Shell interpolation CI/CD, one‑off overrides Not persistent – lost on shell exit
env_file directive Long variable lists, secrets (file gitignored) Requires file path management

When to choose: - Use .env + ${VAR} references as your development default – clean and portable. - Use env_file for production secrets – the file can be provided by a deployment tool like Ansible or Kubernetes (not committed). - Use shell interpolation only for temporary overrides during testing.

Troubleshooting & edge cases

Symptom: Variable not passed to container

Cause: The variable name contains a character that is not alphanumeric or an underscore (e.g., MY-VAR). Compose interpolates only [a-zA-Z0-9_] identifiers.

Fix: Rename to MY_VAR.

Symptom: .env file not being read

Check: 1. The file must be named exactly .env (no extension). 2. It must be in the same directory from which you run docker compose up. 3. Lines must use = with no spaces around it: KEY=VALUE (not KEY = VALUE).

Symptom: Secret visible in docker compose config

If you run docker compose config, you'll see the resolved values. This is fine for environment variables – the real issue is if you commit the compose file with plaintext secrets. Always use .env files and add them to .gitignore.

Edge case: Boolean values

YAML interprets true, false, yes, no as booleans. To pass a literal string, quote it:

environment:
  - DEBUG="true"   # becomes the string 'true'
  - FLAG=false     # becomes boolean false, which may break your app

In .env files or env_file, all values are strings – no quoting needed.

What you learned & what's next

You now understand how to pass environment variables in Docker Compose using the environment block, the .env file, the env_file directive, and host shell interpolation. You can:

  • Explain why environment variables decouple configuration from code.
  • Use .env files to store per‑environment defaults safely.
  • Override variables at runtime without editing the compose file.
  • Troubleshoot common issues like unread variables or faulty .env syntax.

In the next lesson, you'll learn how to scale services with docker compose up --scale – essential for load testing and multi‑replica deployments.

Practice recap

Create a new project with a docker-compose.yml and a .env file containing APP_ENV=development. Define a service that uses ${APP_ENV} in an environment block and print it with docker compose exec <service> env. Then override APP_ENV=staging from the shell without editing the .env file and verify the change.

Common mistakes

  • Using hyphens in environment variable names: Docker Compose only interpolates variables with names matching [a-zA-Z_][a-zA-Z0-9_]*. A name like MY-VAR is not substituted.
  • Forgetting a fallback default when referencing shell variables: ${DB_HOST} without a default will cause Compose to fail if DB_HOST is not set. Always use ${VAR:-default} or ${VAR-default} to provide a fallback.
  • Putting .env file in a subdirectory: Compose looks for .env only in the project directory where you run docker compose up. Place it at the same level as your docker-compose.yml.
  • Misunderstanding variable substitution precedence: Shell environment variables override .env file values, which override env_file directive values. Not knowing this order can lead to unexpected overrides.

Variations

  1. Use multiple env_file entries to layer configurations: env_file: [./common.env, ./secrets.env, ./local-overrides.env] – later files override earlier ones.
  2. Substitute variables directly in the compose file without an environment block: image: myapp:${TAG:-latest} – the image tag itself becomes dynamic.
  3. Externalize configuration via Docker configs or secrets in Swarm mode: For production, consider docker config create or docker secret create instead of env files.

Real-world use cases

  • A Flask app reads DATABASE_URL from environment variables to connect to different PostgreSQL instances in dev, staging, and production.
  • A CI/CD pipeline builds one Docker image and passes build arguments and runtime configuration (e.g., API keys, feature flags) via shell variables during docker compose up.
  • A microservices platform using Compose for local development loads service-specific .env files (e.g., auth.env, payments.env) to keep configurations separate.

Key takeaways

  • Environment variables in Compose decouple configuration from code, enabling the same image to run anywhere.
  • The environment block is for inline defaults; .env files store per‑environment values; shell interpolation enables runtime overrides.
  • Variable substitution order: shell env > .env file > env_file directive – later overrides earlier.
  • Always quote YAML booleans (true, false) in the compose file to avoid type conversion bugs.
  • Use .env files with .gitignore to keep secrets out of version control.
  • Test variable injection with docker compose config to see the resolved environment without running containers.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.