Docker Environment Variables: Complete Guide with Examples (2026)
Environment variables are how you inject configuration, credentials, and feature flags into Docker containers without baking them into images. This guide covers every mechanism — from docker run -e to Kubernetes Secrets — with real examples and security pitfalls to avoid.
Why Environment Variables in Docker
The twelve-factor app methodology mandates separating configuration from code. In Docker, this means your image contains only application code — it has no knowledge of which database it connects to, which API key it uses, or which environment (staging vs production) it runs in. All of that comes in at runtime via environment variables.
This has three major benefits:
- One image, many environments: The same Docker image runs in development, staging, and production — just with different env vars injected at runtime.
- No secrets in images: Images are often pushed to registries. If credentials were baked in, anyone with registry access would have your production database password.
- Easy rotation: Rotating a credential means updating an env var and restarting a container — not rebuilding and redeploying an image.
Setting Env Vars with docker run -e
The simplest way to pass environment variables to a container is the -e (or --env) flag on docker run:
# Set a single variable
docker run -e NODE_ENV=production myapp
# Set multiple variables
docker run \
-e NODE_ENV=production \
-e PORT=3000 \
-e DATABASE_URL=postgres://user:pass@host:5432/db \
myapp
# Pass a variable from your current shell environment (no value = inherit)
export DATABASE_URL=postgres://localhost/mydb
docker run -e DATABASE_URL myapp # inherits value from host shell
# Verify what was set inside the container
docker run --rm -e FOO=bar alpine env | grep FOO
The inline approach works fine for one-off commands, but becomes unwieldy when you have 10+ variables. That is where --env-file comes in.
Using .env Files with docker run --env-file
The --env-file flag reads variables from a file, one per line, in KEY=VALUE format:
# .env file format
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://user:pass@host:5432/mydb
REDIS_URL=redis://cache:6379
JWT_SECRET=your-secret-here
LOG_LEVEL=info
# Lines starting with # are comments — they are ignored
# API_KEY=old-key
# Empty lines are also ignored
# Use with docker run
docker run --env-file .env myapp
# You can combine --env-file with -e (the -e flag overrides the file)
docker run --env-file .env.production -e LOG_LEVEL=debug myapp
Never commit
.envfiles containing real credentials to version control. Add.envto your.gitignore. Instead, commit a.env.examplefile with placeholder values that documents all required variables.
A good .env.example file looks like this:
# .env.example — commit this to git
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://user:password@localhost:5432/mydb_dev
REDIS_URL=redis://localhost:6379
JWT_SECRET=changeme-min-32-chars
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
STRIPE_SECRET_KEY=
LOG_LEVEL=debug
Docker Compose environment and env_file
Docker Compose offers two ways to inject environment variables into services: the environment key (inline) and env_file (from a file). They can be used together.
Inline environment key
# docker-compose.yml
services:
app:
image: myapp:latest
environment:
NODE_ENV: production
PORT: "3000"
# Pass a variable from the host shell (no value = inherit from host)
DATABASE_URL: ${DATABASE_URL}
# With a default fallback
LOG_LEVEL: ${LOG_LEVEL:-info}
worker:
image: myapp:latest
environment:
- NODE_ENV=production # list format also works
- WORKER_CONCURRENCY=4
env_file key
services:
app:
image: myapp:latest
env_file:
- .env # base vars
- .env.production # environment-specific overrides (applied last)
environment:
# Inline vars override env_file vars
LOG_LEVEL: debug
Compose's own .env file
Docker Compose automatically loads a .env file in the same directory as docker-compose.yml to substitute ${VARIABLE} references in the Compose file itself. This is different from the env_file key — it controls Compose variable substitution, not what gets injected into containers:
# .env (Compose variable substitution)
APP_VERSION=1.4.2
POSTGRES_VERSION=16
# docker-compose.yml — uses .env for substitution
services:
app:
image: myapp:${APP_VERSION} # substituted at compose parse time
db:
image: postgres:${POSTGRES_VERSION}
ARG vs ENV in Dockerfiles
This is the most misunderstood distinction in Docker. ARG and ENV are both ways to set variables in a Dockerfile, but they serve completely different purposes:
ARG— Build-time variable. Available only duringdocker build. Not present in the final image or running container. Used to parameterize the build process (e.g., which Node version to install, which package to download).ENV— Runtime variable. Set in the image layer and present in every container started from that image. Visible indocker inspectand to any process running inside the container.
FROM node:20-alpine
# ARG: only available during build
ARG BUILD_DATE
ARG GIT_COMMIT
ARG NODE_ENV=production # ARG with default value
# Use ARG to set a label (not exposed at runtime)
LABEL build.date=$BUILD_DATE
LABEL build.commit=$GIT_COMMIT
# ENV: available at runtime inside the container
ENV NODE_ENV=$NODE_ENV # copy ARG value to ENV if you need it at runtime
ENV PORT=3000
ENV APP_HOME=/app
WORKDIR $APP_HOME
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE $PORT
CMD ["node", "server.js"]
Pass ARG values at build time:
docker build \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \
--build-arg NODE_ENV=production \
-t myapp:1.0 .
Never use
ARGfor secrets. Build arguments are visible in the image history (docker history myimage) and can be extracted from any layer. If you pass a secret as anARG(e.g., an npm token to install private packages), use Docker BuildKit's--secretflag instead.
Build-Time Secrets with Docker BuildKit
The correct way to use secrets during a build (e.g., to install private npm packages or authenticate to a private registry) is Docker BuildKit's --secret flag. The secret is mounted as a file at build time and is never stored in any layer:
# Dockerfile — mount the secret as a file, use it, leave no trace
FROM node:20-alpine
WORKDIR /app
# Mount .npmrc as a secret — only available during this RUN command
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
COPY . .
RUN npm run build
# Build with the secret — it is passed via the host filesystem
DOCKER_BUILDKIT=1 docker build \
--secret id=npmrc,src=$HOME/.npmrc \
-t myapp .
The .npmrc file (containing your npm auth token) is never written to any image layer. Running docker history myapp will not reveal it.
Convert docker run to Docker Compose
Have a long docker run command with many -e flags? Paste it into our Docker to Compose converter and get a ready-to-use docker-compose.yml in seconds.
Multi-Stage Builds and ENV Scope
In multi-stage Dockerfiles, ENV instructions are scoped to the stage they are defined in. They do not automatically carry over to subsequent stages:
FROM node:20-alpine AS builder
# This ENV is only in the builder stage
ENV NODE_ENV=development
WORKDIR /app
COPY package*.json ./
RUN npm ci # installs devDependencies because NODE_ENV=development
COPY . .
RUN npm run build
FROM node:20-alpine AS production
# Fresh stage — NODE_ENV is NOT inherited from builder
ENV NODE_ENV=production
WORKDIR /app
# Copy only the built output, not the full source + devDeps
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production # only production deps
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
This is intentional and useful — the builder stage can install dev tools and test dependencies without leaking those into the final production image.
Overriding ENV at Runtime
Any ENV set in a Dockerfile is a default. It can always be overridden at runtime with -e or --env-file:
# Dockerfile sets LOG_LEVEL=info
ENV LOG_LEVEL=info
# Override at runtime for debugging
docker run -e LOG_LEVEL=debug myapp
# Same with Compose
services:
app:
image: myapp
environment:
LOG_LEVEL: debug # overrides Dockerfile default
Reading Environment Variables in Applications
Every major language has a standard way to read environment variables at startup. The pattern is identical: read from env, fail fast if a required variable is missing, apply defaults for optional ones.
Node.js
// config.js — centralize all env var access
const config = {
port: parseInt(process.env.PORT || '3000', 10),
nodeEnv: process.env.NODE_ENV || 'development',
databaseUrl: process.env.DATABASE_URL, // required — no default
redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
jwtSecret: process.env.JWT_SECRET, // required
logLevel: process.env.LOG_LEVEL || 'info',
};
// Validate required vars at startup — fail fast
const required = ['DATABASE_URL', 'JWT_SECRET'];
for (const key of required) {
if (!process.env[key]) {
console.error(`Missing required environment variable: ${key}`);
process.exit(1);
}
}
module.exports = config;
Python
import os
import sys
DATABASE_URL = os.environ.get('DATABASE_URL')
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'
PORT = int(os.environ.get('PORT', '8000'))
# Fail fast on missing required vars
required = {'DATABASE_URL': DATABASE_URL, 'SECRET_KEY': SECRET_KEY}
missing = [k for k, v in required.items() if not v]
if missing:
print(f"Missing required env vars: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)
Environment Variables in Docker Compose with Profiles
Compose profiles let you define different sets of services for different environments, each with their own env vars:
services:
app:
image: myapp
env_file: .env
profiles: [production, staging]
app-dev:
image: myapp
environment:
NODE_ENV: development
LOG_LEVEL: debug
volumes:
- .:/app # mount source for live reload
profiles: [development]
# Only start the mail catcher in development
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
profiles: [development]
# Start only development services
docker compose --profile development up
# Start production services
docker compose --profile production up
Kubernetes Integration: ConfigMaps and Secrets
When your containers graduate from Docker Compose to Kubernetes, environment variables come from ConfigMaps (non-sensitive config) and Secrets (credentials).
ConfigMap for non-sensitive vars
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
NODE_ENV: "production"
PORT: "3000"
LOG_LEVEL: "info"
REDIS_URL: "redis://redis-service:6379"
Secret for credentials
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
DATABASE_URL: "postgres://user:pass@postgres-service:5432/mydb"
JWT_SECRET: "your-jwt-secret"
STRIPE_SECRET_KEY: "sk_live_..."
Injecting into a Pod via envFrom
spec:
containers:
- name: app
image: myapp:1.0
# Inject all keys from ConfigMap as env vars
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets
# Or inject individual keys
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
In production Kubernetes clusters, never create Secrets with raw stringData committed to Git. Use external secret management: AWS Secrets Manager with External Secrets Operator, HashiCorp Vault, or Sealed Secrets to encrypt secrets before committing them to Git.
Security Best Practices
1. Never bake secrets into images
Anything set with ENV in a Dockerfile is part of the image and visible to anyone who can pull it. Use runtime injection (-e, --env-file, Kubernetes Secrets) for all credentials.
2. Use .dockerignore to prevent accidental inclusion
# .dockerignore
.env
.env.*
!.env.example
*.pem
*.key
.git
node_modules
3. Avoid logging environment variables
Startup scripts that print all env vars for debugging (printenv, env, or framework debug modes) will leak secrets to logs. Guard these with an environment check:
if (process.env.NODE_ENV !== 'production') {
console.log('Config:', { ...config, jwtSecret: '[REDACTED]' });
}
4. Use read-only secrets where possible
In Kubernetes, mount Secrets as volumes (files) instead of env vars for highly sensitive values. Files can be mounted read-only, and their contents are not exposed in kubectl describe pod output.
5. Rotate credentials without rebuilding
A primary advantage of runtime env var injection is that rotating a credential means updating the secret store and restarting pods — the image never changes. Build this rotation workflow into your runbooks from day one.
6. Validate env vars at application startup
A container that silently starts with a missing DATABASE_URL and crashes 30 seconds later on first request is harder to debug than one that exits immediately with a clear error message. Validate all required variables during the initialization phase.
Quick Reference: All the Ways to Set Env Vars
# 1. docker run — inline
docker run -e KEY=value myapp
# 2. docker run — from file
docker run --env-file .env myapp
# 3. docker run — inherit from host shell
docker run -e KEY myapp # KEY must be exported in host shell
# 4. Dockerfile — baked into image (no secrets!)
ENV KEY=value
# 5. Dockerfile — build arg copied to env
ARG BUILD_ENV=production
ENV NODE_ENV=$BUILD_ENV
# 6. docker build — build arg
docker build --build-arg BUILD_ENV=staging .
# 7. Docker Compose — inline
environment:
KEY: value
# 8. Docker Compose — from file
env_file:
- .env
# 9. Docker Compose — host variable substitution
environment:
KEY: ${HOST_KEY:-default}
# 10. Kubernetes — from ConfigMap
envFrom:
- configMapRef:
name: my-config
# 11. Kubernetes — from Secret
envFrom:
- secretRef:
name: my-secret
Debugging Environment Variable Issues
When a container behaves unexpectedly, these commands help verify what environment variables are actually set inside it:
# Print all env vars in a running container
docker exec mycontainer env
# Print a specific variable
docker exec mycontainer printenv DATABASE_URL
# See env vars set in the image (not runtime overrides)
docker inspect myimage:latest | jq '.[0].Config.Env'
# See env vars for a running container (includes runtime overrides)
docker inspect mycontainer | jq '.[0].Config.Env'
# Start a container just to check its env (exits immediately)
docker run --rm --env-file .env myapp env
Validate Your .env Files
Use our free ENV Validator to check your .env files for syntax errors, duplicate keys, and missing values before they cause container startup failures.
The Bottom Line
Environment variables are the standard interface between Docker containers and the infrastructure that runs them. Use -e and --env-file for local development, docker-compose.yml env_file for multi-service stacks, and Kubernetes Secrets/ConfigMaps in production. Keep ARG strictly for build-time parameterization, use ENV only for non-sensitive runtime defaults, and validate required variables at application startup to catch misconfigurations before they become runtime failures.
Related tools: Docker to Compose converter, ENV Validator, YAML Validator for Compose files, and 70+ more free developer tools.