Skip to content

Environment Variables and Secrets

Overview

Applications need configuration — database URLs, API keys, feature flags, log levels — that changes between environments without rebuilding the image. The Twelve-Factor App principle states: store config in the environment, not in code. Docker provides several mechanisms to inject configuration at runtime: environment variables, env files, secrets, bind mounts, and integration with external secret managers.

This tutorial explains when to use each approach, how to avoid leaking credentials into image layers and logs, and how to wire secrets through Docker Compose and production patterns. You will configure a sample web application with non-sensitive env vars and sensitive credentials using Docker's recommended secret primitives.

This is Tutorial 12 in Module 4: Networking & Registry of the REBASH Academy Docker series. Complete Container Registries and Distribution before this tutorial.

Prerequisites

Learning Objectives

By the end of this tutorial, you will be able to:

  • Inject configuration with -e, --env-file, and Dockerfile ENV vs runtime overrides
  • Distinguish configuration from secrets and apply least-exposure patterns
  • Mount secrets with Docker Swarm secrets and Compose secrets blocks
  • Avoid baking credentials into images, layers, and container metadata
  • Integrate external secret managers conceptually (Vault, AWS Secrets Manager, GCP Secret Manager)
  • Debug missing or incorrect environment variables in running containers

Architecture

Configuration flows from sources of truth into the container process environment or mounted files — never through rebuilt image layers for secrets.

Compose and configuration

Theory

Configuration vs Secrets

Type Examples Storage approach
Configuration LOG_LEVEL, PORT, FEATURE_X_ENABLED Environment variables, ConfigMaps (K8s)
Secrets DB passwords, API tokens, TLS private keys Secret mounts, secret managers — not plain env in prod
Build-time args NODE_ENV for compile, version stamps Dockerfile ARG — visible in image history

Rule: If losing the value would require rotation (revoking a key), treat it as a secret.

Dockerfile ENV vs Runtime Environment

Dockerfile ENV instructions bake default values into the image. They appear in docker inspect and image history. Use ENV for harmless defaults (PORT=8080). Override at runtime with docker run -e PORT=9090.

Dockerfile ARG values exist only at build time. Never pass secrets as build args — they persist in layer metadata and build cache.

# Good: non-sensitive default
ENV APP_PORT=8080

# Bad: never do this for secrets
# ARG DB_PASSWORD=supersecret

docker run Environment Injection

Three common methods:

  1. Inline: docker run -e DATABASE_HOST=db.internal app
  2. Pass-through: docker run -e DATABASE_HOST app (reads from host shell)
  3. Env file: docker run --env-file ./app.env app

Env files use KEY=VALUE lines. Docker Compose automatically loads .env from the project directory for variable substitution in the Compose file — a separate concern from container environment.

Docker Compose Environment Blocks

Compose supports multiple env patterns in docker-compose.yml:

services:
  web:
    image: myapp:1.0
    environment:
      LOG_LEVEL: info
      DATABASE_HOST: db
    env_file:
      - ./config/web.env

Order of precedence (highest wins): shell environment → environment section → env_file → Dockerfile ENV.

Compose also supports variable substitution with ${VAR} and defaults ${VAR:-default} in the YAML file itself — useful for port mappings and image tags, not for secrets committed to Git.

Docker Secrets (Swarm Mode)

Docker secrets are designed for sensitive data in Swarm mode. Secrets are:

  • Encrypted at rest in the Swarm Raft log
  • Mounted as files in /run/secrets/<secret_name> inside containers
  • Never stored in container env vars or image layers
  • Rotated by creating a new secret and updating the service

Standalone docker run does not support Swarm secrets natively. Compose v3+ secrets top-level block works with docker stack deploy (Swarm), not plain docker compose up on a single node without Swarm — an important distinction.

For local Compose without Swarm, common patterns:

  • Bind-mount a read-only secret file from a path outside Git
  • Use external secret sync tools before compose up
  • Enable Swarm on a single node for lab: docker swarm init

Bind Mounts for Secret Files

Mount credentials from the host without putting them in the image:

docker run -v /secure/path/db-password.txt:/run/secrets/db-password:ro myapp

The application reads the file at startup. Restrict host file permissions (chmod 600). This pattern works everywhere but places burden on host filesystem security.

External Secret Managers

Production platforms rarely rely on flat files alone:

Platform Typical integration
HashiCorp Vault Sidecar or init container fetches secrets; entrypoint exports or writes files
AWS Secrets Manager ECS task secrets block injects into env or files via task execution role
GCP Secret Manager Cloud Run and GKE mount secrets as env or volumes
Azure Key Vault ACI and AKS integrate via managed identities

Docker containers are ephemeral — secret managers provide rotation, audit logs, and centralized access control. The container receives short-lived values at start, not permanent copies in Git.

Twelve-Factor and Container Config

The Twelve-Factor config factor aligns with containers:

  • Strict separation of config from code
  • One codebase, many deploys with different env
  • Never commit .env with secrets to Git — use .env.example with placeholder keys

Add .env to .gitignore. Scan repos with secret detection tools in CI.

Hands-on Lab

Create a workspace for this tutorial.

mkdir -p ~/rebash-docker/environment-variables-and-secrets && cd ~/rebash-docker/environment-variables-and-secrets

Focus: pass env files safely and avoid baking secrets into images

Step 1 – Env file runtime inject

