Environment Variables
Environment variables configure containers without rebuilding the image. ENV bakes a default into the image, -e sets one at run time, --env-file loads a file, and Compose / Kubernetes / cloud platforms layer their own overrides on top. Used well they keep secrets out of the image and configuration close to the platform.
ENV, ARG, env_file, secrets, layers
EXAMPLE
# 1) ENV in Dockerfile — defaults baked into the image
FROM node:20-alpine
ENV NODE_ENV=production
ENV PORT=3000
ENV LOG_LEVEL=info
CMD ['node', 'server.js']
# These show up in 'docker inspect' and persist in every container started from the image.
# Don't put SECRETS in ENV — anyone with the image can read them.
# 2) ARG — build-time only
FROM node:20-alpine
ARG BUILD_VERSION=dev
LABEL version=$BUILD_VERSION
ENV APP_VERSION=$BUILD_VERSION
CMD ['node', 'server.js']
# Build:
docker build --build-arg BUILD_VERSION=1.2.3 -t myapp:1.2.3 .
# ARG values are NOT available at run-time unless copied to ENV.
# ARG values DO end up in image history (docker history), so don't bake secrets there.
# 3) -e / --env at run time
docker run -e NODE_ENV=staging -e PORT=8080 myapp
docker run --env API_KEY=sk_test_... myapp
# 4) Pass variables already in your shell
docker run -e DATABASE_URL myapp # uses host's DATABASE_URL
# 5) Load from a file
# .env.local
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://app:pw@db:5432/app
API_KEY=sk_test_...
docker run --env-file .env.local myapp
# Caveat: --env-file's parsing is simpler than dotenv
# • No quotes; values are literal
# • No multi-line values
# • Empty values pass empty strings (not undefined)
# 6) Inspect what's in a running container
docker exec myapp env
docker inspect myapp --format '{{range .Config.Env}}{{.}}\\n{{end}}'
# 7) Compose — multiple ways
services:
web:
image: myapp:1.0
environment:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://app:pw@db:5432/app
env_file:
- .env.shared
- .env.web
ports: ['8080:3000']
# Later entries override earlier; explicit 'environment' overrides 'env_file'.
# Variable substitution from host env (or .env in compose project dir)
services:
web:
image: myapp:${TAG:-latest}
environment:
API_KEY: ${API_KEY} # required; errors if not set
LOG_LEVEL: ${LOG_LEVEL:-info} # default if unset
# 8) Docker Compose secrets — better than env for sensitive values
secrets:
api_key:
file: ./secrets/api_key.txt
db_password:
external: true # Docker Swarm secret
services:
web:
image: myapp:1.0
secrets: [api_key, db_password]
environment:
API_KEY_FILE: /run/secrets/api_key
DB_PASSWORD_FILE: /run/secrets/db_password
# App reads /run/secrets/* files at startup; never appears in ENV.
# 9) Kubernetes — best practice: separate ConfigMap + Secret
apiVersion: v1
kind: ConfigMap
metadata: { name: app-config }
data:
LOG_LEVEL: info
NODE_ENV: production
---
apiVersion: v1
kind: Secret
metadata: { name: app-secrets }
type: Opaque
stringData:
DATABASE_URL: 'postgresql://app:pw@db:5432/app'
API_KEY: 'sk_...'
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: web }
spec:
template:
spec:
containers:
- name: web
image: myapp:1.0
envFrom:
- configMapRef: { name: app-config }
- secretRef: { name: app-secrets }
# 10) Cloud platforms layer their own
# AWS ECS: Task definition environment + secretsManagerArn
# AWS Lambda: env vars + Secrets Manager / SSM Parameter Store
# Cloud Run: --set-env-vars + --set-secrets
# Fly.io: fly secrets set
# Heroku: heroku config:set
# Render: dashboard / env_groups
# 11) Reading env vars in the app — defensive defaults + validation
# Node
import { z } from 'zod';
const env = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.coerce.number().int().positive(),
DATABASE_URL: z.string().url(),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
}).parse(process.env);
# Fails fast at startup; clear error messages for missing/invalid values.
# 12) DON'T put these in env
# • TLS certs / private keys — mount as files
# • Multi-line config (Kubernetes yaml in env) — use ConfigMap files
# • Files > a few KB — env has limits (ARG_MAX)
# • Database connection POOLS — config code, not env
# 13) Env layering in Compose (order of precedence)
# Compose file 'environment' > env_file > .env > shell environment > Dockerfile ENV
# 14) Multi-stage builds + ARG/ENV
FROM node:20-alpine AS build
ARG SENTRY_AUTH_TOKEN
RUN npm ci && SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN npm run build
FROM node:20-alpine
COPY --from=build /app/dist /app
CMD ['node', '/app/server.js']
# The SENTRY_AUTH_TOKEN lives only in the BUILD stage; it never ships to the runtime image.
# Use BuildKit secrets for even tighter handling:
# RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# 15) Debugging
# • docker run -it --env-file .env myapp env # see what's actually loaded
# • Compose: docker compose config # resolved final config
# • k8s: kubectl describe pod / kubectl exec env
# 16) Common bugs
# • Quoting env values in --env-file → quotes become part of the value
# • Spaces around = in env file → leading/trailing space in value
# • Forgetting to escape $ in compose → variable substitution attempted
# • Building with --build-arg containing secrets → visible in image history
# • Setting NODE_ENV=production in dev → dev deps missing
# • Same env var in compose 'environment' AND 'env_file' → environment wins; surprising override
# • Running 'docker run -e API_KEY' (no value) → passes EMPTY string, not host's value (without --env-file)
# • Multiline secrets in env → use volume mounts instead
# • Bash export inheritance — only exported vars cross into docker run; check 'export'
Why it matters
Use ENV for defaults baked into the image, -e/--env-file at runtime, and Compose / Kubernetes / cloud platform mechanisms for environment-specific overrides. Keep secrets OUT of ENV — mount them as files (Docker secrets, Kubernetes Secret as volume) and validate every env var at startup with Zod or similar.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
docker run -e NODE_ENV=production -e DB_URL=... my-api docker run --env-file .env my-apiTry it Yourself »
Discussion
Loading…