Skip to content

How to Use Environment Variables in Docker Compose

Use environment variables in Docker Compose the right way: the .env file, variable substitution, environment vs env_file, and keeping secrets out of git.

SDSysadmin Desk October 10, 2026 7 min read
Photographic illustration for How to Use Environment Variables in Docker Compose: server racks with glowing container blocks

Environment variables in Docker Compose trip people up because the same term covers two different jobs. One is filling in values inside the Compose file — image tags, ports, paths. The other is setting variables inside the running container — database passwords, app config. They look similar, they sometimes share a .env file, and confusing them is why a variable shows up empty in your container even though it’s right there in your file.

This guide separates the two clearly. You’ll learn how variable substitution works with the .env file, how to set real environment variables inside containers with environment and env_file, which one wins when they collide, and how to keep secrets out of version control.

By the end, “why is this variable empty?” won’t be a mystery.

The two jobs environment variables do

Hold these apart in your head and everything else follows:

  1. Substitution (interpolation): Compose replaces ${VAR} placeholders in the Compose file itself before anything starts. The values come from your shell or a .env file in the project directory. This is for configuring the file — tags, ports, volume names.
  2. Container environment: Variables set inside a running container via the environment: or env_file: keys. This is what the application reads at runtime.

The big misunderstanding: the .env file does not automatically become the environment inside your containers. It only powers substitution in the Compose file. If you want a value inside a container, you have to put it there explicitly.

Variable substitution with the .env file

When you run a Compose command, Compose looks for a file literally named .env in the directory you’re in and uses it to fill any ${VAR} references in your Compose file.

NGINX_TAG=1.27
HOST_PORT=8080
services:
  web:
    image: nginx:${NGINX_TAG}
    ports:
      - "${HOST_PORT}:80"

Before starting anything, Compose substitutes those placeholders, so image: becomes nginx:1.27 and the port mapping becomes "8080:80". Change the values in .env, run docker compose up -d again, and the file reconfigures itself without you editing the YAML.

You can provide defaults inline, which is handy when a variable might be missing:

    image: nginx:${NGINX_TAG:-1.27}   # use 1.27 if NGINX_TAG is unset or empty
    ports:
      - "${HOST_PORT:-8080}:80"

To confirm what Compose actually resolved, ask it:

docker compose config

That prints the fully merged file with every ${VAR} replaced. It’s the fastest way to catch a placeholder that didn’t get filled.

Setting variables inside the container

To give the application running in a container its configuration, use the environment: key. This is where database credentials and app settings go:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: change-me
      POSTGRES_DB: appdb

Those three variables land inside the container, and the official Postgres image reads them on first start. You can also combine substitution with the container environment — pull a value from .env and pass it into the container:

POSTGRES_PASSWORD=a-real-secret
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Now the secret lives in .env (which you keep out of git) and gets injected into the container through environment:. This pattern — secret in .env, referenced explicitly under environment: — is the clean way to do it.

environment vs env_file

When you have more than a handful of variables, listing them all under environment: gets noisy. env_file: loads them from a file instead:

APP_DEBUG=false
APP_TIMEZONE=UTC
DB_HOST=db
DB_NAME=appdb
services:
  app:
    image: my-app:1.0
    env_file:
      - app.env
    environment:
      APP_DEBUG: "true"   # overrides the value from app.env

Every KEY=value line in app.env becomes an environment variable inside the container. Note the precedence: if the same key appears in both places, the inline environment: value wins. That makes environment: a clean way to override one or two values from a shared env_file.

Which mechanism does what

Mechanism Purpose
<code>.env</code> (project dir) Substitute ${VARS} in the Compose file
<code>environment:</code> Set variables inline for a service
<code>env_file:</code> Load many variables from a file into a service
<code>--env-file</code> flag Pick a different file for substitution

Don’t confuse the project-level .env (substitution) with a file you pass to env_file: (container variables). They’re different files doing different jobs, even if both end in .env.

Keeping secrets out of git

A .env file with real passwords should never be committed. The standard setup:

# Add the real secrets file to .gitignore
echo ".env" >> .gitignore