cat > app.env << 'EOF'
APP_MODE=lab
GREETING=hello
EOF
cat > Dockerfile << 'EOF'
FROM alpine:3.20
CMD ["sh", "-c", "echo mode=$APP_MODE greeting=$GREETING"]
EOF
docker build -t rebash-env:lab .
docker run --rm --env-file app.env rebash-env:lab
docker image inspect rebash-env:lab --format '{{json .Config.Env}}'

Step 2 – Cleanup

rm -f app.env
docker rmi rebash-env:lab

Final step – Cleanup note

rm -f app.env
docker rmi rebash-env:lab 2>/dev/null || true
# Keep ~/rebash-docker/ for later tutorials

Validation

Confirm the lab before moving on:

  1. Re-run the critical commands from the Hands-on Lab and compare them to the expected output in each step.
  2. Check that you can explain why each successful result matters (not only that it printed).
  3. Note any warnings or unexpected output — resolve them using Troubleshooting before continuing.
Check Pass criteria
Env injection Container sees expected non-secret configuration via env
Secret handling Lab secret is supplied without committing real credentials to Git
Inspect awareness You can show where env values appear in docker inspect
Cleanup Containers and any local .env lab files handled safely

Code Walkthrough

Command / directive Description Example
docker run -e Set environment variable docker run -e PORT=8080 app
docker run --env-file Load variables from file docker run --env-file prod.env app
ENV (Dockerfile) Default image environment ENV NODE_ENV=production
ARG (Dockerfile) Build-time variable only ARG VERSION=1.0
Compose environment Service env in YAML Key-value map under service
Compose env_file External env file per service List of file paths
docker secret create Create Swarm secret docker secret create db_pass ./pass.txt

Safe entrypoint pattern for secret files

Applications should read secrets from files when available, falling back only in development:

#!/bin/sh
set -eu
if [ -f /run/secrets/database_url ]; then
  export DATABASE_URL="$(cat /run/secrets/database_url)"
fi
exec python server.py

Use set -eu without -o pipefail for maximum POSIX shell compatibility in minimal images.

.env.example for teams

Commit a template without real values:

# Copy to .env for local development — never commit .env
LOG_LEVEL=info
APP_PORT=8080
DATABASE_HOST=localhost
# Secrets: mount at /run/secrets/api_token instead of env vars

Security Considerations

  • Never commit .env files containing real credentials; provide .env.example with placeholders only
  • Prefer Docker secrets / external secret managers over plain environment: for production
  • Remember environment variables are visible via docker inspect to anyone who can talk to the daemon
  • Rotate lab credentials after demos; treat shared lab passwords as compromised
  • Avoid passing secrets on the CLI (docker run -e PASS=…) where shell history retains them
  • Scrub CI logs — echo and debug printing commonly leak secrets from env blocks

Common Mistakes

Passing secrets via -e or ENV

Environment variables appear in docker inspect, process listings (ps e), and crash dumps. Use file-based secrets for credentials in production.

Committing .env files to Git

One accidental push exposes all keys. Use .env.example, .gitignore, and pre-commit secret scanning.

Using ARG for credentials at build time

Build args are stored in image history. Use runtime injection or multi-stage builds that do not copy secret stages.

Assuming Compose secrets work with docker compose up

Top-level secrets in Compose require Swarm (docker stack deploy) unless you bind-mount files manually on standalone Docker.

Best Practices

Separate config repos from secret delivery

Version Compose and Dockerfiles in Git; deliver secrets from vaults or cloud secret managers at deploy time.

Use read-only mounts for secret files

Append :ro to volume mounts so compromised containers cannot overwrite credential files.

Rotate secrets without rebuilding images

File-based secrets allow rotation by updating the secret source and restarting the container — image digest unchanged.

Validate required variables at startup

Fail fast with clear errors when DATABASE_URL or secret files are missing — avoids silent misconfiguration in production.

Troubleshooting

Issue Cause Solution
App sees empty env var Typo in key name or wrong precedence Check docker inspect Env section; verify Compose override order
--env-file ignored Wrong path or CRLF line endings Use absolute path; convert Windows line endings
Secret file not found in container Mount path mismatch Align app path with -v target; check :ro mount
Secret visible in logs App logs full environment Log only non-sensitive keys; read secrets from files
Compose variable substitution empty .env missing for YAML Add .env for ${VAR} in compose file or export in shell
Permission denied on secret mount Host file permissions too open chmod 600 on host; match container user if needed

Summary

  • Configuration belongs in the environment; secrets need stronger protection than plain env vars
  • Use Dockerfile ENV for safe defaults; override at runtime with -e and --env-file
  • Docker secrets (Swarm) and read-only bind mounts deliver credentials as files under /run/secrets/
  • Never bake secrets into images, build args, or Git-tracked .env files
  • Production integrates external secret managers for rotation, audit, and least-privilege access
  • Compose merges multiple env sources — understand precedence to avoid surprises

Interview Questions

  1. Why are ENV instructions in Dockerfiles risky for secrets?
  2. Runtime --env-file versus build-time ARG?
  3. How can secrets still leak via docker inspect?
  4. Better secret patterns for Swarm/Kubernetes?
  5. How do you rotate a secret used by containers?

Sample answer — question 2

Inspect the image config and container env to see whether a secret was baked in.

Sample answer — question 4

Use secret managers / orchestrator secret objects and short rotation intervals.

References