Your Docker Image Is Leaking Secrets

Secrets baked into a Docker image leak everywhere the image goes, and you cannot rotate or revoke them. Here is how to keep images clean by fetching secrets at runtime with Infisical.

Your Docker Image Is Leaking Secrets
If your Docker container talks to a database, a payment provider, or an upstream API, it needs secrets: a connection string, a Stripe key, an access token. The question is how those secrets get into the container, and the easiest answer is also the most dangerous one. You bake them in. A `.env` file copied in next to your code, an `ENV` line in the Dockerfile, a build argument passed at build time. It works on the first try, the app boots, and everybody moves on.
The problem is that those secrets are now part of the image. They go everywhere the image goes. This walkthrough shows how easily they can be extracted, then rebuilds the same image with secrets injected at runtime by Infisical, an open-source secrets manager, so the image ships only code, no secrets.
How secrets end up baked into an image
Here is how it usually happens. Your app reads its config from a `.env` file. That file is in your `.gitignore`, so it never touches the repo, and locally everything works. Then you containerize the app and the Dockerfile looks completely clean: no secrets in the file. But `COPY . .` copies the whole build context, there is no `.dockerignore`, and the `.env` file goes straight into the image.
This is not a rare mistake. Researchers scanned 337,000 images from Docker Hub and other registries and found that 8.5% of them contained secrets, many of them private keys and API keys. A `.env` file swept in by `COPY` is one of the most common ways they get there.
Build the image, and reading the secrets back out is trivial. Open a shell in the container, `cat` the `.env` file, and there they are: the full database connection string and the live Stripe key. Anyone who pulls the image can do the same.
Why you can't scrub a secret out later
A common instinct is to delete the `.env` file in a later step of the Dockerfile. It does not work. Layers are append-only, so deleting a file in a later layer does not remove it from the earlier one. Everything is still in the image. `RUN rm` hides the file from the final filesystem, but the bytes remain in the layer where they were added.
The other two shortcuts are just as exposed. `ENV` lines and build arguments get recorded in the image metadata, so `docker history` and `docker inspect` print them right back.
Three problems with baking secrets in
Baking secrets into an image creates three concrete problems.
First, the secret is everywhere the image is. Every registry you push to, every CI runner that builds on it, every laptop that ever pulls it, every backup, and every cache.
Second, you cannot rotate or revoke it. The value is frozen into the layer, so changing it means rebuilding the image and redeploying every running container. Rotation gets skipped, and if a credential leaks there is no kill switch.
Third, you have no control and no record. There is no access policy on who can read each secret, no audit log of who pulled it, and no way to answer which image contains which secrets.
Multiply all three by every service, every environment, and every tag, and you get secret sprawl: the same credentials sitting in hundreds of images with no central source of truth.
Runtime secrets vs. build-time secrets
One distinction matters here. This is about the secrets your app reads at runtime. Build-time secrets, like a token to pull a private package, are a different problem, and Docker already solves that with BuildKit secret mounts. The trap worth avoiding is baking runtime secrets into the image.
The fix is simple to state: do not put the secret in the image at all.
The fix: fetch secrets at runtime
The secrets move into Infisical, a centralized store for your team's secrets with access control, audit logs, and rotation built in. From then on, that is where every secret lives.
The image ships two things: your code and the Infisical CLI, a small client that fetches secrets from the store. When the container starts, the CLI authenticates with a short-lived token, pulls exactly the secrets this container is allowed to see, sets them as environment variables, and then runs your app. The secrets live in the process for the life of the container and nowhere else. Nothing is ever written into a layer.
That resolves all three problems. The secret is not in the image, so it is not in any registry, cache, or laptop. You rotate it in one place and the next container start picks it up with no rebuild. Every fetch is authenticated, scoped, and logged. And because the container holds a short-lived identity instead of a permanent key, you can cut it off whenever you want.
Setting up the project and a machine identity
The secrets live in an Infisical secrets management project, organized by environment and folder path. In this example, a production environment holds two values: `DB_URL` and `STRIPE_SECRET_KEY`.
The container needs a way to prove who it is and that it is allowed to fetch those secrets. In Infisical, that is a machine identity: the same idea as a user account, except it belongs to a workload instead of a person. You add the identity to the project, give it viewer access so it can read secrets, and create it.
Like a user account, the identity needs credentials to log in. The platform-agnostic option is Universal Auth, which gives you a Client ID and a Client Secret. These are like a username and password for the container. On their own they unlock nothing except the ability to log in as one identity, and that identity can only read what you have scoped it to: this project, this environment, this path, and nothing else. On the machine that runs the container, you set them as `INFISICAL_CLIENT_ID` and `INFISICAL_CLIENT_SECRET`.
If your containers run on Kubernetes, AWS, GCP, or Azure, Infisical can authenticate using that platform's own identity instead, with no Client ID or Client Secret at all. The container proves who it is with its native cloud identity. That is the stronger option and worth using wherever possible. Universal Auth is used here for simplicity.
Rebuilding the image without the secret
Two changes make the image clean. First, the `.env` file never goes into the image again. You can delete it outright and add `.env` to a `.dockerignore` as a backstop. Second, you install the Infisical CLI in the image and change the start command to run through `infisical run`, which fetches the secrets and then launches the app. The install steps for the CLI come straight from the Docker entrypoint guide.
The start command also needs a project ID, which tells the CLI which project to pull from. You copy it from the project settings. It is not a secret. The command pulls secrets from the production environment and runs `node index.js` as a process that inherits them. The secrets never touch disk and are gone the moment the process exits.
Running the container and confirming it's clean
To run the container, it needs a token to authenticate. You mint a short-lived one from the machine identity credentials and hand it in as a single environment variable. The CLI inside the container picks it up automatically. The Docker entrypoint guide walks through creating the machine identity, exporting the token, and setting these variables end to end.
Build it, run it, and the app boots with a live database URL and a live Stripe key, fetched at startup. Now rerun the same commands that leaked everything before. There is no `.env` file to `cat`. Run `docker history` or `docker inspect` and the layers come back empty. You could push this image to any registry, public or private, and there is nothing sensitive to leak or steal.
If you would rather not put the CLI inside the image at all, it can run on the host and inject the secrets at Docker runtime with `infisical export` instead.
Closing the loop: rotation, audit logs, and least privilege
Pulling secrets at runtime keeps them out of the image, but centralizing them is what closes the loop on the three original problems.
Rotation becomes a single edit. Change the value of `DB_URL` in Infisical, restart the container, and the logs show the new value. No image rebuild, no Dockerfile change. Rotation goes from "redeploy and rebuild everywhere" to "edit one value," which is what makes real secret rotation practical.
Control and record come for free. Every time a container fetches secrets, Infisical logs which identity, which environment and path, from which IP address, and at what time. The question you could not answer with a baked image, which container read this secret and when, becomes a filter you look at in one place. Access follows least privilege through access controls, so a machine identity sees only the secrets you grant it and never another team's. Because the token is short-lived, removing a role cuts the container's access off on its next fetch.
You can take this further with dynamic secrets, where each container gets its own database credential generated on demand, scoped to a short lease, and revoked automatically when the lease ends. If that credential leaks, it expires on its own before anyone can use it.
Scaling past one container
The same pattern extends to a full stack. With Docker Compose, you can share one machine identity across every service or give each service its own. Separate identities mean your web service and your workers only see their own secrets, show up separately in the audit log, and can be revoked one at a time.
Some apps do not read environment variables and expect a config file, and some need to pick up new values without a restart. That is what the Infisical Agent is for: a small daemon that runs next to your app, authenticates with its own identity, renders your secrets to a file on a shared volume, and keeps that file current as values change. Your app just reads the file and never talks to Infisical directly.
On Kubernetes, do not wire the CLI into every image. Infisical has a dedicated Kubernetes operator that syncs secrets into the cluster as native Kubernetes secrets, with rotation and auto-reload built in. There is a full walkthrough in The Infisical Kubernetes Operator.
Ship your code, not your secrets
Pick the method that fits where your secrets run. The rule underneath all of them is the same: fetch secrets at runtime instead of baking them into an image. Baked in, they leak everywhere the image goes, and you cannot rotate or revoke them. Fetched at runtime with Infisical, the image carries nothing sensitive, you rotate in one place, and every access is logged. Infisical is open source and free to get started.
Starting with Infisical is simple, fast, and free.