Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Set Environment Variables

Learn how to set environment variables in Docker containers using ENV in Dockerfiles, --env with docker run, and environment blocks in Compose. Hands-on examples and edge cases included.

Focus: set environment variables in containers

Sponsored

You've built images, run containers, and mapped ports — but there's a lingering problem: how do you configure your app without hardcoding secrets, API keys, or environment-specific values inside the image? If you bake credentials or config into the image itself, every container from that image shares the same settings, which is a security and flexibility nightmare. Environment variables solve this cleanly: they let you inject configuration at runtime, keeping your image portable and your secrets safe.

The problem this lesson solves

When you write FROM python:3.11-slim and COPY . /app, you're committing to static files. But real apps need different config in dev, staging, and production — database URLs, API tokens, debug flags. The naive approach (hardcoding values in source or Dockerfiles) forces you to rebuild the image for every environment. Worse, it leaks secrets into your image layers. The lesson's core pain: how to pass dynamic, often sensitive, values into a running container without embedding them in the image. Environment variables are the industry-standard answer.

Core concept / mental model

Think of a Docker container as a mini operating system running your app. Environment variables (env vars) are like post-it notes the host sticks on the container's front door before the app starts. The app can read them at runtime via os.environ['DB_URL'] (Python), process.env.API_KEY (Node.js), or getenv('MODE') (C). These variables live only inside the container's session — they're not baked into the image filesystem.

Key definitions: - ENV (Dockerfile): Sets a default value that's built into the image. Overridable at runtime. - --env / -e (docker run): Passes a variable at container start. Can override ENV defaults. - --env-file: Loads many variables from a text file — secure because the file often stays out of version control. - environment: (Compose): Declares env vars in docker-compose.yml for multi-service apps.

Pro tip: Treat ENV in Dockerfiles as fallback defaults. Use --env in CI/CD or Compose blocks for environment-specific values. Never hardcode production secrets in ENV.

How it works step by step

Here's the cause-and-effect sequence:

  1. Dockerfile build phase: ENV MY_VAR=default writes a key-value pair into the image's metadata. Every container from that image starts with MY_VAR=default unless overridden.
  2. Container start (docker run): You supply -e MY_VAR=production or --env-file=./prod.env. Docker merges the new values on top of any ENV defaults. Later values override earlier ones.
  3. Inside the container: The app's runtime (Python, Node, etc.) reads the environment block. Env vars are just strings — the app parses them as needed.
  4. Container stops: Env vars disappear with the container. No trace on the host filesystem.

Order of precedence (highest wins): 1. -e / --env / --env-file at runtime 2. ENV in Dockerfile 3. Image default (if nothing else)

Hands-on walkthrough

Let's build a minimal Python app that reads environment variables.

Step 1: Project structure

mkdir docker-env-demo && cd docker-env-demo
echo 'DB_URL=postgres://dev:pass@localhost:5432/mydb' > dev.env

Step 2: Create a Dockerfile with ENV defaults

FROM python:3.11-slim
WORKDIR /app
COPY app.py .
# Default ENV values (used if nothing else is supplied)
ENV MODE=development \
    DB_URL=sqlite:///local.db
CMD ["python", "app.py"]

Step 3: Write app.py

import os

mode = os.environ.get('MODE', 'unknown')
db_url = os.environ['DB_URL']
print(f"Application running in {mode} mode")
print(f"Connecting to database at: {db_url}")

Step 4: Build and test

docker build -t env-demo .

# Run with defaults (from ENV in Dockerfile)
docker run --rm env-demo
# Output:
# Application running in development mode
# Connecting to database at: sqlite:///local.db

# Run with runtime override
docker run --rm -e MODE=production -e DB_URL=postgres://prod:secret@prod:5432/proddb env-demo
# Output:
# Application running in production mode
# Connecting to database at: postgres://prod:secret@prod:5432/proddb

Step 5: Use an env-file

docker run --rm --env-file dev.env env-demo
# Output:
# Application running in unknown mode
# Connecting to database at: postgres://dev:pass@localhost:5432/mydb

