SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-07

Docker Compose .env vs env_file vs environment: wetin win?

See why .env, env_file and environment no be the same for Docker Compose, which value wins, and why secrets no belong inside container environment variables.

Wetin be the three things wey people dey call an env file

Docker Compose get three separate mechanisms wey get names wey resemble each other and fit confuse person. The .env file dey fill ${VARIABLE} placeholders inside compose.yaml itself, before Compose even parse the file. The env_file: attribute dey load key/value pairs from one file enter the container environment. The environment: attribute dey set variables directly for the container, as dem write am for the compose file. Dem no be the same thing, and when two of dem set the same key, documented precedence order dey decide which one win.

This guide go show how each one dey work, prove the precedence with command wey you fit run, then cover the matter wey more important: anybody wey fit run docker inspect fit read environment variables, so passwords no suppose dey inside dem. If compose files generally still new to you, start with Docker Compose basics for VPS and come back here for configuration.

The .env file na for the compose file, no be for the container

Create one directory and put two files inside am.

mkdir -p ~/envdemo && cd ~/envdemo
printf 'ALPINE_TAG=3.20\n' > .env
services:
  demo:
    image: alpine:${ALPINE_TAG}
    command: printenv ALPINE_TAG

Now ask Compose wetin e actually parse.

docker compose config

The output show image: alpine:3.20. Placeholder don disappear because interpolation happen during parsing. Compose dey find .env inside project directory, wey be the directory wey hold the compose file, then e substitute every ${NAME} wey e find.

Then run the service.

docker compose run --rm demo

printenv ALPINE_TAG exit with status 1 and e print nothing. The variable no dey inside the container. Na this be the most common misunderstanding: .env configure the compose file, no be the process. A .env file wey get POSTGRES_PASSWORD=hunter2 inside am no do anything for your database unless some part of the compose file reference am.

${NAME:-default} provide fallback when the variable no set or e empty. ${NAME:?message} make Compose refuse to start and print your message. Na the correct choice for value wey no get safe default.

env_file dey load variables inside the container

The env_file: attribute dey name one or more files wey their contents go become container environment variables.

printf 'GREETING=from_env_file\nAPP_MODE=production\n' > app.env
services:
  demo:
    image: alpine:3.20
    command: printenv GREETING
    env_file:
      - ./app.env
docker compose run --rm demo

This one dey print from_env_file. The file format na plain KEY=value lines, one for each line, with # for starting comment. E no be shell. For most cases, quotes remain part of the value, and export prefixes no dey necessary. No put spaces around the = sign, because KEY = value go create variable wey name literally be KEY , with leading space inside the value.

If env_file path no dey, na error and Compose go stop. Mark am as optional if the file fit legitimately no dey:

    env_file:
      - path: ./app.env
        required: false

environment dey set variables inline

services:
  demo:
    image: alpine:3.20
    command: printenv GREETING
    environment:
      GREETING: from_environment

Two syntax dey accepted: the mapping form wey dey above, and a list form wey use - GREETING=from_environment. Dem behave the same way. The list form get one extra trick: bare key wey no get value go pass the variable through from the shell wey you run docker compose inside.

    environment:
      - GREETING
GREETING=from_my_shell docker compose run --rm demo

That one go print from_my_shell. Run am without setting GREETING for the shell, and Compose no go set anything, with no warning. You need know about silent pass-through failures, because service wey start with empty password variable often go start successfully and simply remain completely open.

Kedụ nke na-emeri

Docker kọwara precedence order, malite na nke kachasị elu: docker compose run -e for command line, emesia environment ma ọ bụ env_file, bụ́ nke value ya na-abịa site shell gị ma ọ bụ env file, emesia plain environment for compose file, emesia env_file, ma emesia ENV directive wey dem bake inside image.

Short version for daily work: environment: na-emeri env_file:, ma -e for command line na-emeri ha abụọ. Prove am for one file.

services:
  demo:
    image: alpine:3.20
    command: printenv GREETING
    env_file:
      - ./app.env
    environment:
      GREETING: from_environment
docker compose run --rm demo
docker compose run --rm -e GREETING=from_cli demo printenv GREETING

The first one dey print from_environment, so environment: override value wey dey for app.env. The second one dey print from_cli. Nothing for compose file override command line.

When container dey behave as if your config no apply, no guess. docker compose config dey print fully resolved file, while docker compose config --environment dey print interpolation variables wey Compose dey use. Most reports say “my env file dey ignored” because same value dey set twice for two different levels.

Why environment variables dey leak

