From the team behind aidrop.it — one workspace to build, host, and keep changing your code.

The deploy needs an API key, so the key goes where the build can see it: a build argument, an ENV line in the Dockerfile, a .env copied in with the rest of the source. It works on the first try.

It also puts the credential into the image, where it stays in every copy, and into the one step that runs code you did not write.

A build should be able to run with no secrets at all. If yours cannot, either something that belongs at runtime has moved to build time, or the credential is now part of the artefact. Both are fixable, and whether you are done is settled by two commands against the image, at the end of this article.

Three ways a secret ends up in the image

A credential reaches an image through a build argument, an ENV line, or a file copied in. All three survive: arguments and ENV values sit in layer metadata that docker history prints for anyone holding the image, and a copied file is in its layer whether or not a later step deletes it. Removing the running container removes none of them.

  • ARG and --build-arg are recorded in the layer that used them; they were designed for versions and flags.
  • ENV is part of the image configuration, visible to docker inspect and inherited by every container started from it.
  • COPY . . with a .env on disk ships the file; a delete in the next line adds a layer that hides it and removes nothing.

This is the same problem as a credential committed to a repository: the copy you can see is not the one that matters, and the only honest response is to rotate.

The build runs code you did not write

A build executes whatever the repository and its dependencies ask for — install hooks, build scripts, framework plugins. Anything the build step can read, every one of those can read too. So the question is not whether your Dockerfile is careful; it is what a compromised dependency would find in the environment it ran in. The right answer is nothing.

That is why the better-designed builders split the work: the source is fetched in one step, the repository's own build runs in a second step that holds no credential, and the result is published in a third. A bad dependency can produce a bad image for its own project. It cannot publish one for anybody else's, because it cannot publish at all.

Public prefixes are public

NEXT_PUBLIC_, VITE_ and REACT_APP_ mean the value is inlined into the JavaScript sent to every browser. That is what the prefix is for. A key carrying it is not leaked by accident; it is published by design, and no server-side check can call it back. Keep the prefix for what the browser is meant to see: a public URL, a publishable key, a feature flag.

The mistake usually arrives with a framework that reads the variable at build time and bundles the result — the same class of assumption as reading PORT at import time in a module that gets bundled: what was captured is whatever the build machine had, and the value at start-up is never consulted.

What is actually build-time, and what is not

Build time is for what the source needs to become an artefact: a compiler, a package registry, the code. Runtime is for what the artefact needs in order to serve: a database URL, a third-party key, the port. The image should be identical for development and production; only what is injected at start differs.

Belongs at build Belongs at runtime
Installing dependencies DATABASE_URL and every connection string
Compiling, bundling, static assets Third-party API keys
A private registry token, for one command PORT, the public origin, session secrets

The one legitimate exception is the private registry token, and it has its own mechanism: RUN --mount=type=secret,id=npm_token npm ci exposes the value to that command as a file and writes it into no layer. A short-lived token is better still, because the mount protects the image and not the log.

Two commands that settle it

Run both against the image you are about to ship. Both print nothing on a clean build, and anything they print is a finding.

IMAGE=your-image:latest

# 1. Anything in a layer or its metadata: build arguments, ENV lines, copied files.
docker history --no-trunc "$IMAGE" | grep -iE 'secret|token|password|api[_-]?key|\.env'

# 2. Anything the build inlined into what browsers will download.
grep -rEo '(sk_live|sk_test|AKIA|ghp_|xox[bp]-)[A-Za-z0-9_-]{8,}' dist .next/static build 2>/dev/null

The third proof is the image starting with nothing: docker run --rm "$IMAGE" env should show no credential, and the same image should then serve once the values are injected at start. An image that only works when built with the secrets is bound to one environment — the kind of thing a preflight should say before the build spends your time.

FAQ

Are Docker build arguments secret? No. A --build-arg value is recorded in the metadata of the layer that used it and printed by docker history. Use them for versions and flags. For a value that must stay private during one command, use a secret mount instead.

How do I use a private npm or pip token during a Docker build? With a BuildKit secret mount: RUN --mount=type=secret,id=npm_token npm ci, passed with --secret at build time. The command sees a file; no layer records it.

Is NEXT_PUBLIC_ safe for an API key? Only for a key that is meant to be public, such as a publishable payment key or an analytics id. Anything with the prefix is inlined into the client bundle and readable by every visitor.

Does deleting a file in a later Dockerfile step remove it from the image? No. The delete adds a layer that hides the file from the final filesystem; the layer that copied it is still in the image. Keep the file out with .dockerignore instead.

How does aidrop.it handle secrets at build time? The step that runs your repository's build holds no credential at all: the source is cloned and the image is pushed in separate steps that run none of your code. Runtime values are entered by a person, never returned, and decrypted only for a deploy, so they reach the running container and never the build.


Cover: https://commons.wikimedia.org/wiki/File:Constructions_and_scaffolding_at_the_Oosterdokseiland,_Amsterdam-Centrum.jpg by Fons Heijnsbroek, CC0, via Wikimedia Commons. Cropped to 16:9 within the original frame. Alt text: A building site under scaffolding, with cranes overhead and materials stacked on the wet ground below.

Before the next deploy

Get it running from the session you are already in

Connect your coding agent over MCP and ask it to deploy. aidrop builds the repository and runs it on a public address; when a build or the app fails, the log says why, and your agent fixes it and builds again.