Almost every Compose CLI flag has an environment-variable counterpart. Setting them once in your shell, in direnv, or in CI removes the need to retype the same flags on every command — and makes the configuration of a stack visible to anything that reads the environment.
The core set
The variables you reach for most often:
| Variable | What it sets | CLI equivalent |
|---|---|---|
COMPOSE_FILE | Path(s) to the Compose file(s) | -f, --file |
COMPOSE_PATH_SEPARATOR | Separator when listing multiple files | (in COMPOSE_FILE) |
COMPOSE_PROJECT_NAME | Project name | -p, --project-name |
COMPOSE_PROFILES | Profiles to enable | --profile |
COMPOSE_ENV_FILES | Project-level env files | --env-file |
COMPOSE_PARALLEL_LIMIT | Max parallel operations | --parallel (on some subcommands) |
COMPOSE_IGNORE_ORPHANS | Don’t warn about orphan containers | (no flag) |
COMPOSE_REMOVE_ORPHANS | Always remove orphans on up/down | --remove-orphans |
COMPOSE_ANSI | Control ANSI output (auto, never, always) | --ansi |
COMPOSE_PROGRESS | Progress style (auto, tty, plain, json, quiet) | --progress |
COMPOSE_STATUS_STDOUT | Send status messages to stdout instead of stderr | (no flag) |
COMPOSE_MENU | Disable the interactive Docker Desktop menu | (no flag) |
The full list lives in the Compose docs. The table above covers the ones that show up in real workflows.
COMPOSE_FILE with multiple files
The most useful pairing. Combine several Compose files into one stack without retyping -f every time:
export COMPOSE_FILE=compose.yaml:compose.override.yaml:compose.local.yaml
docker compose up
# Same as: docker compose -f compose.yaml -f compose.override.yaml -f compose.local.yaml up
The default separator is : on Linux/macOS and ; on Windows. Override it explicitly if you need to:
export COMPOSE_PATH_SEPARATOR=,
export COMPOSE_FILE=compose.yaml,compose.prod.yaml
Per-environment defaults with direnv
Drop a .envrc next to the compose.yaml:
# .envrc
export COMPOSE_FILE=compose.yaml:compose.dev.yaml
export COMPOSE_PROJECT_NAME=myapp-dev
export COMPOSE_PROFILES=full
cd into the directory and every docker compose invocation picks up the right files, the right project name, and the right profiles. Switch to another worktree and the defaults change automatically.
Pinning for deterministic CI runs
In a CI job, you want every docker compose call to behave the same way regardless of who triggers it. Set the variables once at the top of the job and forget about per-step flags:
# GitHub Actions snippet
env:
COMPOSE_FILE: compose.yaml:compose.ci.yaml
COMPOSE_PROJECT_NAME: ${{ github.run_id }}
COMPOSE_PROGRESS: plain
COMPOSE_REMOVE_ORPHANS: "true"
COMPOSE_IGNORE_ORPHANS: "false"
jobs:
test:
steps:
- uses: actions/checkout@v4
- run: docker compose up --wait
- run: docker compose exec api npm test
- run: docker compose down -v
if: always()
COMPOSE_PROJECT_NAME set to the run ID isolates each CI build, so two concurrent runs on the same runner don’t fight over the same project name.
COMPOSE_PROGRESS=plain (or json) keeps the log output readable in CI; the default TTY-aware progress bar is for humans.
Profiles via environment
COMPOSE_PROFILES works the same as --profile, with a comma-separated list:
COMPOSE_PROFILES=dev,observability docker compose up
Useful when the choice of profiles depends on context (local vs CI, dev vs demo) and you don’t want every script to remember which profiles to pass.
Precedence
Order of precedence, from lowest to highest:
- Defaults defined in Compose itself
- Values from the project
.env - Values from
COMPOSE_*env vars set in the shell - Explicit CLI flags
So a --file compose.alt.yaml on the command line always beats COMPOSE_FILE in the environment. The env vars are defaults, not overrides.
Pro tip: keep them visible
A small make print-compose-env (or shell alias) that dumps the active variables is invaluable when “it works on my machine” strikes:
env | grep ^COMPOSE_
Stick that in the troubleshooting section of your README. Half the support requests on Compose stacks end up being “you have COMPOSE_FILE set to something unexpected”.