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:

VariableWhat it setsCLI equivalent
COMPOSE_FILEPath(s) to the Compose file(s)-f, --file
COMPOSE_PATH_SEPARATORSeparator when listing multiple files(in COMPOSE_FILE)
COMPOSE_PROJECT_NAMEProject name-p, --project-name
COMPOSE_PROFILESProfiles to enable--profile
COMPOSE_ENV_FILESProject-level env files--env-file
COMPOSE_PARALLEL_LIMITMax parallel operations--parallel (on some subcommands)
COMPOSE_IGNORE_ORPHANSDon’t warn about orphan containers(no flag)
COMPOSE_REMOVE_ORPHANSAlways remove orphans on up/down--remove-orphans
COMPOSE_ANSIControl ANSI output (auto, never, always)--ansi
COMPOSE_PROGRESSProgress style (auto, tty, plain, json, quiet)--progress
COMPOSE_STATUS_STDOUTSend status messages to stdout instead of stderr(no flag)
COMPOSE_MENUDisable 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:

  1. Defaults defined in Compose itself
  2. Values from the project .env
  3. Values from COMPOSE_* env vars set in the shell
  4. 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”.

Further reading