SSD Nodes Learn 8GB RAM — $66/yr
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-02

Docker Compose: .env, env_file and environment na wetin?

Learn why .env, env_file and environment no be the same for Docker Compose, which setting wins, and why secrets no suppose dey inside environment variables.

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 file into the container environment. The environment: attribute dey set variables directly for the container, as dem dey write am inside the compose file. Dem no dey interchangeable. 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. E go prove the precedence with command wey you fit run. After that, e go cover the more important matter: anybody wey fit run docker inspect fit read environment variables, so passwords no belong inside dem. If compose files new to you, start with Docker Compose basics for VPS and come back here for configuration.

.env file na for compose file, no be for 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 really parse.

docker compose config

The output show image: alpine:3.20. The placeholder don comot because interpolation happen when Compose parse the file. Compose dey look for .env inside the project directory, wey be the directory wey hold the compose file, and e dey substitute every ${NAME} wey e find.

Then run the service.

docker compose run --rm demo

printenv ALPINE_TAG exit with status 1 and e no print anything. 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 dey 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 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. For most cases, quotes dey remain as part of the value, and export prefixes no dey needed. No put spaces around = sign, because KEY = value go create variable wey name na KEY , with leading space inside the value.

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

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

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

That one prints 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 silent pass-through failures, because service wey start with empty password variable fit start successfully, but e go simply dey wide open.

Which one win

Docker document the precedence order, starting from the highest: docker compose run -e for command line, then environment or env_file wey dem value come from interpolation from your shell or env file, then plain environment for compose file, then env_file, then ENV directive wey dem don bake inside the image.

The short version for everyday work: environment: dey beat env_file:, and -e for command line dey beat both. 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 print from_environment, so environment: override the value for app.env. The second one print from_cli. Nothing for the compose file fit override command line.

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

Why environment variables leak

Set a password for environment: and e go store for the container configuration on 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" for plain text. Three other paths dey expose the same value. docker compose config go print am for the terminal, and na so e fit end up pasted for a 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 regularly dump the whole environment inside a log or error report.

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

Compose secrets dey keep the value for file

Compose support file based secrets. The value dey mount inside the container as file, instead of injecting am into the 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. Their entrypoint scripts dey check for VARNAME_FILE, read the file, and use wetin dey inside. E no be Docker feature, so e go 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 one dey print the password. The second one no dey print anything, because the value never enter the environment. Na the main reason be this: docker inspect for this container dey show only 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

Di pragmatic middle ground for VPS

Plenty self hosted images no dey support _FILE variables, so na environment variables be the only way wey values fit enter. For one administrator VPS, the realistic goal na to stop make the values dey inside file for your project directory wey everybody fit read, and to keep dem comot from 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 mode already set, so no time dey when everybody fit read am. Root own am, so user wey no be root for the box no fit read am, but anybody wey fit run docker still fit read the value from inside 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 na password wey you must rotate.

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 wey the n8n behind HTTPS for VPS guide dey use, where the encryption key dey outside the compose file.

Configuration dey split per environment

Compose dey read .env from the project directory by default. Use --env-file point am to another location.

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

System dey read multiple files one after another, and later files dey override the earlier ones. Keep non secret default values for committed file, and keep secrets for file wey never comot from the server. The same rule dey apply to env_file:, where the last file wey you list go win if key dey duplicate.

FAQ

Why my .env file no dey work inside the container?

E no be say e dey ignored. The .env file only dey substitute ${NAME} placeholders inside 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 documentation 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 go show.

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

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

Compose secrets dey encrypted?

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

I fit use quotes and spaces inside an env file?

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