• Home
BuildWithMatija
Get In Touch
  1. Home
  2. Blog
  3. Docker
  4. Docker Compose env_file: When to Use vs environment Variables

Docker Compose env_file: When to Use vs environment Variables

Precedence rules, security trade-offs, and when to use each approach

13th May 2025·Updated on:3rd March 2026·MŽMatija Žiberna·
Docker
Docker Compose env_file: When to Use vs environment Variables

🐳 Docker & DevOps Implementation Guides

Complete Docker guides with optimization techniques, deployment strategies, and automation prompts to streamline your containerization workflow.

No spam. Unsubscribe anytime.

Related Posts:

  • •Fix Docker Permission Denied: Volumes, Bind Mounts & CI/CD
  • •How to Configure Development Containers with Docker
  • •Update Docker to Latest Version on Ubuntu

Early on, I kept bumping into the same frustrating problem: I'd set a variable in my .env file, Docker Compose would seem to ignore it, then I'd add it under environment: and it'd suddenly work — but then something else would break. I didn't understand the system.

Turns out Docker Compose doesn't have one mechanism for environment variables. It has three, and they interact in a specific precedence order that's non-obvious until you've been burned by it.

This guide covers all three mechanisms, how they rank against each other, the --env-file CLI flag most developers don't know about, when to switch to Docker Secrets instead, and the fixes for the most common failure modes.

TL;DR — Quick reference

MechanismWhere definedInterpolation supportPriority
docker compose run -eCLI at runtimeYes (from shell)Highest
environment: with interpolated valuecompose.ymlYes — reads from shell / .env2nd
environment: with plain literalcompose.ymlNo3rd
env_file: directivecompose.yml → external fileNo (inside the file)4th
Image ENV directiveDockerfileNoLowest

The Three Mechanisms

1. environment: — inline in compose.yml

Define variables directly in your compose file:

services:
  app:
    environment:
      - DB_HOST=postgres
      - DB_PORT=5432
      # Or mapping syntax:
      API_URL: http://localhost:${HOST_PORT}

The key feature here is interpolation: values can reference shell variables or variables from your .env file using ${VAR} syntax. If HOST_PORT=3000 is set in your shell or .env, the above resolves to http://localhost:3000 at runtime.

Use environment: for:

  • Service-specific config that doesn't need to be shared across services
  • Values that depend on shell interpolation
  • Overrides on top of a shared env_file:

The downside: literal secrets in your compose file end up in version control. Never put passwords or tokens here as hardcoded values — use interpolation from a git-ignored .env file instead.


2. env_file: — load from external files

Tell Compose to load variables from one or more external files:

services:
  app:
    env_file:
      - ./common.env
      - ./app.env

The file format is simple — one KEY=value per line, # for comments:

# common.env
DB_HOST=postgres
DB_PORT=5432
REDIS_URL=redis://cache:6379

Multiple files and load order: files listed later override those listed earlier. So if both common.env and app.env define DB_HOST, the value from app.env wins.

The required field (Compose v2.24.0+): by default, Compose throws an error if a listed env file doesn't exist. You can make files optional:

env_file:
  - path: ./default.env
    required: true    # default — error if missing
  - path: ./override.env
    required: false   # silently skip if missing

Useful for environment-specific overrides that may not exist in all deployment contexts.

The interpolation limitation: ${VAR} syntax does not work inside .env files. If you write API_URL=${BASE_URL}/api inside an env file, Compose treats it as a literal string — BASE_URL won't expand. Use environment: in the compose file for anything that needs interpolation.


3. The automatic .env pickup

This is the one that catches most people. Docker Compose automatically loads a .env file from your project root (next to your compose.yaml) without any configuration. You don't need to declare it anywhere.

This file serves a different purpose from env_file: — it's primarily a source for variable interpolation inside compose.yaml itself, not a direct injection into the container environment.

# .env — auto-loaded from project root
HOST_PORT=3000
DB_PASSWORD=secret
# compose.yaml — uses values from .env via interpolation
services:
  app:
    ports:
      - "${HOST_PORT}:3000"
    environment:
      - DB_PASSWORD=${DB_PASSWORD}

If you don't specify --env-file and haven't set COMPOSE_DISABLE_ENV_FILE=true, this automatic loading always happens. It's documented behavior, not a side effect.

The automatic .env file is not the same as the env_file: directive. The .env auto-load provides variables for interpolation in compose.yaml. The env_file: directive injects variables directly into the container's environment. Both work simultaneously — they serve different purposes.


The Full Precedence Order

When the same variable is defined in multiple places, Docker Compose resolves it in this order (highest wins):

  1. docker compose run -e VAR=value — CLI flag at runtime
  2. environment: or env_file: with the value interpolated from shell or .env
  3. environment: with a plain literal value
  4. env_file: directive
  5. ENV in the Dockerfile

A concrete example — DB_HOST defined in three places:

# .env (project root, auto-loaded)
DB_HOST=from-dotenv
# compose.yaml
services:
  app:
    env_file:
      - ./app.env     # contains: DB_HOST=from-env-file
    environment:
      - DB_HOST=from-environment

Result: the container sees DB_HOST=from-environment because environment: (level 3) beats env_file: (level 4).

Run docker compose run -e DB_HOST=from-cli app bash and the container sees from-cli — level 1 beats everything.


The --env-file CLI Flag

