Skip to main content

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

.env
# 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=VALUE per line
  • Lines starting with # are comments
  • No quotes needed around values (quotes are included literally)
  • No spaces around =
  • Empty lines are ignored
Quoting Gotcha

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):

  1. environment directive (inline)
  2. Shell environment variables
  3. env_file directive
  4. ENV in 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}
SyntaxBehavior
${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

TypeSet WithAvailable AtPersists in Image?
ARG--build-argBuild time only❌ No
ENV-e, --env-fileBuild + 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

PracticeWhy
Use --env-file for secretsKeeps secrets out of shell history and docker inspect
Add .env to .gitignorePrevents secrets from being committed
Use Docker secrets (Swarm)Secrets mounted as files, not visible in env
Rotate secrets regularlyLimits blast radius of a leak

Don't

Anti-PatternRisk
docker run -e PASSWORD=secretVisible in docker inspect and shell history
Hardcode secrets in Dockerfile ENVBaked into every image layer permanently
Commit .env files to gitSecrets exposed in repository history forever
Log environment variables at startupSecrets appear in log aggregation systems
danger

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

.env.defaults
LOG_LEVEL=info
PORT=8080
CACHE_TTL=300
.env.production
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-file instead of -e for secrets to keep them out of shell history.
  • In Compose, environment (inline) overrides env_file values.
  • Never bake secrets into Dockerfile ENV instructions -- they persist in image layers.
  • Add .env files to .gitignore and rotate secrets regularly.

What's Next