Skip to content

Docker Multi-Stage Builds

Back to Docker Tutorials


Observe a Bloated Single-Stage Build

A common mistake is installing build tools in the same image that runs the application. Build tools like compilers and test frameworks are only needed during the build — shipping them in production images wastes disk space and increases the attack surface.

graph TD
    SRC["📄 Source Code
+ Build Tools
(gcc, go, npm...)"]-->|single RUN|IMG["📀 Fat Image
(build tools INCLUDED)"]
    IMG-->|docker run|CNT["📦 Container
(carries unused build tools)"]

    style SRC fill:#f3f4f6,stroke:#9ca3af,stroke-width:2px,color:#1f2937
    style IMG fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d
    style CNT fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d

Write a single-stage Dockerfile.

cat > Dockerfile.single << 'EOF'
FROM golang:1.22-alpine
WORKDIR /app
RUN echo 'package main' > main.go && \
    echo 'import "fmt"' >> main.go && \
    echo 'func main() { fmt.Println("Hello") }' >> main.go
RUN go build -o app main.go
CMD ["./app"]
EOF

Build it by running docker build -t go-single -f Dockerfile.single .

docker build -t go-single -f Dockerfile.single .
[+] Building 3.5s (8/8) FINISHED                                docker:default
...
 => => naming to docker.io/library/go-single                              0.0s

Check its size by running docker images go-single.

docker images go-single
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
go-single    latest    1a2b3c4d5e6f   15 seconds ago   256MB

Build and Compare a Multi-Stage Image

A multi-stage build uses multiple FROM instructions in a single Dockerfile. Each stage is independent. The AS keyword names a stage. COPY --from=STAGE copies artifacts from one stage into another without carrying over the build environment.

graph TD
    SRC["📄 Source Code"]-->|Stage 1: builder
 go build|BIN["⚙️ Binary
(compiled)"]
    BIN-->|COPY --from=builder|RT["📀 Runtime Image
(alpine only)"]
    RT-->|docker run|CNT["📦 Container
(lean, no build tools)"]

    style SRC fill:#f3f4f6,stroke:#9ca3af,stroke-width:2px,color:#1f2937
    style BIN fill:#ffedd5,stroke:#f59e0b,stroke-width:2px,color:#78350f
    style RT fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
    style CNT fill:#dcfce7,stroke:#22c55e,stroke-width:2px,color:#14532d

Write the multi-stage Dockerfile.

cat > Dockerfile << 'EOF'
# --- Stage 1: Build ---
FROM golang:1.22-alpine AS builder
WORKDIR /app
RUN echo 'package main' > main.go && \
    echo 'import "fmt"' >> main.go && \
    echo 'func main() { fmt.Println("Hello") }' >> main.go
RUN go build -o app main.go

# --- Stage 2: Runtime ---
FROM alpine:3.22
WORKDIR /app
COPY --from=builder /app/app .
CMD ["./app"]
EOF

Build the multi-stage image.

docker build -t go-multi .
[+] Building 4.2s (10/10) FINISHED                              docker:default
...
 => => naming to docker.io/library/go-multi                               0.0s

Run it to verify it works.

docker run --rm go-multi
Hello

Finally, run docker images | grep -E "go-single|go-multi" to compare both image sizes side by side.

docker images | grep -E "go-single|go-multi"
go-single         latest    1a2b3c4d5e6f   2 minutes ago    256MB
go-multi          latest    f1g2h3i4j5k6   15 seconds ago   9.2MB

Observe that go-multi is dramatically smaller because the Go toolchain (golang:1.22-alpine) is not present in the final image — only the compiled binary was copied over.


Build a Specific Stage

docker build --target stops the build at a named stage. This is useful for debugging the build environment without producing the full runtime image.

Build only the builder stage.

docker build --target builder -t go-builder-only .
[+] Building 0.2s (7/7) FINISHED                                docker:default
...
 => => naming to docker.io/library/go-builder-only                        0.0s

Verify the Go compiler is present in this intermediate stage.

docker run --rm go-builder-only go version
go version go1.22.5 linux/amd64

Now, try running the same command on your final production image.

docker run --rm go-multi go version
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "go": executable file not found in $PATH: unknown.

It will fail! This proves that the multi-stage build successfully stripped out the massive build tools before producing the final image.

🧠 Quick Quiz

#

What is the primary benefit of using a multi-stage build?

#

How do you name a specific stage in a multi-stage Dockerfile?

#

Which command is used to retrieve artifacts from a previous build stage?


🐳

Practice Live in Your Browser

Don't just read about Docker commands—execute them in real time! Launch a fully-configured, secure Docker sandbox directly in your browser with automated task validation ready for you.

FREE LAUNCH OFFER Get all premium Docker labs for FREE until June 30th! (No Credit Card Required)

📬 DevopsPilot Weekly — Learn DevOps, Cloud & Gen AI the simple way.
👉 Subscribe here