Environment Variables
Environment variables are the primary way to configure containerized applications. They allow you to change behavior without modifying images or code.
Why Environment Variables?
Environment variables provide:
- Configuration without code changes: Same image, different configs
- Secret management: Keep credentials out of images
- Environment-specific settings: Different values for dev/staging/prod
- 12-Factor App compliance: External configuration
Setting Environment Variables
Using -e Flag
# Single variable
docker run -e NODE_ENV=production myapp
# Multiple variables
docker run \
-e NODE_ENV=production \
-e PORT=3000 \
-e LOG_LEVEL=info \
myapp
# Pass from host environment
export API_KEY=secret123
docker run -e API_KEY myapp # Uses host's API_KEY value
Using --env-file
# Load from file
docker run --env-file .env myapp
# Multiple env files
docker run --env-file .env --env-file .env.local myapp
Example .env file:
# .env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=your-api-key
LOG_LEVEL=info
Environment Variables in Dockerfile
ENV Instruction
FROM node:18
# Set environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Multiple in one instruction
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=info
# Use in subsequent instructions
ENV APP_DIR=/app
WORKDIR $APP_DIR
Build-time vs Runtime
# ARG: Build-time only (not in final image)
ARG VERSION=1.0.0
RUN echo "Building version $VERSION"
# ENV: Available at both build and runtime
ENV APP_VERSION=$VERSION
Docker Compose Environment
Inline Environment
services:
api:
image: myapi
environment:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://db:5432/myapp
Environment File
services:
api:
image: myapi
env_file:
- .env
- .env.local
Variable Substitution
# docker-compose.yml
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME:-myapp} # Default value
# .env file (Compose reads automatically)
DB_PASSWORD=secret
DB_NAME=production_db
Interpolation Syntax
services:
api:
environment:
# Required variable (error if not set)
API_KEY: ${API_KEY}
# Default value if not set
PORT: ${PORT:-3000}
# Default if empty or not set
LOG_LEVEL: ${LOG_LEVEL-info}
# Error with message if not set
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY must be set}
Common Configuration Patterns
Database Connection
docker run \
-e DATABASE_URL=postgresql://user:password@host:5432/dbname \
myapp
# Or separate variables
docker run \
-e DB_HOST=localhost \
-e DB_PORT=5432 \
-e DB_USER=myuser \
-e DB_PASSWORD=secret \
-e DB_NAME=mydb \
myapp
Service URLs
services:
frontend:
environment:
API_URL: http://api:3000
CDN_URL: https://cdn.example.com
api:
environment:
REDIS_URL: redis://redis:6379
DATABASE_URL: postgresql://postgres:secret@db:5432/app
Feature Flags
services:
api:
environment:
FEATURE_NEW_UI: "true"
FEATURE_ANALYTICS: "false"
FEATURE_BETA_ENDPOINTS: ${ENABLE_BETA:-false}
Logging Configuration
services:
api:
environment:
LOG_LEVEL: ${LOG_LEVEL:-info}
LOG_FORMAT: json
LOG_DESTINATION: stdout
Best Practices
1. Don't Hardcode Secrets in Images
# BAD - secret in image
ENV API_KEY=abc123
# GOOD - set at runtime
# (No ENV for secrets in Dockerfile)
# Set at runtime
docker run -e API_KEY=$API_KEY myapp
2. Use Meaningful Defaults
ENV NODE_ENV=production
ENV PORT=3000
ENV LOG_LEVEL=info
3. Document Required Variables
# Required environment variables:
# - DATABASE_URL: PostgreSQL connection string
# - API_KEY: External API authentication key
# - SECRET_KEY: Application secret for signing
FROM node:18
# ...
4. Validate on Startup
// In application code
const required = ['DATABASE_URL', 'API_KEY', 'SECRET_KEY'];
for (const key of required) {
if (!process.env[key]) {
console.error(`Missing required env var: ${key}`);
process.exit(1);
}
}
5. Separate Configs by Environment
.env # Shared defaults
.env.development # Development overrides
.env.production # Production overrides
.env.local # Local overrides (not committed)
# docker-compose.yml
services:
api:
env_file:
- .env
- .env.${ENV:-development}
Viewing Environment Variables
From Outside Container
# Show config
docker inspect -f '{{json .Config.Env}}' container_name | jq
# With docker compose
docker compose config
From Inside Container
# View all
docker exec container_name env
# View specific
docker exec container_name printenv DATABASE_URL
Environment Variable Precedence
Order of precedence (highest to lowest):
-eflag on command lineenvironmentin compose fileenv_filein compose file.envfile (for Compose variable substitution)ENVin Dockerfile- Base image defaults
# Example: LOG_LEVEL will be "debug"
services:
api:
env_file:
- .env # LOG_LEVEL=info
environment:
LOG_LEVEL: debug # This wins
Security Considerations
Don't Log Secrets
// BAD
console.log('Config:', process.env);
// GOOD
const safeConfig = {
port: process.env.PORT,
nodeEnv: process.env.NODE_ENV,
// Don't log API_KEY, DATABASE_URL, etc.
};
console.log('Config:', safeConfig);
Gitignore Sensitive Files
# .gitignore
.env.local
.env.*.local
.env.production
*.pem
*.key
Use Docker Secrets for Production
# For Docker Swarm or sensitive data
services:
api:
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Key Takeaways
- Use
-efor single variables,--env-filefor multiple - ENV in Dockerfile sets defaults, override at runtime
- Compose supports variable substitution with
${VAR} - Never hardcode secrets in Dockerfiles or images
- Validate required variables on application startup
- Use separate env files for different environments
- Consider Docker secrets for production secrets

