Pass environment variables in Compose
Learn how to pass environment variables in Docker Compose for flexible configuration. This lesson covers .env files, inline variables, and host environment access with practical examples.
Focus: pass environment variables in Compose
Every real-world application needs configuration that changes between environments – database URLs, API keys, or log levels. Hardcoding these into your Docker Compose file is a recipe for disaster: you'd either commit secrets to version control or manually edit files before every deployment. The solution is to pass environment variables in Docker Compose, keeping your services portable and your secrets safe.
This lesson shows you the three main ways to inject environment variables into your containers: the environment key, the env_file directive, and direct access to the host's shell variables. By the end, you'll be able to configure any Compose service without touching the image itself.
The problem this lesson solves
Imagine you have a Python web app that connects to a PostgreSQL database. The database host, user, and password are different on your laptop (localhost) vs. staging (staging.db.com) vs. production (prod-db-01.internal). If you bake those values into the Docker image, you'd need a separate image per environment – defeating the purpose of containers.
Even if you use a generic image, editing docker-compose.yml before every deployment is error-prone and breaks automation. Environment variables decouple configuration from code, allowing you to change behavior without modifying the image or compose file. This is one of the Twelve-Factor App principles, and Docker Compose supports it out of the box.
Pro tip: Never store secrets (passwords, tokens) directly in
docker-compose.yml. Use.envfiles or secrets management tools instead.
Core concept / mental model
Think of Docker Compose as a recipe for running multiple containers. Each service in the compose file has an environment namespace – a set of key-value pairs that the container's processes can read. You have three sources for those values:
- Inline
environmentblock – values written directly in the compose file. env_filedirective – a path to a file containing key=value lines.- Host shell variables – values from the machine running
docker compose up, accessed via${VAR}syntax.
| Source | Use case | Security | Portability |
|---|---|---|---|
environment block |
Defaults, non‑secret config | Low – values visible in compose file | High – self‑contained |
env_file |
Secrets, per‑env overrides | Medium – file can be gitignored | Medium – file must exist |
| Host shell variables (interpolated) | CI/CD, dynamic values | High – values never stored in repo | Low – depends on caller's env |
The mental model is layering: Compose first loads the .env file (if present) from the project directory, then substitutes any ${VAR} references in the compose file, and finally passes the resulting environment to each container.
How it works step by step
1. Using environment (inline)
This is the simplest method – list key=value pairs directly under the service.
services:
web:
image: my-python-app
environment:
- DB_HOST=localhost
- DB_PORT=5432
What happens: When the container starts, any process inside (e.g., Python's os.environ.get('DB_HOST')) sees DB_HOST=localhost. This is fine for development defaults but not for per‑deployment differences.
2. Using a .env file
Create a file named .env in the same directory as your docker-compose.yml:
DB_HOST=staging.db.com
DB_USER=app_user
DB_PASSWORD=supersecret
Compose automatically reads this file when you run docker compose up. You can then reference the variables in your compose file:
services:
web:
image: my-python-app
environment:
- DB_HOST=${DB_HOST}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
Pro tip: Add
.envand*.envto your.gitignoreto prevent accidental commits.
3. Using env_file directive
If you prefer keeping the variable list outside the compose file altogether, use the env_file key:
services:
web:
image: my-python-app
env_file:
- production.env
This passes every variable from production.env into the container. The file must exist at the specified path when Compose runs.
4. Host shell interpolation
You can also pass variables directly from your terminal – no file needed:
export DB_USER=admin DB_PASSWORD=pass123
docker compose up
In your compose file, reference them with ${VAR:-default} (the :-default provides a fallback):
services:
web:
image: my-python-app
environment:
- DB_USER=${DB_USER:-default_user}
Caveat: This only works for the shell that runs docker compose. If you switch terminals, the variables are gone.
Hands-on walkthrough
Let's build a concrete example. You'll create an Nginx service that uses environment variables to set the server name and worker processes.
Step 1: Project structure
my_project/
├── docker-compose.yml
└── .env
Step 2: .env file
NGINX_HOST=my-app.local
NGINX_WORKERS=4
Step 3: Compose file with environment block
services:
web:
image: nginx:latest
ports:
- "8080:80"
environment:
- NGINX_HOST=${NGINX_HOST:-localhost}
- NGINX_WORKERS=${NGINX_WORKERS:-1}
Step 4: Run it
cd my_project
docker compose up
Now inspect the container's environment:
docker compose exec web env | grep NGINX
Expected output:
NGINX_HOST=my-app.local
NGINX_WORKERS=4
The values from .env were injected into the container.
Step 5: Override from the shell
export NGINX_HOST=my-other-app.com
docker compose up
The container now sees NGINX_HOST=my-other-app.com because shell variables override .env file variables.
Important: The order of precedence is: shell environment >
.envfile >env_filedirective (last read wins).
Compare options / when to choose what
| Method | Best for | Downside |
|---|---|---|
environment inline |
Defaults, non‑secret config | Clutters compose file if many vars |
.env file |
Per‑environment defaults | Only one file; must be in project root |
Multiple env_file entries |
Different configs for different services | Merge order matters |
| Shell interpolation | CI/CD, one‑off overrides | Not persistent – lost on shell exit |
env_file directive |
Long variable lists, secrets (file gitignored) | Requires file path management |
When to choose:
- Use .env + ${VAR} references as your development default – clean and portable.
- Use env_file for production secrets – the file can be provided by a deployment tool like Ansible or Kubernetes (not committed).
- Use shell interpolation only for temporary overrides during testing.
Troubleshooting & edge cases
Symptom: Variable not passed to container
Cause: The variable name contains a character that is not alphanumeric or an underscore (e.g., MY-VAR). Compose interpolates only [a-zA-Z0-9_] identifiers.
Fix: Rename to MY_VAR.
Symptom: .env file not being read
Check:
1. The file must be named exactly .env (no extension).
2. It must be in the same directory from which you run docker compose up.
3. Lines must use = with no spaces around it: KEY=VALUE (not KEY = VALUE).
Symptom: Secret visible in docker compose config
If you run docker compose config, you'll see the resolved values. This is fine for environment variables – the real issue is if you commit the compose file with plaintext secrets. Always use .env files and add them to .gitignore.
Edge case: Boolean values
YAML interprets true, false, yes, no as booleans. To pass a literal string, quote it:
environment:
- DEBUG="true" # becomes the string 'true'
- FLAG=false # becomes boolean false, which may break your app
In .env files or env_file, all values are strings – no quoting needed.
What you learned & what's next
You now understand how to pass environment variables in Docker Compose using the environment block, the .env file, the env_file directive, and host shell interpolation. You can:
- Explain why environment variables decouple configuration from code.
- Use
.envfiles to store per‑environment defaults safely. - Override variables at runtime without editing the compose file.
- Troubleshoot common issues like unread variables or faulty
.envsyntax.
In the next lesson, you'll learn how to scale services with docker compose up --scale – essential for load testing and multi‑replica deployments.
Practice recap
Create a new project with a docker-compose.yml and a .env file containing APP_ENV=development. Define a service that uses ${APP_ENV} in an environment block and print it with docker compose exec <service> env. Then override APP_ENV=staging from the shell without editing the .env file and verify the change.
Common mistakes
- Using hyphens in environment variable names: Docker Compose only interpolates variables with names matching
[a-zA-Z_][a-zA-Z0-9_]*. A name likeMY-VARis not substituted. - Forgetting a fallback default when referencing shell variables:
${DB_HOST}without a default will cause Compose to fail ifDB_HOSTis not set. Always use${VAR:-default}or${VAR-default}to provide a fallback. - Putting
.envfile in a subdirectory: Compose looks for.envonly in the project directory where you rundocker compose up. Place it at the same level as yourdocker-compose.yml. - Misunderstanding variable substitution precedence: Shell environment variables override
.envfile values, which overrideenv_filedirective values. Not knowing this order can lead to unexpected overrides.
Variations
- Use multiple
env_fileentries to layer configurations:env_file: [./common.env, ./secrets.env, ./local-overrides.env]– later files override earlier ones. - Substitute variables directly in the compose file without an
environmentblock:image: myapp:${TAG:-latest}– the image tag itself becomes dynamic. - Externalize configuration via Docker configs or secrets in Swarm mode: For production, consider
docker config createordocker secret createinstead of env files.
Real-world use cases
- A Flask app reads
DATABASE_URLfrom environment variables to connect to different PostgreSQL instances in dev, staging, and production. - A CI/CD pipeline builds one Docker image and passes build arguments and runtime configuration (e.g., API keys, feature flags) via shell variables during
docker compose up. - A microservices platform using Compose for local development loads service-specific
.envfiles (e.g.,auth.env,payments.env) to keep configurations separate.
Key takeaways
- Environment variables in Compose decouple configuration from code, enabling the same image to run anywhere.
- The
environmentblock is for inline defaults;.envfiles store per‑environment values; shell interpolation enables runtime overrides. - Variable substitution order: shell env >
.envfile >env_filedirective – later overrides earlier. - Always quote YAML booleans (
true,false) in the compose file to avoid type conversion bugs. - Use
.envfiles with.gitignoreto keep secrets out of version control. - Test variable injection with
docker compose configto see the resolved environment without running containers.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.