# Commit a template so others know which variables exist
cp .env .env.example
# then edit .env.example to replace real values with placeholders
NGINX_TAG=1.27
HOST_PORT=8080
POSTGRES_PASSWORD=replace-me

Commit .env.example, ignore .env. Anyone cloning the project copies the example to .env and fills in real values. This keeps the shape of your config in version control without leaking the secrets.

Environment variable setup checklist

  • .env in the project directory handles ${VAR} substitution in the Compose file
  • Container config goes under environment: or env_file:, not the project .env alone
  • Ran docker compose config to confirm values resolved correctly
  • .env is listed in .gitignore
  • A .env.example with placeholders is committed for others
  • Production secrets use Docker secrets, not plain env vars

A quick mental model

If a value belongs in the Compose file — an image tag, a host port, a volume name — it comes from substitution and lives in the project .env. If a value belongs to the application inside the container — a password, a feature flag, a connection string — it goes under environment: or env_file:. Asking “is this configuring the file or the program?” answers which mechanism to use almost every time.

This also explains the precedence order, which is worth knowing when values seem to come from nowhere:

Precedence for a container variable (highest wins)

1. Compose file environment: An inline value here beats everything below it.
2. Shell environment A variable set in your shell when you run docker compose.
3. env_file: entries Values loaded from the file(s) you list.
4. Image default (ENV) A default baked into the image with ENV in its Dockerfile.

So if a container variable isn’t what you expect, walk down that list. An inline environment: value is the most common reason an env_file setting appears to be “ignored” — it isn’t ignored, it’s overridden.

Putting it together

A realistic small project ends up using both paths at once: a .env file in the project directory to pick image tags and host ports, and environment: (or env_file:) entries to push real configuration and secrets into the containers. They cooperate, but they’re not the same thing — and remembering that is what stops the “my variable is empty” confusion before it starts.

If you’re still getting your footing with Compose files in general, the Docker Compose beginner guide covers the everyday commands, and the line-by-line Compose file walkthrough explains every key you’ll meet. With substitution and container environment sorted, your Compose files get shorter, safer, and far easier to move between machines.

Frequently asked questions

What's the difference between the .env file and the environment key?

The .env file feeds variable substitution in the Compose file itself — values like ${TAG} get replaced before the stack starts, and those values aren't automatically passed into containers. The environment: key sets variables inside a container at runtime. They solve different problems and are often used together.

Where does Compose look for the .env file?

By default Compose reads a file named .env in the same directory you run the command from (the project directory). You can point at a different file with the --env-file flag. The .env file is for interpolation in the Compose file, not a list of variables injected into every container.

How do I avoid committing secrets to git?

Keep real values in a .env file and add .env to .gitignore. Commit a .env.example with placeholder values so others know which variables are needed. For production, prefer Docker secrets or your platform's secret store over plain environment variables.

What's the difference between environment and env_file?

environment: lists variables inline in the Compose file. env_file: points at one or more files whose KEY=value lines are loaded into the container. Use env_file when you have many variables; use environment for a few, or to override specific values. If both set the same key, environment wins.

Why is my variable empty inside the container?

A common cause is expecting the .env file to be passed into containers automatically — it isn't. The .env file only fills ${VARS} in the Compose file. To get a value inside a container you must list it under environment: or load it with env_file:. Run docker compose config to see the resolved values.

Sources & further reading

Official vendor documentation referenced while writing this guide.

SD

Sysadmin Desk

Infrastructure & Cloud

Hands-on guidance for infrastructure, virtualization, and containers — Hyper-V, VMware, Docker, and the day-to-day operations work that keeps environments running.

MCSA Guru provides independent, educational IT guidance. Microsoft, Windows, Windows Server, Microsoft 365, Exchange, and Microsoft Teams are trademarks of Microsoft Corporation; Docker is a trademark of Docker, Inc. MCSA Guru is not affiliated with or endorsed by Microsoft or Docker. Always test changes in a safe environment before applying them in production.

Related guides

Fixing something right now?

Jump straight into the guide library or search for the exact error or task you are dealing with.