A secret is any credential your app needs but no one else should see: database passwords, API keys, tokens. The rule is simple. Don't bake them into the image. An image is shared and its layers can be inspected, so a secret built into one is a secret leaked to everyone who can pull it.
Two mechanisms feed values into a build or container, and only one is reasonable for secrets.
Build args vs. env vars
- Build arg (
ARG): a value available only while the image is being built, passed with--build-arg. Good for build-time choices like a version number. Not for secrets: the value is visible in the image's build history. - Env var (
ENV/-e): a value available to the app at runtime. This is where configuration belongs, and secrets are injected here at run time, never written into the Dockerfile.
What not to do
# DON'T: hardcode a real secret as an ENV in the image.
# It's baked into a layer and ships with every copy of the image.
ENV STRIPE_API_KEY="sk_live_4eC39HqLyjWDarjtT1zdp7dc"
# DON'T: pass a secret as a build arg either.
# Build args are recorded in the image history; `docker history` reveals them.
ARG STRIPE_API_KEY
RUN echo "$STRIPE_API_KEY" > /app/.key
Anyone who pulls the image can run docker history or inspect its layers and read
these back, even if a later instruction appears to delete the value.
How to use it
# Build-time only: a non-secret choice
ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV # DO: inject the real secret at run time, not at build time
docker run -p 3000:3000 -e STRIPE_API_KEY="sk_live_..." my-api
# DO: load a whole file of vars (keep this file out of git)
docker run -p 3000:3000 --env-file .env my-api
In Compose, the same idea uses environment for non-secret config and
env_file for values you keep out of source control.
Two caveats
Env vars are a baseline, not a vault. Injecting secrets at runtime keeps them out
of the image, which is the main goal. But the values aren't invisible: anyone with access to the
Docker host can run docker inspect on a running container and read its environment.
For a solo project that's usually fine. When more people share the infrastructure, production
platforms (Kubernetes, AWS) provide dedicated secret stores that encrypt values and control who
can read them.
Sometimes a secret is needed during the build itself. Say npm install
has to pull a package from a private registry that requires a token. You can't use a runtime env
var, and you now know not to use a build arg. Docker has a purpose-built escape hatch: a
BuildKit secret mount . You pass the secret with docker build --secret,
and a RUN --mount=type=secret,... instruction makes it available to that single step
only. The value is never written into a layer or the build history. Just remember that "secret
needed at build time" has a dedicated tool, and reach for it when you hit that wall.
Why it matters
-
Image layers and build history are inspectable. A
RUNorARGthat touched a secret can be read back out, even if a later layer "removes" it. - Images get pushed to registries and shared. A baked-in secret travels with every copy.
- Runtime env vars stay out of the image, so the same image is safe to ship to every environment, each supplying its own credentials.
Check your understanding
Build arg isn't safe for secrets
You pass an API key to your image build with `--build-arg STRIPE_KEY=sk_live_...`. The key doesn't appear in any `ENV` instruction. Why is this still unsafe?