docker-compose
environment variables
docker
configuration management
devops

Re-using environment variables in docker-compose.yml

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Re-using environment variables in a Compose file keeps container configuration consistent across services and across environments such as local development, staging, and production. It also reduces copy-and-paste errors, which is where many Compose setups become hard to maintain.

There are two related ideas to keep straight. Compose can interpolate values into the YAML file before it runs, and it can also pass environment variables into the containers themselves. Good Compose configuration uses both deliberately.

Interpolating Shared Values with .env

The most common way to re-use values is to define them once in a project-level .env file and reference them from compose.yaml. Compose resolves expressions such as ${APP_PORT} when it loads the file.

Example .env file:

dotenv
1APP_PORT=8080
2DB_HOST=db
3DB_PORT=5432
4APP_ENV=development
5IMAGE_TAG=1.2.0

Example compose.yaml:

yaml
1services:
2  api:
3    image: "my-api:${IMAGE_TAG}"
4    ports:
5      - "${APP_PORT}:8080"
6    environment:
7      APP_ENV: "${APP_ENV}"
8      DB_HOST: "${DB_HOST}"
9      DB_PORT: "${DB_PORT}"
10
11  worker:
12    image: "my-worker:${IMAGE_TAG}"
13    environment:
14      APP_ENV: "${APP_ENV}"
15      DB_HOST: "${DB_HOST}"
16      DB_PORT: "${DB_PORT}"

This pattern gives you one source of truth for values reused by multiple services. A useful debugging step is:

bash
docker compose config

That command prints the resolved configuration so you can confirm interpolation happened the way you expected.

Re-using the Same Environment Block

If several services need the same set of environment variables, YAML anchors can reduce repetition inside the Compose file itself.

yaml
1x-app-env: &app-env
2  APP_ENV: "${APP_ENV}"
3  DB_HOST: "${DB_HOST}"
4  DB_PORT: "${DB_PORT}"
5
6services:
7  api:
8    image: "my-api:${IMAGE_TAG}"
9    environment:
10      <<: *app-env
11      SERVICE_NAME: api
12
13  worker:
14    image: "my-worker:${IMAGE_TAG}"
15    environment:
16      <<: *app-env
17      SERVICE_NAME: worker

This works well when the same block is repeated with only one or two service-specific overrides. The .env file still provides the values, while the anchor keeps the YAML readable.

When to Use env_file

env_file is different from project-level interpolation. It loads variables into the container environment from a separate file, which is useful when several services should receive the same runtime settings.

yaml
1services:
2  api:
3    image: "my-api:${IMAGE_TAG}"
4    env_file:
5      - ./common.env
6    environment:
7      SERVICE_NAME: api
8
9  worker:
10    image: "my-worker:${IMAGE_TAG}"
11    env_file:
12      - ./common.env
13    environment:
14      SERVICE_NAME: worker

Example common.env:

dotenv
LOG_LEVEL=info
FEATURE_FLAG_X=true
REQUEST_TIMEOUT=30

This is handy when a long list of container variables should be shared unchanged. It is less useful when you need those values for Compose-level fields such as image tags, port mappings, or volume paths. For those cases, use interpolation with .env or --env-file.

Defaults, Overrides, and Escaping

Compose supports shell-style defaults, so you can provide a fallback for optional values:

yaml
1services:
2  api:
3    image: "my-api:${IMAGE_TAG:-latest}"
4    ports:
5      - "${APP_PORT:-8080}:8080"

You can also require a variable to be present:

yaml
services:
  api:
    image: "my-api:${IMAGE_TAG:?IMAGE_TAG must be set}"

If you need a literal dollar sign rather than Compose interpolation, escape it with $$:

yaml
services:
  api:
    command: ["sh", "-c", "echo $$HOSTNAME"]

That tells Compose not to replace the value before the container starts.

Common Pitfalls

  • Confusing .env interpolation with env_file. The first affects how Compose builds the configuration, while the second injects variables into containers.
  • Assuming every variable is available everywhere. A value used in image: or ports: must be available during interpolation, not only inside the container.
  • Forgetting to validate the final file. docker compose config is the fastest way to catch missing values and bad substitutions.
  • Repeating large environment blocks by hand. Use anchors or shared files to avoid drift between services.
  • Accidentally interpolating a value meant for the shell inside the container. Use $$ when you need a literal dollar sign.

Summary

  • Use a project .env file for values reused across Compose fields.
  • Use YAML anchors when several services share the same environment block.
  • Use env_file when multiple containers need the same runtime variables.
  • Prefer defaults such as ${VAR:-default} for optional settings.
  • Run docker compose config to verify the resolved configuration before deployment.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.