env.dev

Docker Env Variables: ENV, ARG & Runtime

How to use environment variables in Docker: ENV vs ARG in Dockerfile, docker run -e, --env-file, multi-stage builds, BuildKit secrets, and best practices.

Last updated:

Docker has three places where an environment variable can live, and getting them confused is one of the most common ways to leak a secret into a public image. ARG exists only during the build, ENV bakes into every layer of the image, and docker run -e / --env-file inject values at container start. BuildKit secrets, available since Docker 18.09 and stable in modern Docker Engine, are the only build-time path that does not appear in docker history. This guide walks through every Docker-specific mechanism, the precedence rules, and the gotchas that bite people moving from local .env workflows into containers. For orchestrated multi-container setups, jump to the Docker Compose env guide; for the next layer up, see Kubernetes environment variables.

How do you pass environment variables with docker run?

The -e (or --env) flag sets a single environment variable when starting a container. You can repeat it as many times as needed.

bash
# Set individual variables
docker run -e NODE_ENV=production -e PORT=3000 my-app

# Pass a host variable by name (value taken from host shell)
export API_KEY=sk-abc123
docker run -e API_KEY my-app

# Combine with other run flags
docker run -d --name web -e NODE_ENV=production -e LOG_LEVEL=info -p 3000:3000 my-app

When you specify -e API_KEY without a value, Docker forwards the current value of API_KEY from your host shell. If the variable is not set on the host, it will not be set in the container either.

How do you load environment variables from a file?

The --env-file flag loads variables from a file. Each line should follow the KEY=value format.

bash
# Load from a single file
docker run --env-file ./app.env my-app

# Load from multiple files (later files override earlier ones)
docker run --env-file ./base.env --env-file ./overrides.env my-app

# Combine with inline variables (inline takes priority)
docker run --env-file ./app.env -e NODE_ENV=production my-app

The env file format supports comments with # and blank lines. Quotes around values become part of the value, so avoid them. You can validate your env files with the env validator.

bash
# app.env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://user:pass@db:5432/app

# This is a comment
LOG_LEVEL=info

How does the ENV instruction work in a Dockerfile?

The ENV instruction sets environment variables that persist in the built image. They are available during subsequent build steps and at runtime when a container starts.

dockerfile
FROM node:20-alpine

# Set defaults baked into the image
ENV NODE_ENV=production
ENV PORT=3000

# Use them in subsequent commands
RUN echo "Building for $NODE_ENV"

WORKDIR /app
COPY . .
RUN npm ci --omit=dev

EXPOSE $PORT
CMD ["node", "server.js"]

Variables set with ENV become permanent defaults in the image. Anyone who runs a container from the image will inherit these values unless they override them at runtime with -e.

What is the difference between ARG and ENV in a Dockerfile?

ARG defines build-time variables that exist only during the image build. ENV defines runtime variables that persist in the final image. They solve different problems and are often used together.

dockerfile
FROM node:20-alpine

# ARG: only available during build, not in the final image
ARG BUILD_VERSION=0.0.0
ARG BUILD_DATE

# ENV: persists in the image and is available at runtime
ENV APP_VERSION=$BUILD_VERSION
ENV NODE_ENV=production

# ARG values can be used in RUN commands during build
RUN echo "Building version $BUILD_VERSION on $BUILD_DATE"

WORKDIR /app
COPY . .
RUN npm ci --omit=dev

CMD ["node", "server.js"]

Pass build arguments with the --build-arg flag:

bash
docker build --build-arg BUILD_VERSION=1.2.3 --build-arg BUILD_DATE=$(date -u +%Y-%m-%d) -t my-app .

A common pattern is to accept a value as ARG and copy it into an ENV so it is available both at build time and runtime. Never pass secrets via ARG — build arguments are visible in docker history and image metadata.

How do environment variables behave in multi-stage builds?

Each FROM instruction starts a new build stage with a clean environment. ENV and ARG values from a previous stage do not carry over automatically.

dockerfile
# Stage 1: build
FROM node:20-alpine AS builder
ARG BUILD_VERSION=0.0.0
ENV NODE_ENV=production

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: runtime (clean stage — no ENV or ARG from builder)
FROM node:20-alpine
ENV NODE_ENV=production

# Must re-declare ARG if needed
ARG BUILD_VERSION=0.0.0
ENV APP_VERSION=$BUILD_VERSION

WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

CMD ["node", "dist/server.js"]

If you need a build argument in multiple stages, redeclare ARG in each stage. The value passed via --build-arg is available to every stage that declares the same ARG name.

How do you override Dockerfile ENV values at runtime?

Runtime flags always take priority over image defaults. The -e flag and --env-file override any ENV instruction baked into the image.

