Docker Compose Environment Variables: A Complete Guide
Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.
If you’ve ever hardcoded a database password directly into a docker-compose.yml file and then panicked when you realized it got pushed to a public GitHub repo, you already understand why managing a docker compose environment correctly matters. Environment variables are the standard way to inject configuration into containers without baking secrets or environment-specific values into your images. Get this wrong and you end up with leaked credentials, brittle deployments, or containers that behave differently in staging than in production.
This guide walks through every practical way to set, override, and debug environment variables in Docker Compose, plus the precedence rules that trip up almost everyone the first time.
Why Environment Variables Matter in a Docker Compose Setup
Environment variables let you decouple your application’s configuration from its code. Instead of rebuilding an image every time a database URL or API key changes, you pass that value in at runtime. This is the core idea behind the 12-Factor App methodology, and Docker Compose was built with it in mind.
A well-structured docker compose environment gives you:
If you’re new to Compose in general, it’s worth reading our Docker Compose networking guide first, since environment variables and networking often intersect (service discovery, hostnames, ports).
Setting Variables with the environment Key
The most direct way to pass environment variables into a service is the environment key inside your docker-compose.yml. You can write it as a list or a mapping:
services:
web:
image: node:20-alpine
environment:
- NODE_ENV=production
- API_PORT=3000
- DEBUG=false
or the map form:
services:
web:
image: node:20-alpine
environment:
NODE_ENV: production
API_PORT: "3000"
DEBUG: "false"
Both are functionally identical. The map form is easier to read and merge in overlay files, so most teams standardize on it. Numeric and boolean-looking values should be quoted, because YAML will otherwise try to parse 3000 or false as their native types instead of strings, which can cause subtle bugs in some applications.
You can also pull values from your shell into the compose file:
services:
web:
environment:
- API_KEY=${API_KEY}
Running API_KEY=abc123 docker compose up will inject abc123 into the container. This is useful for CI pipelines where secrets are already exported as shell variables.
Using env_file for Larger Configuration Sets
Once you have more than a handful of variables, listing them inline gets unwieldy. The env_file directive lets you point to a plain text file instead:
services:
web:
image: node:20-alpine
env_file:
- .env.production
The file itself is just KEY=VALUE pairs, one per line:
NODE_ENV=production
API_PORT=3000
DATABASE_URL=postgres://user:pass@db:5432/appdb
A few rules to know:
# are treated as comments$HOME substitution)env_file entries; later files override earlier onesThis is the cleanest option when you’re managing dozens of variables across multiple services, and it keeps your docker-compose.yml readable. Just make sure any file containing real secrets is listed in .gitignore — a mistake we cover in more depth in our container secrets management guide.
The Special .env File and Variable Substitution
Docker Compose treats a file literally named .env in the same directory as your docker-compose.yml differently from other env files. It isn’t injected into containers automatically — instead, Compose reads it to substitute ${VARIABLE} placeholders inside the compose file itself, before the file is even parsed.
# .env
POSTGRES_VERSION=16
POSTGRES_PASSWORD=supersecret
COMPOSE_PROJECT_NAME=myapp
services:
db:
image: postgres:${POSTGRES_VERSION}
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
This distinction confuses a lot of people: .env drives compose-file templating, while env_file: drives container environment injection. You can use both together — a .env file for values referenced in the YAML structure (image tags, project name, ports) and env_file: entries for values that need to land inside the running container.
You can verify what Compose actually resolved with:
docker compose config
This prints the fully rendered configuration with all variables substituted, which is the fastest way to catch a typo before it reaches production.
Using Multiple Compose Files to Override Environment Values
Docker Compose supports merging multiple compose files together, which is handy for maintaining a base environment configuration and layering environment-specific overrides on top:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# docker-compose.prod.yml
services:
web:
environment:
NODE_ENV: production
LOG_LEVEL: warn
Values in the later file override matching keys in the base file, following the same merge rules as top-level list-vs-map behavior in the compose spec. This pattern is especially useful for keeping a shared docker-compose.yml for local development and a slim docker-compose.prod.yml that only overrides the handful of variables that actually differ — log level, replica counts, or a production database URL. Compose also automatically loads a file named docker-compose.override.yml if it exists in the same directory, without needing the -f flag at all, which is convenient for keeping personal local overrides out of version control.
Precedence: Which Value Actually Wins
When the same variable is set in multiple places, Compose applies a strict precedence order, from highest to lowest priority:
1. Variables set with docker compose run -e VAR=value
2. Variables defined under the environment key in the compose file
3. Variables from files listed under env_file
4. Variables set in the shell environment before running docker compose up
5. Variables defined in a .env file (used only for compose-file substitution, not container injection)
In practice, this means an environment: entry will always override a matching key from env_file:, even if env_file is listed. If your container isn’t picking up the value you expect, check every one of these layers — this is the single most common source of “why isn’t my environment variable working” issues reported on the official Docker Compose documentation and forums.
Managing Secrets Safely
Environment variables are convenient, but they aren’t a secure secrets store by themselves — anything passed via environment: or .env is visible to anyone who can run docker inspect or docker compose config against the running stack. For sensitive values in production, consider:
secrets: block, which mounts values as files rather than environment variables.env files (chmod 600 .env).env or env_file targets to git — add them to .gitignore immediately after creating them.env files per environment (.env.dev, .env.staging, .env.production) and loading the correct one explicitlyIf you’re deploying these stacks on your own infrastructure, make sure the host itself is locked down too — a misconfigured environment variable is a much smaller risk than an exposed Docker socket on a public-facing VPS. Providers like DigitalOcean make it straightforward to spin up a hardened droplet specifically for container workloads, with private networking so your secrets never traverse the public internet.
Once your environment variables are correctly configured, the next challenge is observability — knowing when a misconfigured variable causes a service to silently fail. Tools like BetterStack can monitor container logs and alert you the moment a service crashes from a missing environment variable, which saves hours of manual debugging in production.
Debugging a Broken Environment
When a container isn’t seeing the variable you expect, work through this checklist:
# See what Compose resolved before starting anything
docker compose config
# Check the environment inside a running container
docker compose exec web env
# Check a stopped/crashed container's environment
docker inspect <container_id> --format '{{.Config.Env}}'
Common causes of mismatched values:
DATABSE_URL vs DATABASE_URL).env file (Compose does not strip surrounding quotes consistently across versions)environment: entry silently overriding an env_file: value.env — Compose doesn’t hot-reload environment changes; you need docker compose up -d --force-recreateA Real-World Multi-Service Example
Here’s how these pieces fit together in a typical stack with a web app, a database, and Redis cache. Assume the following directory structure:
myapp/
├── docker-compose.yml
├── docker-compose.prod.yml
├── .env
├── .env.production
└── app/
The base docker-compose.yml stays generic:
services:
web:
build: ./app
env_file:
- .env.production
environment:
REDIS_URL: redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:${POSTGRES_VERSION}
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- db-data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
db-data:
Notice the layering: ${POSTGRES_VERSION} and ${POSTGRES_PASSWORD} come from the auto-loaded .env file and get substituted into the YAML structure itself, while REDIS_URL is hardcoded because it never changes between environments, and the application’s own secrets live in .env.production, loaded via env_file:. This split keeps infrastructure-level values (image versions, resource names) separate from application-level configuration (API keys, connection strings), which makes the compose file easier to audit at a glance.
Common Mistakes to Avoid
.env files with real secrets to version control instead of using .env.example as a templatedocker compose up automatically picks up changes to .env — you need to recreate containers.env (compose-file substitution) with env_file: (container injection) and expecting them to behave the same wayRecommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.
FAQ
Q: What’s the difference between .env and env_file in Docker Compose?
A: .env is read by Compose itself to substitute ${VAR} placeholders inside the docker-compose.yml file before parsing. env_file injects variables directly into a container’s runtime environment. They serve different purposes and can be used together.
Q: Can I use multiple .env files with different names?
A: Compose only auto-loads a file literally named .env. To use a differently named file for substitution, pass it explicitly with docker compose --env-file .env.staging up.
Q: Does environment: override env_file: if both set the same variable?
A: Yes. Values under the environment: key always take precedence over matching keys loaded from env_file:.
Q: Are environment variables secure enough for passwords and API keys?
A: For low-stakes local development, generally fine. For production secrets, use Docker’s secrets: block, a vault service, or your cloud provider’s secrets manager instead, since environment variables are visible via docker inspect.
Q: Why isn’t my .env file being picked up?
A: The most common cause is the file not being in the same directory as docker-compose.yml, or a typo in the filename (it must be exactly .env, not env or .env.txt).
Q: How do I pass a variable from the shell into a container at runtime?
A: Export it in your shell and reference it in the compose file, e.g. environment: - API_KEY=${API_KEY}, then run API_KEY=abc123 docker compose up.
Wrapping Up
A clean docker compose environment setup comes down to picking the right mechanism for the job: environment: for a handful of values, env_file: for larger sets, and .env for compose-file-level substitution. Once you understand the precedence order and keep secrets out of version control, most of the “why isn’t this variable working” headaches disappear. For a deeper look at structuring multi-service stacks around these variables, check out our complete Docker Compose guide. Test your setup locally with docker compose config before every deploy, and you’ll catch most environment-related surprises long before they reach production.
Leave a Reply