Environment Variables in Containers
Environment variables are name-value settings that Docker can place inside a container before its main process starts. They matter because the same image should usually run in different environments without rebuilding it: development, staging, production, tests, or one-off troubleshooting.
A container environment is not magic storage. It is process configuration. Docker combines defaults from the image with values you provide at runtime, then starts the container process with that final environment.
Overview: How Environment Variables Work
A Docker image is a read-only template made from filesystem layers and image metadata. A container is a running or stopped instance of that image with a thin writable layer, runtime settings, mounts, networking, and a process. Environment variables live in the container configuration and are passed to the process when Docker starts it.
There are several places environment variables can come from. A Dockerfile can set image defaults with ENV. A docker run command can set or override values with -e, --env, or --env-file. A Compose file can set them with environment or env_file. The application inside the container then reads them the same way it would read environment variables on any Linux system.
This separation is one of Docker’s most important configuration patterns: build the application once, then configure it at runtime. For example, an image can contain a web server and application code, while APP_MODE, LOG_LEVEL, PORT, and database host settings are supplied when the container is created.
Environment variables are visible to the container process and often visible through Docker inspection tools to users who can access the Docker daemon. They are convenient, but they are not a strong secret boundary. Do not bake passwords, tokens, or private keys into images with ENV. For real secrets, prefer Docker secrets, mounted secret files, or the secret mechanism provided by your orchestrator or deployment platform.
Syntax
docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG...]
| Form | Purpose |
|---|---|
-e NAME=value |
Sets one environment variable for the new container. |
--env NAME=value |
Long form of -e. |
-e NAME |
Passes the current host shell value of NAME if it is set. |
--env-file FILE |
Reads variables from a file containing NAME=value lines. |
ENV NAME=value |
Dockerfile instruction that stores a default in the image metadata. |
environment: |
Compose key for explicit environment variables on a service. |
env_file: |
Compose key for loading variables from one or more files. |
Put Docker flags before the image name. In docker run -e APP_MODE=dev alpine:3.20 env, -e is a Docker runtime flag. Anything after alpine:3.20 is the command that runs inside the container.
An env file uses simple lines such as APP_MODE=staging. Blank lines and comments are commonly used for readability. Quote placeholders in shell commands when they contain angle brackets or other shell-sensitive characters, such as -e "API_KEY=<YOUR_API_KEY>".
Examples
Example 1: Pass Variables with docker run
docker run --rm --name env-demo \
-e APP_MODE=development \
-e LOG_LEVEL=debug \
alpine:3.20 \
sh -c 'printf "mode=%s\nlevel=%s\n" "$APP_MODE" "$LOG_LEVEL"'
Output:
mode=development
level=debug
Docker creates a temporary container from alpine:3.20, adds APP_MODE and LOG_LEVEL to the container configuration, and starts sh. The shell expands the variables inside the container, not on the host, because the command is quoted with single quotes in the host shell.
Example 2: Load Variables from an Env File
printf 'APP_MODE=staging\nLOG_LEVEL=info\n' > app.env
docker run --rm --env-file app.env alpine:3.20 sh -c 'env | grep -E "^(APP_MODE|LOG_LEVEL)=" | sort'
Output:
APP_MODE=staging
LOG_LEVEL=info
--env-file is useful when a service has several settings or when you want repeatable local commands. The file is read when the container is created. Changing app.env later does not change an already-created container; recreate the container to apply new values.
Example 3: Set Image Defaults with Dockerfile ENV
FROM alpine:3.20
ENV APP_MODE=production PORT=8080
CMD ["sh", "-c", "printf 'mode=%s\nport=%s\n' \"$APP_MODE\" \"$PORT\""]
Build and run it:
docker build -t env-defaults:1.0 .
docker run --rm env-defaults:1.0
docker run --rm -e APP_MODE=debug env-defaults:1.0
Output:
mode=production
port=8080
mode=debug
port=8080
The Dockerfile stores harmless defaults in the image metadata. The first run uses both defaults. The second run overrides only APP_MODE, so PORT still comes from the image. This lets one image adapt to different environments without rebuilding.
Example 4: Configure a Service with Compose
services:
api:
image: alpine:3.20
command: sh -c 'printf "mode=%s\nlevel=%s\n" "$APP_MODE" "$LOG_LEVEL"'
environment:
APP_MODE: development
LOG_LEVEL: info
Output:
mode=development
level=info
Compose writes the service environment into the container it creates. The modern command is docker compose up, not the old standalone docker-compose binary. Compose is especially useful when multiple containers need coordinated settings, networks, and volumes.
How It Works Step by Step
- The Docker CLI parses runtime flags such as
-eand--env-file, then sends a create-container request to the Docker daemon. - The daemon reads the image configuration, including any Dockerfile
ENVdefaults saved in image metadata. - Docker applies runtime overrides. Values from
docker run -e,--env-file, or Compose service configuration are written into the new container’s configuration. - If the same variable appears more than once, the later or more specific runtime setting wins for that container. A variable provided at runtime overrides an image default.
- The storage driver mounts the image’s read-only layers and adds the container writable layer. Environment variables are not separate image layers at runtime; they are process configuration attached to the container.
- Docker starts the configured process with the final environment. Child processes inherit that environment unless the application changes it.
- When the process exits, the container stops. With
--rm, Docker removes the container object and its writable layer, but the original image remains.
Environment changes do not mutate an existing image. Running docker run -e APP_MODE=test env-defaults:1.0 creates a container with a different environment; it does not rewrite env-defaults:1.0. To change image defaults, edit the Dockerfile and rebuild. To change runtime configuration, recreate the container with new runtime settings.
Common Mistakes
Putting Secrets in a Dockerfile
FROM alpine:3.20
ENV API_KEY=hardcoded-secret
RUN echo "configured" > /status.txt
This is wrong because ENV values are stored in image metadata and may be visible through inspection or build history. Removing the value in a later layer does not reliably erase it from earlier image history. Use runtime secret injection, Docker secrets, mounted files, or your orchestrator’s secret store instead.
Forgetting to Quote Shell-Sensitive Placeholders
docker run --rm -e API_KEY=<YOUR_API_KEY> alpine:3.20 env
A bare <YOUR_API_KEY> looks like shell redirection, so the command is not valid shell syntax. Quote the whole assignment:
docker run --rm -e "API_KEY=<YOUR_API_KEY>" alpine:3.20 env
Expecting Env File Changes to Update Running Containers
printf 'APP_MODE=production\n' > app.env
docker run -d --name env-stale --env-file app.env alpine:3.20 sleep 300
printf 'APP_MODE=debug\n' > app.env
The running container still has APP_MODE=production. Docker reads the env file only when the container is created. Recreate the container to apply the new file contents.
Confusing ARG and ENV
FROM alpine:3.20
ARG APP_MODE=production
CMD ["sh", "-c", "echo APP_MODE=$APP_MODE"]
ARG is for build-time values. It is not automatically present when the container runs. If the runtime process needs a default, persist it with ENV:
FROM alpine:3.20
ARG APP_MODE=production
ENV APP_MODE=${APP_MODE}
CMD ["sh", "-c", "echo APP_MODE=$APP_MODE"]
Best Practices
- Use environment variables for configuration that changes between environments, not for application code or persistent data.
- Set harmless Dockerfile
ENVdefaults only when they are safe and broadly correct, such asPORT=8080orLOG_LEVEL=info. - Use
docker run -efor one-off overrides and--env-filewhen several settings belong together. - Use Compose
environmentfor clear service-level settings andenv_filefor shared local configuration files. - Do not put real secrets in Dockerfiles, image labels, env files committed to Git, or copied project files.
- Use obvious placeholders such as
<YOUR_API_KEY>orchangemein examples and documentation. - Quote environment assignments in shell commands when values contain spaces, dollar signs, angle brackets, ampersands, or other special characters.
- Recreate containers after changing env files or Compose configuration; running processes do not automatically receive updated environment variables.
- Keep image tags specific, such as
alpine:3.20, becauselatestis a moving target and makes configuration debugging harder.
Practice Exercises
- Run an
alpine:3.20container that printsAPP_MODEandLOG_LEVEL. Pass both values with-e. Expected end state: the output shows exactly the values you supplied. - Create an
app.envfile with three non-secret settings, then run a container with--env-fileand print only those variables. Hint: useenv,grep, andsort. - Write a Dockerfile with a safe default
ENV PORT=8080. Build it, run it once with the default, then run it again overridingPORTat runtime.
Summary
- Environment variables configure a container process at startup.
- Dockerfile
ENVcreates image defaults;docker run -e,--env-file, and Compose can override them per container. - Changing an env file or Compose file does not update already-running containers; recreate them.
ARGis build-time configuration, whileENVis runtime-visible configuration.- Environment variables are convenient but not a complete secret-management system.
- Quote placeholders and special values so your commands are valid shell syntax.
