Docker Compose Variable Substitution Deep-Dive¶
- This lab is a deep-dive into Docker Compose variable substitution - the mechanism that makes your compose files dynamic, environment-aware, and reusable across different contexts.
- You’ll learn every syntax form (
${VAR},${VAR:-default},${VAR:?error},${VAR:+replacement},$$escaping), understand the.envfile loading order, master theenv_fileper-service directive, and explore real-world patterns like concatenation, nested variables, and multi-env configuration. - Each concept is backed by a standalone example and a progressive set of exercises that build from simple defaults to production-grade multi-file setups.

CTRL + click to open in new window
Table of Contents¶
- Docker Compose Variable Substitution Deep-Dive
- CTRL + click to open in new window
- Table of Contents
- Why Variable Substitution Matters
- Substitution Syntax Reference
- 1. Basic Substitution -
${VAR} - 2. Default Value -
${VAR:-default} - 3. Mandatory Variable -
${VAR:?error} - 4. Alternate Value -
${VAR:+replacement} - 5. Literal Dollar Sign -
$$Escape - The
.envFile - Project-Level Variables - Loading Order and Precedence
- The
env_fileDirective - Per-Service Variables - Multi-File Strategy
- Precedence Rules (From Highest to Lowest)
- Advanced Patterns
- Nested / Indirect Variable References
- Concatenation
- Empty String Defaults
- Dynamic
env_filePaths - Environment-Specific Configuration
- Lab Examples
- Example 1: Basics - Default Values
- Example 2: Mandatory Variables
- Example 3: Advanced - All Techniques Combined
- Example 4: Env-File Strategy
- Best Practices
- Troubleshooting
- Hands-On Tasks
- Task 1: Run the Basics Example
- Task 2: Enforce Mandatory Variables
- Task 3: Explore All Advanced Techniques
- Task 4: Multi-File Environment Strategy
- Task 5: Build Your Own Multi-Env Setup
- Verification Checklist
- Additional Resources
- Next Steps
- Cleanup
Why Variable Substitution Matters¶
Hardcoding values in docker-compose.yaml creates brittle configurations that break across environments. Variable substitution solves this by letting you:
- Use the same compose file for development, staging, and production
- Keep secrets out of version control (load them from
.envor secret files) - Set sensible defaults that can be overridden per deployment
- Fail fast when required configuration is missing
- Compose dynamic values from multiple variables (concatenation)
Instead of this:
You write this:
services:
web:
image: nginx:${NGINX_TAG:-alpine}
ports:
- "${HOST_PORT:-8080}:80"
environment:
- NODE_ENV=${NODE_ENV:-development}
Now one file works everywhere - just change the .env file or shell environment.
Substitution Syntax Reference¶
Docker Compose supports five variable syntax forms, inspired by POSIX shell parameter expansion:
1. Basic Substitution - ${VAR}¶
Replaces ${VAR} with the value of the environment variable. If the variable is unset or empty, the compose file is invalid and docker compose config will raise an error.
# Must set these before running:
export NGINX_TAG=alpine
export HOST_PORT=8080
docker compose up -d
# Alternatively, define them in a .env file:
echo "NGINX_TAG=alpine" >> .env
echo "HOST_PORT=8080" >> .env
2. Default Value - ${VAR:-default}¶
If VAR is unset or empty, the :- operator substitutes the literal string after the colon. This is the most common form - it makes your compose file self-documenting with sensible fallbacks.
services:
web:
image: nginx:${NGINX_TAG:-alpine}
ports:
- "${HOST_PORT:-8195}:80"
environment:
- APP_NAME=${APP_NAME:-MyApp}
- APP_ENV=${APP_ENV:-development}
- APP_VERSION=${APP_VERSION:-latest}
- LOG_LEVEL=${LOG_LEVEL:-info}
- MAX_CONNECTIONS=${MAX_CONNECTIONS:-100}
# With defaults only (no .env needed):
docker compose up -d
# Override specific variables:
APP_NAME=ProductionApp APP_ENV=production docker compose up -d
3. Mandatory Variable - ${VAR:?error}¶
If VAR is unset or empty, Docker Compose prints the error message and refuses to start. Use this for values that must always be provided - passwords, API keys, etc.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER:?error}
POSTGRES_PASSWORD: ${DB_PASS:?password is required}
POSTGRES_DB: ${DB_NAME:-myapp}
api:
image: node:20-alpine
environment:
DB_USER: ${DB_USER:?}
DB_PASS: ${DB_PASS:?}
API_KEY: ${API_KEY:?API key is mandatory}
NODE_ENV: ${NODE_ENV:-development}
# This will fail with a clear error:
docker compose up -d
# ❌ Compose file is invalid because:
# DB_USER is not set and is required
# Provide the mandatory variables:
DB_USER=admin DB_PASS=supersecret API_KEY=sk-abc-def-ghi docker compose up -d
| Syntax | Error Message |
|---|---|
${VAR:?} | VAR is not set and is required |
${VAR:?error} | VAR is not set and is required / error |
${VAR:?custom message} | VAR is not set and is required / custom message |
4. Alternate Value - ${VAR:+replacement}¶
If VAR is set and non-empty, substitutes the replacement string. Otherwise substitutes an empty string. This is the inverse of :-.
services:
web:
image: nginx:alpine
environment:
# If DEBUG_MODE is set to "true", use "debug"; otherwise empty
LOG_LEVEL: ${DEBUG_MODE:+debug}
# Conditional feature flag
EXTRA_FEATURES: ${ENABLE_BETA:+beta,experimental}
# LOG_LEVEL will be empty:
docker compose up -d
# LOG_LEVEL becomes "debug":
DEBUG_MODE=true docker compose up -d
5. Literal Dollar Sign - $$ Escape¶
When you need a literal $ in your configuration (e.g., shell variables inside a command or entrypoint), double the dollar sign. $$ is replaced with a single $ before the value reaches the container.
services:
web:
image: nginx:alpine
environment:
# This becomes: RAW_DOLLAR=$not_a_variable
RAW_DOLLAR: $$not_a_variable
app:
image: alpine
command: sh -c "echo \$$HOME" # Escaped for shell
# Verify the escaping:
docker compose run --rm web env | grep RAW_DOLLAR
# Output: RAW_DOLLAR=$not_a_variable
# Without $$, Docker Compose would try to substitute ${not_a_variable}
The .env File - Project-Level Variables¶
By default, Docker Compose reads a file named .env in the project directory (where you run docker compose from). Every variable defined in .env is available for substitution in docker-compose.yaml.
# .env - loaded automatically by docker compose
NGINX_TAG=alpine
PORT=8196
APP_NAME=VariableSubDemo
LOG_LEVEL=debug
# docker-compose.yaml - uses variables from .env
services:
web:
image: nginx:${NGINX_TAG:-alpine}
ports:
- "${PORT:-8196}:80"
environment:
- APP_NAME=${APP_NAME:-MyApp}
- LOG_LEVEL=${LOG_LEVEL:-info}
Loading Order and Precedence¶
Docker Compose loads variables in this order (later sources override earlier ones):
- Shell environment variables (highest priority)
.envfile in the project directoryenv_filedirective inside a service definitionenvironmentsection inside a service definition- Variable defaults (
:-syntax inside the compose file)
graph TD
A[Shell env<br/>export VAR=val] -->|Highest| B[.env file]
B --> C[env_file directive]
C --> D[environment: section]
D --> E[Default :- values]
E -->|Lowest| F[Final Resolved Value]
Important: The
.envfile is used for variable substitution in the compose file itself. It is not automatically passed to containers. To make variables available inside containers, use theenvironmentsection orenv_file.
The env_file Directive - Per-Service Variables¶
The env_file directive loads environment variables into the container from one or more files. Unlike .env (which is global and used for substitution), env_file is per-service.
services:
web:
image: nginx:alpine
env_file:
- ./config/common.env
- ./config/development.env
- ./secrets/credentials.env
Multi-File Strategy¶
You can specify multiple env_file entries. Docker Compose processes them in order - variables from later files override earlier ones. This enables a layered configuration pattern:
Project Structure:
├── .env ← Project-level (substitution)
├── docker-compose.yaml
├── config/
│ ├── common.env ← Shared defaults
│ ├── development.env ← Dev overrides
│ └── production.env ← Prod overrides
└── secrets/
└── credentials.env ← Sensitive values (git-ignored)
# config/common.env - shared across all environments
APP_NAME=MultiEnvApp
CACHE_ENABLED=true
CACHE_TTL=300
# config/development.env - dev-specific
LOG_LEVEL=debug
DEBUG=true
HOT_RELOAD=true
# config/production.env - prod-specific
LOG_LEVEL=warn
DEBUG=false
HOT_RELOAD=false
# secrets/credentials.env - sensitive (never commit!)
DB_USER=admin
DB_PASS=supersecret123
API_KEY=sk-abc-def-ghi
# docker-compose.yaml
services:
web:
image: nginx:${NGINX_TAG:-alpine}
ports:
- "${PORT:-8197}:80"
env_file:
- ./config/common.env
- ./config/${APP_ENV:-development}.env # Dynamic path!
- ./secrets/credentials.env
environment:
APP_NAME: ${APP_NAME:-FallbackApp} # Overrides env_file
graph LR
A[APP_ENV=development] --> B[./config/development.env]
C[APP_ENV=production] --> D[./config/production.env]
B --> E[env_file layer 2]
D --> E
F[./config/common.env] -->|layer 1| E
G[./secrets/credentials.env] -->|layer 3| E
E --> H[Container Runtime Env]
# Switch environments by changing APP_ENV:
APP_ENV=development docker compose up -d # Uses development.env
APP_ENV=production docker compose up -d # Uses production.env
Precedence Rules (From Highest to Lowest)¶
Understanding precedence is critical for predictable behavior. Here is the complete hierarchy:
| Priority | Source | Scope | Used For |
|---|---|---|---|
| 1 (highest) | Shell environment (export VAR=val) | Global | CI/CD, ad-hoc overrides |
| 2 | environment: section in service | Per-service | Container-injected variables |
| 3 | env_file: files (last wins) | Per-service | Layered config files |
| 4 | .env file in project directory | Global | Substitution defaults |
| 5 | :- default in compose file | Inline | Fallback values |
Key nuance: Shell environment variables and .env are used for substitution in the compose file. The environment section and env_file define what actually reaches the container runtime.
# docker-compose.yaml
services:
web:
image: nginx:${NGINX_TAG:-alpine} # Substitution: shell > .env > :- default
env_file:
- ./config/common.env # Runtime: loaded into container
environment:
APP_NAME: ${APP_NAME:-FallbackApp} # Substitution + override of env_file
Rule of thumb: Use
.envfor compose-file substitution defaults. Useenv_filefor layered runtime configuration. Useenvironment:for per-service overrides and inline values.
Advanced Patterns¶
Nested / Indirect Variable References¶
You can nest variable references by using a variable’s value as the default for another:
How this resolves:
- If
ENV_NAMEis set, use it directly - If
ENV_NAMEis unset, checkDEFAULT_ENV - If
DEFAULT_ENVis also unset, usedevelopment
# All three produce different results:
docker compose up -d
# → ENV_NAME=development (both fallbacks)
DEFAULT_ENV=staging docker compose up -d
# → ENV_NAME=staging (second fallback)
ENV_NAME=production docker compose up -d
# → ENV_NAME=production (direct value)
Concatenation¶
Build compound values by combining multiple variables:
services:
web:
image: nginx:alpine
environment:
DB_URL: "postgres://${DB_USER:-app}:${DB_PASS:-secret}@${DB_HOST:-db}:5432/${DB_NAME:-myapp}"
With defaults set via .env:
The resolved value becomes:
Empty String Defaults¶
Sometimes you want a variable to resolve to an empty string rather than a fallback:
If OPTIONAL_FEATURE is unset, it becomes an empty string ("") instead of causing an error. This is useful for optional flags or feature toggles.
Dynamic env_file Paths¶
The env_file paths can themselves use variable substitution, enabling environment-aware config loading:
services:
web:
image: nginx:${NGINX_TAG:-alpine}
env_file:
- ${ENV_FILE:-./.env}
- ./secrets/env.${APP_ENV:-dev}
# .env file
NGINX_TAG=alpine
PORT=8196
APP_ENV=dev
# secrets/env.dev - loaded when APP_ENV=dev
DB_USER=dev_user
DB_PASS=dev_pass
DB_HOST=localhost
# secrets/env.prod - loaded when APP_ENV=prod
DB_USER=prod_user
DB_PASS=prod_pass_123
DB_HOST=db.prod.internal
Environment-Specific Configuration¶
Combine everything above for a production-grade pattern that selects configuration at runtime:
services:
web:
image: nginx:${NGINX_TAG:-alpine}
ports:
- "${PORT:-8196}:80"
env_file:
- ./config/common.env
- ./config/${APP_ENV:-development}.env
- ./secrets/credentials.env
environment:
APP_NAME: ${APP_NAME:-MyApp}
DB_URL: "postgres://${DB_USER:-app}:${DB_PASS:-secret}@${DB_HOST:-db}:5432/${DB_NAME:-myapp}"
API_KEY: "${API_KEY:?API key required}"
# Development
APP_ENV=development docker compose up -d
# Production (fails if API_KEY is missing)
APP_ENV=production API_KEY=sk-live-xxx docker compose up -d
Lab Examples¶
Example 1: Basics - Default Values¶
Directory: examples/basics/
Demonstrates the :- operator for ports, environment variables, and image tags. Every variable has a sensible default so the compose file works out of the box.
# examples/basics/docker-compose.yaml
services:
web:
image: nginx:${NGINX_TAG:-alpine}
ports:
- "${HOST_PORT:-8195}:80"
environment:
- APP_NAME=${APP_NAME:-MyApp}
- APP_ENV=${APP_ENV:-development}
- APP_VERSION=${APP_VERSION:-latest}
- LOG_LEVEL=${LOG_LEVEL:-info}
- MAX_CONNECTIONS=${MAX_CONNECTIONS:-100}
- ${CUSTOM_VAR:-SKIP_THIS}
cd examples/basics
# Start with all defaults
docker compose up -d
docker compose config
docker compose down
# Override specific values
APP_NAME=MyProductionApp APP_ENV=production docker compose up -d
docker compose exec web env | grep APP_
docker compose down
Example 2: Mandatory Variables¶
Directory: examples/mandatory/
Demonstrates :- for optional values and :? to enforce required variables. The config will refuse to start if mandatory variables are missing.
# examples/mandatory/docker-compose.yaml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER:?error}
POSTGRES_PASSWORD: ${DB_PASS:?password is required}
POSTGRES_DB: ${DB_NAME:-myapp}
api:
image: node:20-alpine
command: ["sh", "-c", "node -e \"console.log('DB_USER:', process.env.DB_USER)\""]
environment:
DB_USER: ${DB_USER:?}
DB_PASS: ${DB_PASS:?}
API_KEY: ${API_KEY:?API key is mandatory}
NODE_ENV: ${NODE_ENV:-development}
depends_on:
- db
cd examples/mandatory
# This will fail with a clear error:
docker compose config
# Provide mandatory variables:
DB_USER=admin DB_PASS=supersecret API_KEY=sk-abc-123 docker compose config
# Start the stack:
DB_USER=admin DB_PASS=supersecret API_KEY=sk-abc-123 docker compose up -d
docker compose logs api
docker compose down
Example 3: Advanced - All Techniques Combined¶
Directory: examples/advanced/
A comprehensive demo that showcases defaults, mandatory variables, nested references, $$ escaping, concatenation, and dynamic env_file paths.
# examples/advanced/docker-compose.yaml
services:
web:
image: nginx:${NGINX_TAG:-alpine}
ports:
- "${PORT:-8196}:80"
environment:
# Default value
APP_NAME: ${APP_NAME:-MyApp}
# Mandatory (fails if unset)
API_KEY: "${API_KEY:?API key required}"
# Empty string if unset
OPTIONAL_FEATURE: ${OPTIONAL_FEATURE:-}
# Literal dollar sign (escaping)
RAW_DOLLAR: $$not_a_variable
# Nested variable (indirect reference)
ENV_NAME: ${ENV_NAME:-${DEFAULT_ENV:-development}}
# Concatenation
DB_URL: "postgres://${DB_USER:-app}:${DB_PASS:-secret}@${DB_HOST:-db}:5432/${DB_NAME:-myapp}"
# Array from env var
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost}
# Config from external file with dynamic path
env_file:
- ${ENV_FILE:-./.env}
- ./secrets/env.${APP_ENV:-dev}
cd examples/advanced
# Explore the resolved configuration:
docker compose config
# Start with .env defaults:
docker compose up -d
docker compose exec web env | grep -E "APP_|DB_|RAW_|ENV_|API_"
docker compose down
# Switch to production secrets:
APP_ENV=prod docker compose up -d
docker compose exec web env | grep DB_
docker compose down
Example 4: Env-File Strategy¶
Directory: examples/env-file/
A production-ready multi-file environment pattern with shared defaults, environment-specific overrides, and isolated secrets.
examples/env-file/
├── .env # Project-level substitution defaults
├── docker-compose.yaml
├── config/
│ ├── common.env # Shared across all environments
│ ├── development.env # Dev overrides
│ └── production.env # Prod overrides
└── secrets/
└── credentials.env # Sensitive values (git-ignored)
# examples/env-file/docker-compose.yaml
services:
web:
image: nginx:${NGINX_TAG:-alpine}
ports:
- "${PORT:-8197}:80"
env_file:
- ./config/common.env
- ./config/${APP_ENV:-development}.env
- ./secrets/credentials.env
environment:
APP_NAME: ${APP_NAME:-FallbackApp}
cd examples/env-file
# See substitution defaults:
cat .env
# Start with development environment:
docker compose up -d
docker compose exec web env | sort
docker compose down
# Switch to production:
APP_ENV=production docker compose up -d
docker compose exec web env | sort
docker compose down
Best Practices¶
1. Always provide defaults with :-
# Good - works without any .env
image: nginx:${NGINX_TAG:-alpine}
# Bad - fails if NGINX_TAG is unset
image: nginx:${NGINX_TAG}
2. Use :? for secrets and required values
3. Keep a .env.example in version control
# .env.example - commit this, not the real .env
NGINX_TAG=alpine
APP_ENV=development
DB_USER=change_me
DB_PASS=change_me
Add the real .env to .gitignore:
4. Use layered env_file for complex environments
config/common.env ← Shared defaults
config/${APP_ENV}.env ← Environment-specific (dynamic path!)
secrets/credentials.env ← Sensitive values (last, so it can override)
5. Use docker compose config to debug substitution
# See the fully resolved compose file
docker compose config
# Check for missing variables without starting containers
docker compose config --quiet
6. Prefer :- over hardcoding in service definitions
7. Use $$ when embedding shell variables
# Correct - $$ becomes $ in the container
command: sh -c "echo \$$HOME"
# Wrong - ${HOME} would be substituted by Compose
command: sh -c "echo $HOME"
8. Keep substitution in environment: or env_file: for container runtime The .env file is for compose-file substitution, not runtime. Use environment: or env_file: to pass values into containers.
Troubleshooting¶
| Problem | Cause | Solution |
|---|---|---|
Compose file is invalid | Mandatory variable (:?) is unset | Set the variable via shell or .env |
WARNING: The VAR variable is not set. Defaulting to a blank string. | Variable used without :- default | Add :-default or set the variable |
VAR appears literally in output | Variable name misspelled or using $ instead of ${} | Use ${VAR} syntax |
| Container sees empty string | Variable set but empty in .env | Use :- default or set a non-empty value |
| Container sees wrong value | Precedence conflict | Check shell env > .env > env_file > environment order |
$ appearing literally inside container | Missing $$ escape | Use $$ for literal dollar signs |
env_file not found | Path is relative to compose file | Verify path exists; use ${PWD} if needed |
.env file ignored | Different project directory | Ensure .env is in the same directory where docker compose is run |
| Variable not available in container | Variable was only in .env (substitution) | Add to environment: or env_file: |
Hands-On Tasks¶
Task 1: Run the Basics Example¶
cd examples/basics
# Run with all defaults
docker compose up -d
docker compose exec web env | grep -E "APP_|LOG_|MAX_"
docker compose ps
docker compose down
# Override via shell
APP_NAME=CustomApp LOG_LEVEL=debug docker compose up -d
docker compose exec web env | grep APP_NAME
docker compose down
# Inspect the resolved config
docker compose config
Expected outcome: The nginx container starts with your chosen environment variables. When no shell variables are set, the :- defaults take effect.
Task 2: Enforce Mandatory Variables¶
cd examples/mandatory
# Try to start without required variables (will fail)
docker compose config 2>&1
# You should see an error about DB_USER, DB_PASS, API_KEY
# Provide ALL required variables
DB_USER=admin DB_PASS=secret123 API_KEY=sk-abc-123 \
docker compose config
# Start the full stack
DB_USER=admin DB_PASS=secret123 API_KEY=sk-abc-123 \
docker compose up -d
# Verify the api service received the values
docker compose logs api
# Clean up
docker compose down
Expected outcome: Without the required variables, docker compose config fails with a descriptive error. With them, the stack starts and the api container prints the correct DB_USER.
Task 3: Explore All Advanced Techniques¶
cd examples/advanced
# View the resolved configuration with .env defaults
docker compose config
# Start and inspect environment variables
docker compose up -d
docker compose exec web env | sort
docker compose down
# Verify $$ escaping
docker compose run --rm web env | grep RAW_DOLLAR
# Should output: RAW_DOLLAR=$not_a_variable
# Test nested variable resolution
DEFAULT_ENV=staging docker compose run --rm web env | grep ENV_NAME
# Should output: ENV_NAME=staging
# Test dynamic env_file with production secrets
APP_ENV=prod docker compose run --rm web env | grep DB_
# Should show prod DB credentials
Expected outcome: Each substitution technique works as documented. Nested variables cascade correctly. $$ produces a literal $ in the container. Dynamic paths load environment-specific files.
Task 4: Multi-File Environment Strategy¶
cd examples/env-file
# Examine the file structure
ls -la .env config/ secrets/
# Start in development mode (default)
docker compose up -d
# Inspect environment variables
docker compose exec web env | sort
# Should see: APP_NAME=MultiEnvApp (from common.env)
# LOG_LEVEL=debug (from development.env)
# DB_USER=admin (from credentials.env)
# Verify precedence: environment: overrides env_file
docker compose exec web env | grep APP_NAME
# Output: APP_NAME=MultiEnvApp (from common.env)
# The environment: section has ${APP_NAME:-FallbackApp}
# but APP_NAME is already set by common.env, so it uses MultiEnvApp
docker compose down
# Switch to production
APP_ENV=production docker compose up -d
docker compose exec web env | sort
# Should see: LOG_LEVEL=warn, DEBUG=false, HOT_RELOAD=false
docker compose down
Expected outcome: The env_file array loads files in order, with later files overriding earlier ones. The environment section has the final say. Switching APP_ENV changes which environment-specific config is loaded.
Task 5: Build Your Own Multi-Env Setup¶
# Create a new project directory
mkdir -p my-multi-env/config my-multi-env/secrets
# Create a shared config file
cat > my-multi-env/config/common.env << 'EOF'
APP_NAME=MyApp
CACHE_ENABLED=true
EOF
# Create environment-specific files
cat > my-multi-env/config/dev.env << 'EOF'
LOG_LEVEL=debug
DEBUG=true
EOF
cat > my-multi-env/config/prod.env << 'EOF'
LOG_LEVEL=warn
DEBUG=false
EOF
# Create a secrets file
cat > my-multi-env/secrets/credentials.env << 'EOF'
DB_PASS=changeme
EOF
# Create the compose file
cat > my-multi-env/docker-compose.yaml << 'EOF'
services:
app:
image: alpine
command: ["sh", "-c", "env | sort"]
environment:
DB_PASS: ${DB_PASS:?Database password is required}
env_file:
- ./config/common.env
- ./config/${APP_ENV:-dev}.env
- ./secrets/credentials.env
EOF
# Test both environments
cd my-multi-env
echo "=== Development ==="
APP_ENV=dev DB_PASS=devpass docker compose up --build
echo "=== Production ==="
APP_ENV=prod DB_PASS=prodpass docker compose up --build
# Clean up
cd ..
rm -rf my-multi-env
Expected outcome: Your custom setup selects the right config based on APP_ENV. The mandatory DB_PASS must be provided, and credentials are kept separate from config.
Verification Checklist¶
- I understand all five substitution syntax forms:
${VAR},:-,:?,:+,$$ - I know how the
.envfile is loaded and its role in substitution - I can use
env_filewith multiple files for layered configuration - I understand the precedence order: shell > environment > env_file > .env >
:- - I can enforce mandatory variables with
:?and custom error messages - I can nest variable references for cascading defaults
- I can concatenate multiple variables to build compound values
- I can use
$$to escape literal dollar signs - I can create environment-specific config files with dynamic
env_filepaths - I can debug variable resolution with
docker compose config - I know the difference between
.env(substitution) andenv_file(runtime) - I can build a production-ready multi-file environment configuration
Additional Resources¶
- Docker Compose Variable Substitution Docs
- Docker Compose File Reference
- Docker Compose
.envFile - Docker Compose Precedence Rules
- Docker Compose Best Practices
Next Steps¶
Now that you’ve mastered variable substitution, you’re ready to explore more advanced Docker Compose patterns:
| Lab | Description |
|---|---|
| 009 - Fragments | Reusable YAML fragments with anchors and extends |
| 010 - Profiles | Environment-aware service selection with profiles |
| 013 - Merge & Override | Multi-file compose merging strategies |
| 015 - Secrets & Configs | Secure secret management in Compose |
What to try next:
- Combine variable substitution with profiles to dynamically set profile-specific variables
- Use multi-file compose (
-f) to separate environment-specific overrides into separate files - Create a central
.envtemplate for your team with documented defaults - Integrate variable validation into your CI/CD pipeline with
docker compose config - Build a secret injection system using
env_filewith git-ignored credential files
