iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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-api
Try it Yourself »

Discussion

Loading…