Learn how to manage configuration and secrets in Docker using ENV, ARG, .env files, runtime injection, and Docker secrets - without baking sensitive data into images.
Environment Variables and Secrets
Almost every application needs configuration: database URLs, API keys, feature flags, port numbers. Docker provides several mechanisms to inject this configuration into containers. Using them correctly - especially for sensitive data - is one of the most important habits to develop early.
The golden rule: secrets must never be baked into an image. An image is a shareable artefact. Anything embedded in it can be extracted by anyone who has access to it, now or in the future.
ENV vs ARG
Both ENV and ARG set variables in a Dockerfile, but they have different scopes and different security implications.
ENV
Use ENV for configuration that the application needs at runtime.
At runtime, override specific variables:
ARG
Use ARG for values needed only during the build step - version numbers, build flags.
The ARG + ENV pattern
To make a build-time value available at runtime, explicitly promote it:
Never Bake Secrets into Images
Here are three antipatterns that commit secrets into image layers:
Antipattern 1: ENV with a real secret
Even if you later RUN unset DATABASE_PASSWORD, the value is still visible in the layer history:
Antipattern 2: Secrets copied in via COPY
Even if you RUN rm /app/.env in a later layer, the file exists in the layer where it was copied and can be extracted.
Antipattern 3: Secrets passed as ARG
ARG values are visible in docker history and the build cache. Never pass real secrets as ARG.
.env Files with Docker Compose
The .env file is the most practical secret management solution for local development. Compose automatically reads it and substitutes values into ${VARIABLE} placeholders in the YAML.
Make sure .env is in your .gitignore and .dockerignore:
Provide a .env.example with placeholder values that developers can copy:
Runtime Injection Patterns
docker run with -e
The simplest approach for standalone containers:
docker run with --env-file
Load multiple variables from a file:
Unlike Compose's .env (which substitutes into YAML), --env-file injects variables directly into the container. The file is not read by the daemon - it is processed by the CLI on your local machine.
Docker Secrets (Swarm / Compose)
Docker Swarm has native secret management. Compose v3+ can use the same mechanism for local development (with limitations).
Secrets are stored encrypted at rest (in Swarm's Raft store) and mounted as files inside the container at /run/secrets/<name>. They are never exposed as environment variables, making them harder to accidentally leak.
Creating secrets in Swarm
Using secrets in a Compose file (Swarm deploy)
Inside the container, the secret is available at /run/secrets/db_password. Your application reads it from the filesystem rather than from an environment variable:
Secrets in Compose for local development
Without Swarm, you can use file-based secrets in Compose:
The file is mounted read-only at /run/secrets/db_password inside the container.
Using Build Secrets (BuildKit)
For secrets needed only during docker build (e.g., an NPM token to install private packages), use BuildKit's --secret flag. The secret is mounted at build time and is never stored in any image layer.
This is the correct way to use credentials during builds.
Summary of Patterns
Checklist
Before shipping a Docker image:
- No hardcoded passwords, API keys, or tokens in the Dockerfile
-
.envis in.gitignoreand.dockerignore -
docker history my-imageshows no secrets -
docker inspect my-image | grep -i secretreturns nothing sensitive - Secrets are injected at runtime, not baked in at build time
- A
.env.examplewith placeholder values exists for onboarding new developers
Proper secret handling is not just about security - it makes your images truly portable. An image without hardcoded secrets can be deployed to development, staging, and production by injecting the appropriate values at runtime.