Pro tip: --env-file does not require you to list every variable — only the ones you want to override. Missing variables will use ENV defaults. The file must be single-line KEY=VALUE pairs (no export).

Compare options / when to choose what

Method Scope Security Best for
ENV in Dockerfile Image metadata Low (visible in image layers) Default config, non-sensitive defaults
-e / --env Single container Medium (in CLI history) Quick dev/test overrides
--env-file Single container High (file excluded from build) Production deployments, CI/CD
environment: in Compose Multi-container set Medium (in YAML, could be committed) Dev environments, small polyglot setups

When to choose what: - Use ENV only for non-secret defaults (e.g., LANG=C.UTF-8, PYTHONUNBUFFERED=1). - Use --env-file for secrets in production (keep the file in a vault or secret store, not in Git). - Use environment: in Compose for dev composability — but consider a .env file referenced by Compose for cleaner separation.

Troubleshooting & edge cases

  • Variable not found: os.environ['VAR'] raises KeyError if VAR isn't defined. Always use os.environ.get('VAR', fallback) — or let the app crash with a clear error message.
  • Spaces or quotes in --env-file: Values with spaces do not need quotes. Write MY_VAR=hello world (not MY_VAR="hello world" — that includes the quotes).
  • Override order: If you pass both -e and --env-file, the last one on the command line wins. Same with multiple --env-file flags.
  • Compilation-level secrets: For build-time secrets (e.g., private package sources), never use ENV. Use --build-arg in docker build instead — but still avoid baking them into the final image.
  • Injection attacks: If an attacker controls an env var value (e.g., DB_URL), they could alter app behavior. Validate and sanitize sensitive inputs in your app's startup.

What you learned & what's next

You now understand how to set environment variables in containers using three distinct mechanisms: ENV for image defaults, --env and --env-file for runtime overrides, and Compose environment: blocks for multi-service apps. You can apply the order of precedence, avoid common pitfalls like missing links or quoting, and choose the right method based on security needs and deployment context.

Next, you'll learn how to manage persistent data with volumes — because env vars configure the app, but volumes keep your data safe across container restarts.

Key takeaway: Environment variables are runtime configuration, not build-time secrets. Keep images portable and secrets outside the container.

Common mistakes

  • Forgetting that --env-file expects plain KEY=VALUE lines without export — using export KEY=value will pass the literal string export KEY=value as value.
  • Hardcoding production secrets in ENV Dockerfile instructions — this bakes secrets into image layers that anyone with docker history can see.
  • Using -e for every variable instead of --env-file — this clutters CLI history and the container run command, defeating security.
  • Assuming environment: in Compose overrides all Dockerfile ENV values — it only overrides variables that appear in the environment block.

Variations

  1. Use .dockerignore with --env-file to avoid committing sensitive files, but note the env-file path is resolved at runtime, not build time.
  2. Combine env vars with Docker secrets for Swarm/Compose production environments — Docker secrets mount files at /run/secrets/ rather than pass env vars.
  3. Leverage environment variable expansion in Compose with ${VAR:-default} syntax for conditional defaults in docker-compose.yml.

Real-world use cases

  • A Python web app reads MONGO_URI from an env var set via --env-file in CI, swapping between dev, staging, and production databases without rebuilding the image.
  • A Node.js microservice uses NODE_ENV=production and API_KEY passed by Docker Compose environment: block, keeping per-service secrets out of version control.
  • A Java application reads JAVA_OPTS from --env to dynamically set JVM flags (heap, GC) per host, optimizing for low-memory or high-throughput deployments.

Key takeaways

  • Environment variables separate configuration from code — reduce rebuilds and improve security.
  • ENV in Dockerfile sets default values, overridable at runtime by --env or --env-file.
  • --env-file is the safest method for secrets because the file stays outside the build context.
  • In Compose, use environment: for dev overrides and reference a .env file for shared defaults.
  • Always use os.environ.get('KEY', default) in your code to avoid crashes from missing variables.
  • Never hardcode production secrets in Dockerfiles — use runtime secrets management instead.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.