Skip to main content

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

TypeExamplesWhere to StoreCan Commit?
ConfigurationApp port, log level, feature flags.env file, compose.yamlYes
SecretsDatabase password, API keys, tokensSecret manager, restricted .envNever

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

PracticeWhy
Never put secrets in compose.yamlThe file is committed to git
Use .env + .gitignore at minimumKeeps secrets out of version control
Use a secret manager for productionHashiCorp Vault, AWS Secrets Manager, etc.
Rotate credentials regularlyLimits exposure if leaked
Use different secrets per environmentA leak in dev should not compromise production

Key Takeaways

  • Configuration (non-sensitive) goes in .env or inline in compose. Secrets must never be committed.
  • Use ${VAR:?error} syntax to fail fast on missing required variables.
  • Always run docker compose config to validate variable substitution before deploying.
  • Commit .env.example (with placeholders) and add .env to .gitignore.
  • For production, use an external secret manager instead of local .env files.

What's Next