Most developers know about the env_file: directive in compose.yaml. Fewer know about the --env-file flag on the CLI itself — and they're different things.

# Override the default .env with a specific file at runtime
docker compose --env-file ./config/.env.staging up

This flag replaces the automatic .env pickup for that command — it doesn't merge with it. It changes which file Compose uses as the interpolation source, not which variables get injected into containers.

You can pass multiple --env-file flags in one command. Later files override earlier ones:

docker compose --env-file .env --env-file .env.override up

The practical use case: switching between environments without editing files.

# Development
docker compose --env-file envs/dev.env up

# Staging
docker compose --env-file envs/staging.env up

Each file sets different values for DB_HOST, API_URL, etc. — same compose file, different environments, no file editing required.

--env-file (CLI flag) changes the interpolation source for compose.yaml. env_file: (compose.yaml directive) injects variables into the container. They look similar but operate at different layers.


When to Use Docker Secrets Instead

Env files work fine for most config. But there's a security boundary worth knowing: environment variables are visible in docker inspect.

Run docker inspect <container> on any running container and you'll see the full Env array in the output — including anything you passed via environment: or env_file:. For non-sensitive config this is fine. For database passwords, API keys, and tokens in production, it's a problem.

Docker Compose supports secrets, which are mounted as files under /run/secrets/<n> inside the container rather than exposed as environment variables:

services:
  app:
    image: myapp:latest
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Your app reads the secret from /run/secrets/db_password at runtime.

env_file vs secrets — when to use which:

env_file:Docker Secrets
Visible in docker inspectYes (Env array)No (mount only)
File formatKEY=valueRaw value (entire file)
Local dev complexityLowLow (Compose v2)
Production (Swarm/K8s)⚠️ Avoid for secrets✅ Native support
Best forNon-sensitive configPasswords, tokens, keys

For local development, the overhead difference is small. For production, if you're using Docker Swarm or Kubernetes, secrets are the right tool for anything sensitive.


Troubleshooting: env_file Not Working

Three failure modes come up constantly.

1. Quoted values are treated as literals

This is the most common one. In a .env file, quotes are not string delimiters — they're literal characters.

# This sets DB_PASSWORD to: "secret" (with the quotes)
DB_PASSWORD="secret"

# This sets DB_PASSWORD to: secret (correct)
DB_PASSWORD=secret

If your app is receiving "secret" instead of secret, this is why. Strip the quotes.

2. Path is relative to the compose file, not your terminal

The path in env_file: resolves relative to the compose.yaml location, not your current working directory. If you run docker compose from a parent directory, paths that look correct may not resolve.

# This resolves relative to where compose.yaml lives
env_file:
  - ./config/app.env

If in doubt, use an absolute path or always run docker compose from the same directory as your compose.yaml.

3. UTF-8 BOM on Windows-created files

If you created the .env file on Windows (Notepad, certain editors), it may have a UTF-8 BOM (byte order mark) at the start of the file. Docker Compose can't parse this — the first variable name gets garbled or the file fails silently.

Fix: open the file in VS Code, check the encoding indicator in the bottom-right corner, and re-save as UTF-8 (not UTF-8 with BOM).


The Power Combo

The most maintainable pattern combines all three mechanisms in the right roles:

services:
  app:
    env_file:
      - ./common.env          # shared config across services
      - path: ./local.env
        required: false       # optional local overrides
    environment:
      - NODE_ENV=${NODE_ENV}  # interpolated from .env or shell
      - DEBUG=false           # service-specific, non-sensitive
# .env (git-ignored, project root)
NODE_ENV=development
HOST_PORT=3000
# common.env (can be committed if non-sensitive)
DB_HOST=postgres
DB_PORT=5432
REDIS_URL=redis://cache:6379

The .env provides interpolation values for the compose file. common.env holds shared non-sensitive config. environment: handles service-specific values and anything needing shell interpolation. For actual secrets in production, graduate to Docker Secrets.

And for switching between dev/staging/prod: docker compose --env-file envs/staging.env up — no file editing, no mistakes.

Thanks, Matija.

📄View markdown version
4

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

Fix Docker Permission Denied: Volumes, Bind Mounts & CI/CD
Fix Docker Permission Denied: Volumes, Bind Mounts & CI/CD

14th May 2025

How to Configure Development Containers with Docker
How to Configure Development Containers with Docker

18th August 2025

Update Docker to Latest Version on Ubuntu
Update Docker to Latest Version on Ubuntu

16th August 2025

Table of Contents

  • The Three Mechanisms
  • 1. `environment:` — inline in compose.yml
  • 2. `env_file:` — load from external files
  • 3. The automatic `.env` pickup
  • The Full Precedence Order
  • The `--env-file` CLI Flag
  • When to Use Docker Secrets Instead
  • Troubleshooting: env_file Not Working
  • The Power Combo
On this page:
  • The Three Mechanisms
  • The Full Precedence Order
  • The `--env-file` CLI Flag
  • When to Use Docker Secrets Instead
  • Troubleshooting: env_file Not Working
Build With Matija Logo

Build with Matija

Matija Žiberna

I turn scattered business knowledge into one usable system. End-to-end system architecture, AI integration, and development.

Quick Links

Payload CMS Websites
  • Bespoke AI Applications
  • Projects
  • How I Work
  • Blog
  • Payload CMS

    • Migration
    • Pricing

    Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Contact me →
    © 2026BuildWithMatija•Principal-led system architecture•All rights reserved