Skip to content
ContainerYAML

Docker Compose environment variables and env_file

Compose uses .env and your shell to fill in compose.yaml, and passes values to containers only through environment, env_file or secrets. Precedence and Crunu.

Docker ComposeAt a glance

Env file that wins
.env

The only .env file it reads. See the load order

Public prefix
None

No names are copied into browser code by a prefix.

Values applied
At runtime

Values are read from the environment when the app runs.

Built-in secret store
Compose secrets, mounted as files under /run/secrets

docs.docker.com/compose/how-tos/use-secrets

Crunu command
crunu run --env shop/development -- docker compose upSee the Docker integration

Common pitfalls

  1. The project .env file fills in variables inside compose.yaml. Its values reach a container only through environment or env_file.
  2. During interpolation, your shell beats --env-file and .env, so a value exported in the terminal silently wins over the file.
  3. docker compose config prints the resolved file, including every secret you interpolated into it.
  4. Values passed with environment are visible to every process in the container and easily end up in logs. Compose recommends secrets.
  5. Secrets from an environment source work only with Docker Compose, not with docker stack deploy.
On this page

Docker Compose handles environment variables in two separate steps. First it fills in ${VAR} references inside compose.yaml from your shell, from files passed with --env-file, or from the .env file in the project directory. Then it gives each container only the variables the service declares with environment or loads with env_file, plus whatever the image sets with ENV. A value in .env doesn't reach a container by itself. For passwords and keys, Docker's own documentation says not to use environment variables at all and to use Compose secrets, which appear as files under /run/secrets. The rules below come from the Docker Compose documentation, checked October 30, 2026.

Step one: interpolation in compose.yaml

When Compose reads the file, it replaces ${DATABASE_URL} and similar references. If the same variable comes from several sources, the interpolation page gives this order:

PrioritySource
1your shell environment
2files passed with --env-file, later files overriding earlier ones
3the .env file in the project directory, if --env-file is not set

${VAR:-default} uses a default when the variable is unset or empty. ${VAR:?error} stops Compose with an error instead, which is the right choice for anything required.

docker compose config prints the file after interpolation. It's the fastest way to check what Compose will do, and also an easy way to print secrets into a terminal or a CI log. Use it with test values, or look at docker compose config --environment only on a machine you trust.

Step two: what the container gets

Inside the container, the precedence page ranks sources like this:

  1. docker compose run -e NAME=value.
  2. environment or env_file entries whose value was interpolated from the shell or an env file.
  3. A plain value under environment.
  4. Values from files listed under env_file.
  5. ENV in the image, which applies only when Compose sets nothing for that name.

The bare form passes a variable straight through: listing - DEBUG under environment with no value copies DEBUG from the shell that runs Compose, with no warning if it isn't set. Writing - DEBUG=${DEBUG} does the same through interpolation and warns when the variable is missing.

env_file paths are relative to compose.yaml. Several files are read in order, and later ones override earlier ones. Since Compose 2.24, an entry can be marked required: false so a missing file is skipped.

Secrets instead of environment variables

The Compose guide on secrets puts the case against environment variables plainly: they are often available to all processes, access is hard to track, and they can be printed in logs while debugging without anyone noticing. Its advice is to use secrets.

A Compose secret is a value that a service must be granted explicitly and that appears inside the container as a file at /run/secrets/NAME. The secrets reference lists two sources: file, the contents of a file on the host, and environment, the value of an environment variable on the host. Many official images, including postgres and mysql, accept a _FILE variant of their settings that points to such a file:

YAML
services:
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    environment: DB_PASSWORD

Secrets are supported on Linux containers only, and the environment source works with Docker Compose but not with docker stack deploy.

Run Compose with values from Crunu

crunu run decrypts a Crunu environment in memory and starts docker compose with its variables in the process environment. To Compose that's the shell, the highest priority for interpolation and the host environment for secrets with an environment source. Containers still get only what compose.yaml passes them, which is the point: the file says which service receives which value.

  1. Put the values into a Crunu environment such as shop/development, using the names that compose.yaml references.
  2. In compose.yaml, declare passwords and keys as top-level secrets with an environment source, and pass non-secret settings with bare names under environment.
  3. Start the stack with crunu run --env shop/development -- docker compose up.
  4. Delete the old .env from the project, or keep only non-secret defaults in it, since values from Crunu win anyway.

crunu itself writes nothing to disk. It hands the values to the Compose process, and Compose passes them to the Docker engine as your file describes. Inside the container, a secret is a file in /run/secrets, so the application should read it from there or through the image's _FILE convention.

crunu run passes only a short allowlist from your shell, including PATH and HOME, so Docker finds its config and contexts in your home directory. If your shell sets DOCKER_HOST for a remote or rootless daemon, or COMPOSE_PROJECT_NAME or COMPOSE_FILE, those are not passed. Put them in the environment or add --inherit-env.

One Compose file, several stages

Teams often keep .env.dev, .env.test and .env.ci next to compose.yaml and switch with --env-file. That works, but every one of those files holds a copy of the values, and the test and CI files tend to drift from what developers use. With Crunu the stage becomes the environment you name, and the files go away:

  • Developers run crunu run --env shop/development -- docker compose up.
  • CI runs the test suite with a service token: exec crunu run --env shop/test -- docker compose run --rm tests.
  • A one-off override still works the Compose way. docker compose run -e LOG_LEVEL=debug web beats everything else for that container, and a bare -e LOG_LEVEL takes the value from the shell, which under crunu run means from the environment.

The service token used in CI gets only shop/test. Even if the job's logs or caches leak, the development and production values were never in them.

What to avoid in images

Values you need at runtime should never be baked into an image with ENV or a copied .env file, because anyone who can pull the image can read them. Exclude .env with .dockerignore. For values a build step needs, Compose has build secrets, mounted only for the RUN step that uses them. Secrets in Docker images covers the ways images leak, and the leak paths below show where a value goes once it's inside one.

Example. Kiteline is a fictional company.

Frequently asked questions

Something else? Write to support@crunu.com

What is the difference between environment and env_file in Compose?

environment sets variables in compose.yaml, optionally filled in from your shell or .env. env_file reads variables from files into the container. When both set the same name, environment wins, and docker compose run -e beats both.

Why doesn't my container see a variable from .env?

The project .env file is for interpolation in compose.yaml. A container gets a variable only when the service lists it under environment, for example as a bare name, or loads a file with env_file.

How do I pass a database password without an environment variable in the container?

Declare a top-level secret with an environment source, grant it to the service, and point the image at the file under /run/secrets. Official images such as postgres read a _FILE variable for that.

Does crunu run work with docker compose up?

Yes. crunu run gives the Compose process the variables of a Crunu environment, and Compose uses them for interpolation and for secrets with an environment source. Containers get only what compose.yaml passes to them.

Sources

Every source this page relies on, with the date we last checked it.

  1. Docker Docs: Environment variables precedence · checked October 30, 2026
  2. Docker Docs: Variable interpolation · checked October 30, 2026
  3. Docker Docs: Set environment variables within your container's environment · checked October 30, 2026
  4. Docker Docs: Manage secrets securely in Docker Compose · checked October 30, 2026
  5. Docker Docs: Compose file reference, secrets · checked October 30, 2026

Keep your .env values in one place

Free during early access. Values are encrypted on your devices, each environment has its own key, and crunu run hands them to your app without writing a file.