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
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
ENVin Dockerfiles as fallback defaults. Use--envin CI/CD or Compose blocks for environment-specific values. Never hardcode production secrets inENV.
How it works step by step
Here's the cause-and-effect sequence:
- Dockerfile build phase:
ENV MY_VAR=defaultwrites a key-value pair into the image's metadata. Every container from that image starts withMY_VAR=defaultunless overridden. - Container start (docker run): You supply
-e MY_VAR=productionor--env-file=./prod.env. Docker merges the new values on top of anyENVdefaults. Later values override earlier ones. - 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.
- 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-filedoes not require you to list every variable — only the ones you want to override. Missing variables will useENVdefaults. The file must be single-lineKEY=VALUEpairs (noexport).
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']raisesKeyErrorifVARisn't defined. Always useos.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. WriteMY_VAR=hello world(notMY_VAR="hello world"— that includes the quotes). - Override order: If you pass both
-eand--env-file, the last one on the command line wins. Same with multiple--env-fileflags. - Compilation-level secrets: For build-time secrets (e.g., private package sources), never use
ENV. Use--build-argindocker buildinstead — 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-fileexpects plainKEY=VALUElines withoutexport— usingexport KEY=valuewill pass the literal stringexport KEY=valueas value. - Hardcoding production secrets in
ENVDockerfile instructions — this bakes secrets into image layers that anyone withdocker historycan see. - Using
-efor every variable instead of--env-file— this clutters CLI history and the container run command, defeating security. - Assuming
environment:in Compose overrides all DockerfileENVvalues — it only overrides variables that appear in the environment block.
Variations
- Use
.dockerignorewith--env-fileto avoid committing sensitive files, but note the env-file path is resolved at runtime, not build time. - Combine env vars with Docker secrets for Swarm/Compose production environments — Docker secrets mount files at
/run/secrets/rather than pass env vars. - Leverage environment variable expansion in Compose with
${VAR:-default}syntax for conditional defaults indocker-compose.yml.
Real-world use cases
- A Python web app reads
MONGO_URIfrom an env var set via--env-filein CI, swapping between dev, staging, and production databases without rebuilding the image. - A Node.js microservice uses
NODE_ENV=productionandAPI_KEYpassed by Docker Composeenvironment:block, keeping per-service secrets out of version control. - A Java application reads
JAVA_OPTSfrom--envto 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.
ENVin Dockerfile sets default values, overridable at runtime by--envor--env-file.--env-fileis the safest method for secrets because the file stays outside the build context.- In Compose, use
environment:for dev overrides and reference a.envfile 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.