bash
# Image has ENV NODE_ENV=production, but override to development
docker run -e NODE_ENV=development my-app

# Override multiple values
docker run --env-file ./dev.env -e LOG_LEVEL=debug my-app

Priority order (highest wins):

  1. -e / --env inline flags
  2. --env-file values
  3. Dockerfile ENV defaults

How should you handle .dockerignore and .env files?

The .dockerignore file prevents files from being sent to the Docker build context. Always exclude .env files to avoid accidentally baking secrets into your image.

bash
# .dockerignore
.env
.env.*
*.env
!.env.example
.git
node_modules

Even if your Dockerfile never references .env files, they still get sent to the build daemon as part of the build context unless excluded. A COPY . . instruction would include them in the image layer. Always add .env to your .dockerignore.

Why should you avoid storing secrets in ENV?

Environment variables set with ENV or ARG in a Dockerfile are permanently embedded in image layers and metadata. Anyone with access to the image can extract them.

bash
# Anyone can see ENV values baked into an image
docker inspect my-app --format '{{json .Config.Env}}'

# ARG values appear in build history
docker history my-app
Never put secrets in ENV or ARG instructions. Database passwords, API keys, and tokens embedded in the image are visible to anyone who can pull it. Use BuildKit secrets for build-time secrets or inject them at runtime with -e or --env-file.

How do you use BuildKit secrets during builds?

BuildKit provides --mount=type=secret to pass sensitive data into build steps without leaving traces in the image layers. The secret is mounted as a file accessible only during that single RUN instruction.

dockerfile
# syntax=docker/dockerfile:1
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./

# Mount the secret file during npm install (e.g., private registry token)
RUN --mount=type=secret,id=npmrc,target=/app/.npmrc npm ci

COPY . .
RUN npm run build

CMD ["node", "dist/server.js"]

Pass the secret at build time:

bash
# Enable BuildKit
export DOCKER_BUILDKIT=1

# Pass a file as a secret
docker build --secret id=npmrc,src=$HOME/.npmrc -t my-app .

# Pass an environment variable as a secret (Docker 23.0+)
docker build --secret id=gh_token,env=GITHUB_TOKEN -t my-app .

Inside the Dockerfile, the secret is available at /run/secrets/<id> by default, or at the path specified by target. It exists only for the duration of that RUN step and is never written to an image layer.

How do you inspect environment variables in a running container?

Use docker inspect or docker exec to view the environment variables set in a container.

bash
# View all env vars configured on a container
docker inspect my-container --format '{{json .Config.Env}}' | jq .

# View env vars from inside a running container
docker exec my-container env

# Filter for a specific variable
docker exec my-container printenv NODE_ENV

# View env vars set in the image (not runtime overrides)
docker inspect my-app:latest --format '{{json .Config.Env}}'

docker inspect shows the combined result of Dockerfile ENV defaults and runtime overrides. It also reveals variables passed via -e and --env-file, so be cautious about running it on production containers that may hold secrets.

References

For a quick reference to the rest of the Docker CLI, see the Docker cheat sheet.

Was this helpful?

Read next

Docker on Windows 2026: Desktop, WSL2, Rancher, Podman

Docker on Windows runs four ways in 2026: Docker Desktop, Docker Engine in WSL2, Rancher Desktop, or Podman Desktop. Licensing, performance, and how to pick.

Continue →

Frequently Asked Questions

What is the difference between ENV and ARG in a Dockerfile?

ARG is only available during the build (docker build) and is not persisted in the final image. ENV is available both during build and at runtime in the container. Use ARG for build-time configuration (version numbers, build flags) and ENV for runtime configuration (API URLs, feature flags).

How do I pass environment variables to docker run?

Use -e KEY=VALUE for individual variables or --env-file .env to load from a file. Environment variables set with -e override ENV values from the Dockerfile.

Should I put secrets in ENV instructions?

No. ENV values are baked into the image layer and visible via docker inspect and docker history. Use BuildKit secrets (--mount=type=secret) for build-time secrets, and inject runtime secrets via -e or a secrets manager.

Why use BuildKit secrets instead of ARG for tokens?

ARG values appear in docker history and image metadata, so any token passed via --build-arg leaks to anyone who can pull the image. BuildKit secrets (--mount=type=secret) mount the value as a file only during a single RUN step, never write it to a layer, and never appear in docker history. Use --secret id=token,env=GITHUB_TOKEN for tokens you already have in your shell.

Do ARG values carry over between multi-stage builds?

No. Each FROM instruction starts a new stage with a clean ARG and ENV scope. If you need the same build argument in multiple stages, you must redeclare ARG NAME in each stage. The value passed via --build-arg is then available to every stage that declares the matching ARG name.

Stay up to date

Get notified about new guides, tools, and cheatsheets.