Environment Variables
Environment variables are the standard way to configure containers. They let you change application behavior -- database URLs, API keys, feature flags -- without rebuilding the image. This lesson covers every way to set them and the security practices you need to follow.
Why Environment Variables?
flowchart LR
A["Same Image"] --> B["Dev Config\nDB_HOST=localhost"]
A --> C["Staging Config\nDB_HOST=staging-db"]
A --> D["Prod Config\nDB_HOST=prod-db.internal"]
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#e8f5e9,stroke:#2e7d32
style C fill:#fff3e0,stroke:#ef6c00
style D fill:#ffebee,stroke:#c62828
One image, many environments. Configuration lives outside the image, injected at runtime.
Setting Variables with -e
The -e flag sets a single environment variable:
docker run -d \
--name api \
-e NODE_ENV=production \
-e DB_HOST=db.internal \
-e DB_PORT=5432 \
my-api:1.0.0
Passing Host Variables
If you omit the value, Docker passes the variable from your host shell:
export API_KEY=abc123
docker run -d -e API_KEY my-api:1.0.0
# Container receives API_KEY=abc123
Using Env Files (--env-file)
When you have many variables, use an env file to keep commands clean:
docker run -d --env-file .env my-api:1.0.0
Env File Format
# Database configuration
DB_HOST=db.internal
DB_PORT=5432
DB_NAME=myapp
DB_USER=appuser
DB_PASSWORD=s3cret
# Application settings
NODE_ENV=production
LOG_LEVEL=info
PORT=8080
Rules:
- One
KEY=VALUEper line - Lines starting with
#are comments - No quotes needed around values (quotes are included literally)
- No spaces around
= - Empty lines are ignored
In env files, quotes are not stripped. DB_HOST="localhost" sets the value to "localhost" (with quotes). Write DB_HOST=localhost instead.
Multiple Env Files
You can use multiple --env-file flags. Later files override earlier ones:
docker run -d \
--env-file .env.defaults \
--env-file .env.production \
my-api:1.0.0
Environment Variables in Docker Compose
Compose offers two directives: environment (inline) and env_file (from file).
Inline with environment
services:
api:
image: my-api:1.0.0
environment:
NODE_ENV: production
DB_HOST: db
DB_PORT: "5432"
LOG_LEVEL: info
From File with env_file
services:
api:
image: my-api:1.0.0
env_file:
- .env.defaults
- .env.production
Combining Both
services:
api:
image: my-api:1.0.0
env_file:
- .env.defaults
environment:
# Overrides take precedence over env_file
NODE_ENV: production
LOG_LEVEL: debug
Precedence order (highest wins):
environmentdirective (inline)- Shell environment variables
env_filedirectiveENVin Dockerfile (build-time default)
Variable Substitution in Compose
Compose can interpolate host environment variables using ${VAR} syntax:
services:
api:
image: my-api:${APP_VERSION:-latest}
environment:
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD must be set}
| Syntax | Behavior |
|---|---|
${VAR} | Use value of VAR, empty string if unset |
${VAR:-default} | Use default if VAR is unset or empty |
${VAR-default} | Use default only if VAR is unset |
${VAR:?error} | Fail with error message if VAR is unset or empty |
Viewing Container Variables
# List all environment variables in a running container
docker exec my-container env
# Check a specific variable
docker exec my-container printenv DB_HOST
# Using inspect (shows variables set at creation)
docker inspect -f '{{json .Config.Env}}' my-container | python3 -m json.tool
Build-Time vs Runtime Variables
| Type | Set With | Available At | Persists in Image? |
|---|---|---|---|
ARG | --build-arg | Build time only | ❌ No |
ENV | -e, --env-file | Build + Runtime | ✅ Yes (as default) |
# ARG: only available during build
ARG APP_VERSION=1.0.0
# ENV: baked into image as default, overridable at runtime
ENV NODE_ENV=production
ENV APP_VERSION=${APP_VERSION}
# Override ENV at runtime
docker run -e NODE_ENV=development my-api:1.0.0
Security Best Practices
Do
| Practice | Why |
|---|---|
Use --env-file for secrets | Keeps secrets out of shell history and docker inspect |
Add .env to .gitignore | Prevents secrets from being committed |
| Use Docker secrets (Swarm) | Secrets mounted as files, not visible in env |
| Rotate secrets regularly | Limits blast radius of a leak |
Don't
| Anti-Pattern | Risk |
|---|---|
docker run -e PASSWORD=secret | Visible in docker inspect and shell history |
Hardcode secrets in Dockerfile ENV | Baked into every image layer permanently |
Commit .env files to git | Secrets exposed in repository history forever |
| Log environment variables at startup | Secrets appear in log aggregation systems |
Never put secrets in ENV instructions in your Dockerfile. They are baked into the image layers and visible to anyone who can pull the image. Use runtime injection with --env-file or Docker secrets instead.
Common Patterns
Database Connection
docker run -d \
--name api \
-e DB_HOST=db.internal \
-e DB_PORT=5432 \
-e DB_NAME=myapp \
-e DB_USER=appuser \
-e DB_PASSWORD_FILE=/run/secrets/db_password \
my-api:1.0.0
Feature Flags
docker run -d \
-e ENABLE_CACHE=true \
-e ENABLE_METRICS=true \
-e LOG_LEVEL=warn \
my-api:1.0.0
Multi-Environment Setup
LOG_LEVEL=info
PORT=8080
CACHE_TTL=300
DB_HOST=prod-db.internal
DB_PASSWORD=<injected-by-CI>
LOG_LEVEL=warn
Key Takeaways
- Environment variables are the standard way to configure containers without rebuilding images.
- Use
--env-fileinstead of-efor secrets to keep them out of shell history. - In Compose,
environment(inline) overridesenv_filevalues. - Never bake secrets into Dockerfile
ENVinstructions -- they persist in image layers. - Add
.envfiles to.gitignoreand rotate secrets regularly.
What's Next
- Continue to Container Resource Management to learn how to monitor and control resource usage.