---
title: "Docker Compose env_file: When to Use vs environment Variables"
slug: "difference-between-environment-and-env_file-in-docker-compose"
published: "2025-05-13"
updated: "2026-03-03"
validated: "2025-10-20"
categories:
  - "Docker"
tags:
  - "docker compose env_file"
  - "environment variables"
  - "docker secrets"
  - "env file precedence"
  - "container configuration"
  - "docker security"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "docker-compose@2"
  - "compose-spec@3.9"
status: "stable"
llm-purpose: "environment or env_file? Learn the key differences, pros/cons, and best practices for setting variables and keeping your configurations clean."
llm-prereqs:
  - "General familiarity with the article topic"
llm-outputs:
  - "Completed outcome: environment or env_file? Learn the key differences, pros/cons, and best practices for setting variables and keeping your configurations clean."
---

**Summary Triples**
- (environment key, where-defined, inline in docker-compose.yml (mapping or list syntax))
- (environment key, supports, Docker Compose variable interpolation (${VAR}) and runtime shell substitution)
- (environment key, best-for, service-specific configuration and small numbers of non-sensitive variables)
- (environment key, risk, exposes sensitive values in compose file (bad for version control))
- (env_file directive, function, loads KEY=VALUE pairs from external files into container environment)
- (env_file directive, supports, multiple files (array of files) to be merged)
- (env_file directive, best-for, cleaner compose files, reusable config across services/projects)
- (precedence, rule, environment entries override variables loaded from env_file)
- (.env file (project dir), used-for, variable substitution in docker-compose.yml (interpolation), not automatically an env_file unless referenced)
- (env_file format, syntax, plain KEY=VALUE lines (supports comments with #); used without YAML mapping)
- (secrets, recommendation, use Docker secrets or external secret managers for sensitive data; avoid committing secrets to compose or env_file)

### {GOAL}
environment or env_file? Learn the key differences, pros/cons, and best practices for setting variables and keeping your configurations clean.

### {PREREQS}
- General familiarity with the article topic

### {STEPS}
1. Follow the detailed walkthrough in the article content below.

<!-- llm:goal="environment or env_file? Learn the key differences, pros/cons, and best practices for setting variables and keeping your configurations clean." -->
<!-- llm:prereq="General familiarity with the article topic" -->
<!-- llm:output="Completed outcome: environment or env_file? Learn the key differences, pros/cons, and best practices for setting variables and keeping your configurations clean." -->

# Docker Compose env_file: When to Use vs environment Variables
> Master docker compose env_file: precedence rules, security trade-offs vs environment variables, and best practices for managing container secrets and config.
Matija Žiberna · 2025-05-13

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.

> [!TIP]
> **TL;DR — Quick reference**
>
> | Mechanism | Where defined | Interpolation support | Priority |
> |---|---|---|---|
> | `docker compose run -e` | CLI at runtime | Yes (from shell) | Highest |
> | `environment:` with interpolated value | compose.yml | Yes — reads from shell / `.env` | 2nd |
> | `environment:` with plain literal | compose.yml | No | 3rd |
> | `env_file:` directive | compose.yml → external file | No (inside the file) | 4th |
> | Image `ENV` directive | Dockerfile | No | Lowest |

---

## The Three Mechanisms

### 1. `environment:` — inline in compose.yml

Define variables directly in your compose file:

```yaml
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:

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

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

```env
# 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:

```yaml
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
# .env — auto-loaded from project root
HOST_PORT=3000
DB_PASSWORD=secret
```

```yaml
# 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.

> [!NOTE]
> 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
# .env (project root, auto-loaded)
DB_HOST=from-dotenv
```

```yaml
# 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.

```bash
# 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:

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

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

```bash
# 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.

> [!NOTE]
> `--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:

```yaml
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 inspect` | Yes (`Env` array) | No (mount only) |
| File format | `KEY=value` | Raw value (entire file) |
| Local dev complexity | Low | Low (Compose v2) |
| Production (Swarm/K8s) | ⚠️ Avoid for secrets | ✅ Native support |
| Best for | Non-sensitive config | Passwords, 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.

```env
# 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.

```yaml
# 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:

```yaml
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
# .env (git-ignored, project root)
NODE_ENV=development
HOST_PORT=3000
```

```env
# 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.

## LLM Response Snippet
```json
{
  "goal": "environment or env_file? Learn the key differences, pros/cons, and best practices for setting variables and keeping your configurations clean.",
  "responses": [
    {
      "question": "What does the article \"Difference Between environment and env_file in Docker Compose\" cover?",
      "answer": "environment or env_file? Learn the key differences, pros/cons, and best practices for setting variables and keeping your configurations clean."
    }
  ]
}
```