Dockerfile Multi-Stage Build Analyzer

Get the stage graph for a multi-stage Dockerfile: which stages reach the final image, which are dead, and whether every COPY --from resolves to a stage that exists above it.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Analyze. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

A misspelled stage name

Docker reads the target as an image and fails at the pull, so the error mentions the registry rather than the typo

FROM golang:1.22 AS builder
RUN go build -o /app .

FROM alpine
COPY --from=bulider /app /app

A stage nothing uses

BuildKit skips it, so it is dead weight in the file rather than wasted build time

FROM alpine AS used
RUN touch /a

FROM alpine AS orphan
RUN touch /b

FROM alpine
COPY --from=used /a /a

A build token in the builder stage

It is not in the final image and it is in the build cache, and --target on that stage hands it over

FROM alpine AS builder
ENV NPM_TOKEN=REPLACE_ME
RUN npm ci

FROM alpine
COPY --from=builder /app /app

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Debugging a misspelled stage name as a registry problem

    Docker reads an unrecognised --from target as an image reference, so the error is a failed pull. The credentials are fine and the stage name is wrong.

    Instead:Check the --from target against the AS aliases in the file before touching anything to do with the registry.

  2. Assuming a discarded stage makes a build secret safe

    The value is genuinely absent from the final image and genuinely present in the build cache layer, and --target on that stage produces a runnable image containing it.

    Instead:Use RUN --mount=type=secret. It keeps the value out of every layer rather than relying on the layer being thrown away.

  3. Deleting an unused stage without checking CI

    A stage nothing copies from may still be the --target of a test or lint job, which no part of the Dockerfile records.

    Instead:Grep the pipeline for --target before deleting, and leave a comment on any stage that exists only for CI.

A bad stage name produces a registry error, not a typo report

That one behaviour is why a graph is more useful here than a rule list. Docker cannot tell a misspelled stage from an image it has not heard of, so it tries to pull it.

An unrecognised --from is treated as an image

There is no separate namespace for stage names. COPY --from=bulider is a valid instruction referring to an image called bulider, so the build fails at the pull with an authentication or not-found error. People then check their registry credentials, which are fine. The fix is a spelling correction three lines up.

A stage can only copy from one above it

File order is the order, and it is not inferred from the dependency graph. Referring to a stage defined lower down fails, and so does a stage copying from itself, which happens when two aliases get transposed during a refactor.

An unused stage costs nothing with BuildKit and is not free without it

BuildKit builds only what the target needs, so a dead stage is skipped. The classic builder walks the file and builds all of them. Since BuildKit has been the default for a while this is mostly a readability problem now, with one exception: if CI builds with --target set past the dead stage, it does get built, so deleting it breaks a pipeline nobody reading this file can see.

A secret in a discarded stage is not in the image, and is not safe either

This is worth being precise about, because the loose version of the advice is wrong. A credential set in a builder stage genuinely does not ship in the final image, so docker history on the pushed image will not show it. It is written into the build cache layer, and anybody who runs the build with --target pointed at that stage gets a runnable image containing it. RUN --mount=type=secret keeps it out of any layer at all, which is the difference that matters.

Two stages with the same alias is accepted

Docker does not reject it. A COPY --from naming that alias resolves to one of them, the build succeeds, and the wrong artefact is in the image. This is the failure mode with no error message attached, which is why it is worth checking for rather than waiting to notice.

What this cannot see

Whether the build works, whether the copied paths exist, or how large the result is. It reads the text you paste, in your browser, and never runs Docker. Anything that looks like a credential is reported by key name with the value redacted.