Passing Secrets to Containers Safely
Pass database passwords and API keys to containers without baking them into images, exposing them in inspect, or committing them to the repository.
Netbay Infrastructure Team
Netbay Engineering
On this page
The moment a container needs a password, the simple solutions are the dangerous ones. Hardcode it in the Dockerfile and the secret is immortalized in image layers, recoverable by anyone who can pull or inspect the image. Export it as an environment variable at run time and it is visible in docker inspect to anyone with daemon access. Commit it to the repo and it outlives rotation cycles for years. This guide ranks the realistic options for passing secrets to containers — build-time, run-time, and file-based mounts — and shows the practices that keep a legitimate variable from becoming a permanent exposure.
The Three Worst Paths, Quickly Discounted
Hardcoding ENV in a Dockerfile or copying a config with credentials embeds the secret into image layers. Deleting the line later does not help: the layer remains in the image history and in any registry that stored it. Handing secrets on the docker run command line puts them in the process list and shell history in addition to the container environment. And a file committed to a git repository is a secret that will outlive every employee, token, and database. The common thread is immutability: once written down in an image, a process table, or a repo, the secret is compromised on a timescale you do not control.
Environment Variables at Run Time
Passing values as environment variables at run time avoids the image-layer trap, which is why it is so common — but the container environment is visible via docker inspect to any account with access to the engine, and it leaks into diagnostics, core dumps, and misbehaving frameworks that print the environment. Treat -e as acceptable for ephemeral, low-value settings and never for the master database password.
# Visible to anyone who can inspect the daemon
docker run -d --name db -e POSTGRES_PASSWORD=hunter2 postgres:16-alpine
docker inspect db # shows the env, in plaintext
# Prefer reading from a protected source on the host
docker run -d --name db --env-file /root/prod.env postgres:16-alpine
chmod 600 /root/prod.envThe --env-file path at least keeps secrets out of the process list and the shell history, and the chmod is the part that turns a config file into a credential. The remaining limitation stands: whatever the container receives as environment ends up readable from the daemon.
File-Based Secrets via Compose
The cleanest non-orchestrator option is mounting secrets as files at runtime, owned and mode-protected by the engine. Compose implements this with a top-level secrets map referencing files, mounted under /run/secrets inside the container. The application reads the value from disk rather than from the environment, so it never appears in docker inspect at all.
name: app
services:
web:
image: my-web:1.9
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_passwordThe referenced file lives outside the repo on the host, the compose file commits cleanly because it contains no value, and the container sees /run/secrets/db_password. Add a refresh or a small entrypoint that exports from the secret file into the app's expected variable, and you keep the application's interface unchanged while the credential moves out of every visible surface.
Build-Time Secrets With BuildKit
The nastiest secret usage is the build that needs a key or token — a private registry credential, a signed artifact — because traditionally that required RUN steps that left the value in layers. BuildKit adds RUN --mount=type=secret, which provides the value to a single build step without ever committing it to an image layer.
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
RUN --mount=type=secret,id=npm_token npm config set //registry.npmjs.org/:_authToken $(cat /run/secrets/npm_token) && npm run build
CMD ["node", "dist/server.js"]docker build --secret id=npm_token,src=/root/.npm_token -t my-web:latest .The value is supplied at build time from a host file, readable only inside that one step, and absent from the final image and its history. This is the correct way to satisfy "the build needs a credential" without "the credential now ships in the image".
Hygiene Rules That Make Each Path Safe
Whatever the mechanism, four rules keep secrets from becoming incidents. Keep secret files out of the repo entirely — a .gitignore entry is not a luxury. Give the host files strict modes (0600 root) and narrow-read access to the one service account that needs them. Rotate deliberately and plan rotation as part of the delivery, because a secret that cannot be rotated is a door that can only be patched. And store values in a manager or at least an encrypted, access-controlled location on the host, so the container runtime remains lean while the source of truth stays armored.
Takeaway
Secrets belong in layers you can enumerate: a build-time mount for values the build needs, files under /run/secrets for runtime, and environment variables for the genuinely trivial. Every mechanism leaks in a specific place — image layers, daemon inspect, shell history — and choosing the mechanism means choosing the leak you can live with least. Netbay VPS hosts give you root and Docker on a fresh SSD disk, so these choices are yours to make correctly from the first container. Put them into practice at netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance