Environment Variables and Secrets
Every application needs configuration: database URLs, feature flags, API keys. Docker Compose gives you several ways to inject these values. The critical distinction is between configuration (safe to commit) and secrets (never commit).
Config vs Secrets
| Type | Examples | Where to Store | Can Commit? |
|---|---|---|---|
| Configuration | App port, log level, feature flags | .env file, compose.yaml | Yes |
| Secrets | Database password, API keys, tokens | Secret manager, restricted .env | Never |
Three Ways to Set Environment Variables
1. Inline in Compose
services:
api:
image: my-api:1.0.0
environment:
APP_ENV: production
LOG_LEVEL: info
DB_HOST: db
Good for non-sensitive defaults. Values are visible in the compose file.
2. From an .env File
Create a .env file alongside your compose file:
# .env
POSTGRES_PASSWORD=my-secret-password
API_TAG=1.2.3
APP_ENV=production
Reference variables using ${VAR} syntax:
services:
api:
image: registry.example.com/api:${API_TAG}
environment:
APP_ENV: ${APP_ENV}
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
3. Using env_file Directive
Load an entire file of variables into a service:
services:
api:
image: my-api:1.0.0
env_file:
- ./config/api.env
Variable Precedence
When the same variable is defined in multiple places, Compose uses this precedence (highest wins):
flowchart TD
A["1. Shell environment<br/>(export VAR=value)"] --> B["2. .env file"]
B --> C["3. env_file directive"]
C --> D["4. Inline environment:<br/>in compose.yaml"]
style A fill:#e8f5e9,stroke:#2e7d32
style D fill:#ffebee,stroke:#c62828
The shell environment always wins. Inline environment: in the compose file has the lowest priority.
The .env.example Pattern
Never commit real secrets. Instead, commit a template:
# .env.example -- Copy to .env and fill in real values
POSTGRES_PASSWORD=
API_KEY=
APP_ENV=development
LOG_LEVEL=debug
Add .env to .gitignore:
# .gitignore
.env
*.env
!.env.example
Required Variables
Use the ${VAR:?error message} syntax to fail fast if a required variable is missing:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD must be set}
If DB_PASSWORD is not defined, docker compose up will immediately fail with a clear error.
Validating Your Configuration
Always render the final configuration before deploying:
docker compose config
This shows the fully resolved YAML with all variables substituted -- catching missing or wrong values before they cause runtime failures.
Secrets Best Practices
| Practice | Why |
|---|---|
| Never put secrets in compose.yaml | The file is committed to git |
Use .env + .gitignore at minimum | Keeps secrets out of version control |
| Use a secret manager for production | HashiCorp Vault, AWS Secrets Manager, etc. |
| Rotate credentials regularly | Limits exposure if leaked |
| Use different secrets per environment | A leak in dev should not compromise production |
Key Takeaways
- Configuration (non-sensitive) goes in
.envor inline in compose. Secrets must never be committed. - Use
${VAR:?error}syntax to fail fast on missing required variables. - Always run
docker compose configto validate variable substitution before deploying. - Commit
.env.example(with placeholders) and add.envto.gitignore. - For production, use an external secret manager instead of local
.envfiles.
What's Next
- Continue to Dependency Order and Healthchecks to ensure services start in the right order.