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

URL: https://crunu.com/environment-variables/docker-compose
Language: en
Updated: 2026-10-30

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](https://docs.docker.com/compose/how-tos/environment-variables/envvars-precedence/), 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](https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/) gives this order:

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

*Figure: Diagram of how Docker Compose loads environment variables. Env files are read in this order of priority, and the first one wins: .env. Values are read from the environment when the app starts. There is no public prefix: nothing reaches the browser unless your code sends it.*

`${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](https://docs.docker.com/compose/how-tos/environment-variables/envvars-precedence/) 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](https://docs.docker.com/compose/how-tos/use-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](https://docs.docker.com/reference/compose-file/secrets/) 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](https://crunu.com/blog/secrets-in-docker-images) covers the ways images leak, and the leak paths below show where a value goes once it's inside one.

*Figure: Diagram of where a secret goes once it is in a Docker image. Image layers: a later rm leaves the earlier layer intact; it stays after you delete it. Every registry it’s pushed to: and every mirror of it; it stays after you delete it. Image metadata: docker history shows each build step. Every host that pulls it: servers, CI runners and laptops; it stays after you delete it. Deleting the copy doesn’t undo the leak, so the value has to be reissued at its provider.*

## Related reading

- The [Docker integration](https://crunu.com/integrations/docker) shows how `crunu` runs in containers with a service token.
- [Node.js](https://crunu.com/environment-variables/nodejs) and [Django](https://crunu.com/environment-variables/django) for the app side of the same setup.
- [Fly.io](https://crunu.com/environment-variables/fly-io) and [Render](https://crunu.com/environment-variables/render), where the platform runs the containers.
- [How to share .env files with your team](https://crunu.com/guides/share-env-files-with-a-team).

## FAQ

### 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

- [Docker Docs: Environment variables precedence](https://docs.docker.com/compose/how-tos/environment-variables/envvars-precedence/) (accessed 2026-10-30)
- [Docker Docs: Variable interpolation](https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/) (accessed 2026-10-30)
- [Docker Docs: Set environment variables within your container's environment](https://docs.docker.com/compose/how-tos/environment-variables/set-environment-variables/) (accessed 2026-10-30)
- [Docker Docs: Manage secrets securely in Docker Compose](https://docs.docker.com/compose/how-tos/use-secrets/) (accessed 2026-10-30)
- [Docker Docs: Compose file reference, secrets](https://docs.docker.com/reference/compose-file/secrets/) (accessed 2026-10-30)
