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

  1. The Docker CLI parses runtime flags such as -e and --env-file, then sends a create-container request to the Docker daemon.
  2. The daemon reads the image configuration, including any Dockerfile ENV defaults saved in image metadata.
  3. Docker applies runtime overrides. Values from docker run -e, --env-file, or Compose service configuration are written into the new container’s configuration.
  4. 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.
  5. 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.
  6. Docker starts the configured process with the final environment. Child processes inherit that environment unless the application changes it.
  7. 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 ENV defaults only when they are safe and broadly correct, such as PORT=8080 or LOG_LEVEL=info.
  • Use docker run -e for one-off overrides and --env-file when several settings belong together.
  • Use Compose environment for clear service-level settings and env_file for 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> or changeme in 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, because latest is a moving target and makes configuration debugging harder.

Practice Exercises

  1. Run an alpine:3.20 container that prints APP_MODE and LOG_LEVEL. Pass both values with -e. Expected end state: the output shows exactly the values you supplied.
  2. Create an app.env file with three non-secret settings, then run a container with --env-file and print only those variables. Hint: use env, grep, and sort.
  3. Write a Dockerfile with a safe default ENV PORT=8080. Build it, run it once with the default, then run it again overriding PORT at runtime.

Summary

  • Environment variables configure a container process at startup.
  • Dockerfile ENV creates 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.
  • ARG is build-time configuration, while ENV is 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.