Set password for environment: and e go store for the container configuration for disk. Any user wey dey inside docker group fit see am.

docker compose run -d --name leaky -e DB_PASSWORD=hunter2 demo sleep 300
docker inspect leaky --format '{{json .Config.Env}}'

The output get "DB_PASSWORD=hunter2" as plain text. Three other paths dey expose the same value. docker compose config go print am for terminal, and na so e fit end up pasted for support forum. Any process inside the container fit read /proc/1/environ, and every child process go inherit the variable. Application crash handlers too dey routinely dump the whole environment enter log or error report.

Membership for docker group practically mean root access for the host, so you no fit depend on this as privilege boundary. The guide on least privilege user accounts for VPS explain why e make sense to restrict that group for any shared box.

Compose secrets dey keep the value for file

Compose support file-based secrets. E mount the value inside the container as file instead of injecting am into environment.

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
secrets:
  db_password:
    file: ./db_password.txt

The secret dey mount for /run/secrets/db_password inside the container. The name after the slash na the secret name from the top-level secrets: block.

The _FILE suffix na convention wey Docker Official Images dey use, including postgres, mysql and mariadb. Those entrypoint scripts dey check for VARNAME_FILE, read the file, and use wetin dey inside. Docker no get this as feature, so e only work where the image implement am. Check the image documentation before you assume say SOMETHING_FILE go work. Applications wey no support am fit often read the file by themselves when dem start, or you fit pass the path and make your own entrypoint handle am.

Verify am from inside the container wey dey run:

docker compose exec db cat /run/secrets/db_password
docker compose exec db printenv POSTGRES_PASSWORD

The first command dey print the password. The second one no dey print anything, because the value never enter the environment. Na the main point be this: docker inspect for this container only show the harmless path.

Protect the source file for the host, because the secret only private as the file wey dey behind am:

chmod 600 db_password.txt

The practical middle ground for one VPS

Plenty self-hosted images no support _FILE variables, so na environment variables be the only way to pass the values. For one administrator VPS, the realistic goal na to stop the values from sitting inside a world-readable file for your project directory, and keep dem out of git.

sudo install -o root -g root -m 600 /dev/null /etc/myapp/app.env
sudo nano /etc/myapp/app.env
    env_file:
      - /etc/myapp/app.env

install -m 600 dey create the file with the correct mode already set, so nobody fit read am during any short period. Root own the file, so non-root user for the server no fit read am, though anybody wey fit run docker still fit read the value from the container. Add *.env and .env to .gitignore, then commit app.env.example wey get the key names with empty values instead. Password wey you commit don need rotation.

To rotate a value mean say you restart the service. Environment variables dey read once when the container process start, so editing the file no change anything until you run docker compose up -d --force-recreate db. Na the same pattern this n8n behind HTTPS for one VPS guide dey use, where the encryption key dey outside the compose file.

Configuration wey you split per environment

Compose dey read .env from project directory by default. You fit point am to another location with --env-file.

docker compose --env-file .env.staging config

Dem dey read multiple files in order, and later files dey override earlier ones. Keep default values wey no be secret for file wey you commit, and keep secrets for file wey never leave the server. The same thing apply to env_file:, where the last file wey you list go win if key duplicate.

FAQ

Why my .env file dey ignored inside the container?

E no dey ignored. The .env file only dey substitute ${NAME} placeholders for the compose file. E no dey set variables inside any container. To carry the value enter the container, reference am: environment: { KEY: "${NAME}" }, or use env_file: ./that-file.env instead.

Environment dey override env_file, or na the other way round?

environment: dey win. Docker documented order put the environment attribute above the env_file attribute, and both dey below docker compose run -e for the command line. If dem set one key for both places, the value for env_file no dey used, and no error dey show.

How I fit see the final value wey Compose go use?

Run docker compose config to print the fully resolved compose file with all interpolation applied. For container wey don already dey run, docker inspect <container> --format '{{json .Config.Env}}' go show exactly wetin its process receive.

Compose secrets dey encrypted?

No. File based secret dey mounted inside the container as plain file for /run/secrets/<name>, and the source file dey on the host disk without encryption. The benefit na scope, no be encryption: the value no dey inside the container environment, no dey show for docker inspect output, and e no dey enter crash dumps wey print the environment.

I fit use quotes and spaces inside env file?

Use KEY=value with spaces and leave the quotes out. Compose dey treat the whole remaining part of the line as the value, so quotes usually go become literal characters inside the value. Never put spaces around =, because the key go carry trailing space and nothing go match am.