Using .env Files
A .env file is a simple text file that stores environment variable names and values. In Docker, it is useful because the same image can run with different settings in development, staging, and production without rebuilding the image.
The important detail is that Docker has two related but different uses for .env files: passing variables into a container, and substituting variables into a Compose file. Confusing those two is the source of many broken deployments.
Overview: How .env Files Work
An environment variable is part of a process environment. When Docker starts a container, the daemon creates the container from read-only image layers, adds a thin writable container layer, prepares namespaces and networking, then starts the configured process with a final set of environment variables. Those variables come from image defaults set with Dockerfile ENV, values passed on the command line with -e, values read from --env-file, and, in Compose, values defined by environment or env_file.
A .env file used with docker run --env-file is direct container configuration. Docker reads each name and value and injects them into the container process. The file is not copied into the image unless your Dockerfile explicitly copies it. This is different from ENV in a Dockerfile, which is stored in image metadata and becomes a default for every container created from that image.
Docker Compose adds another layer. By default, docker compose looks for a file named .env in the project directory and uses it for variable substitution in compose.yml. For example, ${WEB_PORT} in the Compose file can be replaced by a value from .env. That automatic Compose .env file does not automatically put every variable into the container. To pass variables into the container, use the service’s environment or env_file keys.
This separation is useful. Build images once, then supply configuration at runtime. An image named config-demo-api:1.0 can run locally with a development database URL and in production with a production database URL. The image layers stay the same; only the container’s process environment changes.
Do not treat .env as a complete secret-management system. A local .env is often appropriate for development, but production secrets belong in your orchestrator, Docker secrets, cloud secret manager, or mounted secret files. Also add local .env files to .gitignore and .dockerignore so they are not committed or sent in the build context.
Syntax
# app.env
APP_ENV=development
PORT=8080
LOG_LEVEL=debug
API_KEY=<YOUR_API_KEY>
| Form | Meaning |
|---|---|
NAME=value |
Defines one variable. The name should normally use letters, numbers, and underscores, and should not start with a number. |
# comment |
Comment line. Use comments for non-secret notes about expected values. |
EMPTY= |
Defines the variable with an empty string value. |
docker run --env-file app.env IMAGE |
Reads variables from app.env and passes them to the container. |
docker run -e NAME=value IMAGE |
Sets or overrides one variable for that container. |
docker compose --env-file file.env up |
Uses a specific file for Compose variable interpolation. |
env_file: |
Compose service key that passes variables from a file into that service’s containers. |
Keep .env syntax plain. Do not write shell commands, export, or spaces around the equals sign. If a value contains spaces, test the exact behavior with your Docker or Compose version before depending on it; simple values are the most portable. For examples in this lesson, obvious placeholders such as <YOUR_API_KEY> are used instead of real secrets.
Examples
Example 1: Pass an env file to docker run
Create an env file for runtime settings:
APP_ENV=development
PORT=8080
LOG_LEVEL=debug
Run a small container that prints the values:
docker run --rm --env-file app.env alpine:3.20 sh -c 'echo APP_ENV=$APP_ENV; echo PORT=$PORT; echo LOG_LEVEL=$LOG_LEVEL'
Output:
APP_ENV=development
PORT=8080
LOG_LEVEL=debug
Docker reads app.env on the host and adds those names to the process environment inside the container. The file itself is not mounted into the container and is not baked into the alpine:3.20 image. The --rm flag removes the container after the process exits; the image remains available locally.
Example 2: Override one value at runtime
docker run --rm --env-file app.env -e LOG_LEVEL=info alpine:3.20 sh -c 'echo APP_ENV=$APP_ENV; echo PORT=$PORT; echo LOG_LEVEL=$LOG_LEVEL'
Output:
APP_ENV=development
PORT=8080
LOG_LEVEL=info
The command line override wins for LOG_LEVEL. This is handy when most settings come from a file but one value changes for a single test run. The exact precedence you should remember is practical: Dockerfile ENV gives image defaults, --env-file supplies container values, and -e can override a specific value for that run.
Example 3: Use Compose .env for interpolation and env_file for the container
A Compose project might have this .env file:
WEB_PORT=8080
APP_ENV=development
LOG_LEVEL=debug
The Compose file can use WEB_PORT to choose the host port and env_file to pass variables into the container:
services:
web:
image: nginx:1.27-alpine
ports:
- "${WEB_PORT}:80"
env_file:
- .env
environment:
LOG_LEVEL: info
Start it with modern Compose:
docker compose up -d
Output:
[+] Running 2/2
Network env-demo_default Created
Container env-demo-web-1 Started
Here ${WEB_PORT} is replaced before the service is created, so port 8080 on the host maps to port 80 in the container. The env_file key passes APP_ENV, WEB_PORT, and LOG_LEVEL into the service container, but the explicit environment entry changes LOG_LEVEL to info. Remember that ports publishes a port; an environment variable named PORT only configures an application if the application reads it.
Example 4: Select a different Compose env file
docker compose --env-file staging.env config
Output:
name: env-demo
services:
web:
environment:
LOG_LEVEL: info
image: nginx:1.27-alpine
ports:
- mode: ingress
target: 80
published: "8081"
protocol: tcp
docker compose config renders the final Compose model after interpolation. This is a safe way to check whether ${WEB_PORT} became the value you expected before you start or replace containers. In this example, staging.env contains WEB_PORT=8081.
How It Works Step By Step
- You write a plain env file such as
app.envwith oneNAME=valueentry per line. - With
docker run --env-file app.env, the Docker client sends the requested container configuration to the daemon. The daemon creates the container from the image and stores the final environment in the container configuration. - When the process starts, Docker passes that environment to the process, just like any operating system process environment. The application must read those variables itself.
- With Compose, the CLI first loads variables for interpolation. It uses shell variables and a project
.envfile to replace expressions such as${WEB_PORT}incompose.yml. - Compose then creates service containers. Only variables listed in
environmentor files listed underenv_filebecome container environment variables. - If you change an env file, existing containers do not magically change. Recreate the container with
docker runagain, or rundocker compose up -dso Compose can update affected services.
Under the hood, changing a runtime env file does not create a new image layer. That is the main advantage over Dockerfile ENV for deployment-specific values. The image is still a read-only stack of layers; the container’s configuration changes around it.
Common Mistakes
Expecting Compose .env to automatically enter the container
services:
web:
image: nginx:1.27-alpine
ports:
- "${WEB_PORT}:80"
If .env contains WEB_PORT=8080, this publishes the host port, but it does not guarantee a WEB_PORT variable inside the container. Add environment or env_file when the application needs the value:
services:
web:
image: nginx:1.27-alpine
ports:
- "${WEB_PORT}:80"
environment:
WEB_PORT: "${WEB_PORT}"
Copying .env into an image
FROM alpine:3.20
WORKDIR /app
COPY .env .env
CMD ["sh", "-c", "cat .env"]
This is wrong for sensitive configuration. Image layers are reusable artifacts, and deleting a file in a later layer does not erase it from an earlier layer. Keep local env files out of the build context with .dockerignore:
.env
*.env
secrets/
Using production secrets in a local .env file
DATABASE_URL=postgres://admin:changeme@db:5432/app
API_KEY=<YOUR_API_KEY>
This is acceptable only as a placeholder or disposable local configuration. Real production values should come from a secret store or orchestrator and should be rotated if they were ever committed, copied into an image, or shared in logs.
Best Practices
- Use env files for runtime configuration that changes between environments, not for values that should be compiled into the image.
- Name files clearly, such as
app.env,dev.env, orstaging.env, when more than one environment exists. - Add local env files to
.gitignoreand.dockerignore. - Use obvious placeholders in examples and templates, such as
<YOUR_API_KEY>, never real-looking secrets. - Use
docker compose configto inspect interpolated Compose output before starting important services. - Keep Dockerfile
ENVvalues harmless, such asNODE_ENV=productionorPORT=8080. - Prefer
environmentfor a few explicit Compose variables andenv_filewhen a service has many settings. - Recreate containers after changing env files; running processes do not reload their environment automatically.
- Use pinned image tags such as
alpine:3.20andnginx:1.27-alpineso configuration changes are not mixed with unexpected image changes.
Practice Exercises
- Create
app.envwithAPP_ENV=local,PORT=5000, andLOG_LEVEL=debug. Run analpine:3.20container that prints all three values. Hint: use--env-fileandsh -c. - Write a Compose file for
nginx:1.27-alpinewhere${WEB_PORT}controls the published host port. Then rundocker compose configto confirm the rendered value before starting the service. - Audit a project directory that contains
.env,.env.example, andcompose.yml. Decide which files should be committed, which should be ignored by Git, and which should be excluded from the Docker build context.
Summary
docker run --env-filepasses variables from a file into one container.- Compose automatically reads a project
.envfor interpolation, but that alone does not pass every value into containers. - Use Compose
environmentorenv_filefor service container variables. - Runtime env files change container configuration, not image layers.
- Do not copy sensitive
.envfiles into images or commit them to source control.
