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

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.

The three things wey people dey call env file

Docker Compose get three separate mechanisms wey names dey look almost the same and fit confuse person. The .env file dey fill ${VARIABLE} placeholders inside compose.yaml itself, after Compose don read the YAML and before e validate or run anything. 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 dey write am for the compose file. Dem no be interchangeable. When two of dem set the same key, documented precedence order go decide which one win.

This guide go show how each one dey work. E go prove the precedence with command wey you fit run. Then e go cover the more important part: anybody wey fit run docker inspect fit read environment variables, so passwords no belong inside dem. If you never too familiar with compose files, 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. The placeholder don disappear because interpolation happen when Compose load the file. Compose dey look for .env inside the project directory: na the directory wey you give with --project-directory if you set am; otherwise na the directory of the first file wey you pass with -f; otherwise na your current directory. E dey substitute ${NAME} placeholders wey e find inside YAML values. E no dey interpolate keys, and doubled $$ go produce literal dollar sign instead of substitution.

Then run the service.

docker compose run --rm demo

printenv ALPINE_TAG exit with status 1 and print nothing. The variable no dey exist 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 go 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. This one na the correct choice for value wey no get safe default.

env_file variable dey enter 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 go print from_env_file. The file format na plain KEY=value lines, one for each line, with # for starting comment. E no be shell, though Compose dey parse am with some shell-like rules. Compose dey remove quotes around the value, so KEY="value" and KEY=value go give the same result. Double-quoted and unquoted values get ${...} placeholders interpolated, but single-quoted values dey used exactly as dem be. An export prefix dey accepted but e no necessary. Spaces around the = sign dey trimmed. The main thing to watch na inline comment: for unquoted value, space followed by # starts comment, so put quote around any value wey contain #.

If env_file path no dey, na error, and Compose go stop. Mark am 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

Dem accept two syntax: the mapping form wey dey above, and list form wey use - GREETING=from_environment. Dem behave the same way. Both get one extra trick: key wey no get value go pass the variable through from the shell where you run docker compose. For list form, na bare - GREETING; for mapping form, na GREETING: with nothing after the colon.

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

That one go print from_my_shell. If you run am without setting GREETING for the shell, Compose no go set anything, and e no go show warning. You need know about this kind silent pass-through failure, because service wey start with empty password variable fit start successfully, but e go simply dey wide 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

If you set password for environment:, e go store inside the container configuration for disk. Anybody 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 fit expose the same value. docker compose config go print am for terminal, and na so e fit end up pasted for support forum. Child processes inherit the variable by default, unless the parent deliberately start dem with replacement environment. Any process wey dey run as the same user as PID 1, or wey get CAP_SYS_PTRACE, fit read am from /proc/1/environ. For container where everything dey run as root, na every process fit do am. If application dey run as unprivileged user while PID 1 dey run as root, e go get permission error for that file instead. But the application still get the variable for its own environment. Application crash handlers too dey regularly dump the whole environment inside 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 good make you 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 dem dey ignore my .env file inside the container?

E no dey ignore am. The .env file dey substitute ${NAME} placeholders for the compose file, and e fit also set Compose own variables like COMPOSE_PROJECT_NAME or COMPOSE_FILE. E no dey ever set variables inside container. To put the value inside 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: win. Docker documented order put the environment attribute above the env_file attribute, and both dey below docker compose run -e for command line. If dem set one key for both places, the value for env_file no dey used, and no error go show.

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

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

Compose secrets dey encrypted?

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

I fit use quotes and spaces for env file?

Yes. Compose dey remove the quotes around the value, so KEY="value with spaces" and KEY=value with spaces both give value with spaces. Use single quotes when the value get $ wey no suppose get interpolated, and put quotes around any value wey contain #, because if e no get quotes, e go start comment. Spaces around = dey trim, so KEY = value still set KEY.

Dis article help you?