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
(pip, gcc, make...)"]-->|pip install|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

First, create a simple Python application using Flask.

[labuser@container ~]$ cat > app.py << 'EOF'
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)
EOF

Write a single-stage Dockerfile.

[labuser@container ~]$ cat > Dockerfile.single << 'EOF'
FROM python:3.12
WORKDIR /app
COPY app.py .
RUN pip install flask
CMD ["python", "app.py"]
EOF

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

[labuser@container ~]$ docker build -t python-single -f Dockerfile.single .
[+] Building 15.2s (8/8) FINISHED                               docker:default
...
 => => naming to docker.io/library/python-single                          0.0s

Check its size by running docker images python-single.

[labuser@container ~]$ docker images python-single
REPOSITORY       TAG       IMAGE ID       CREATED          SIZE
python-single    latest    1a2b3c4d5e6f   15 seconds ago   1.02GB

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
 pip install|BIN["⚙️ Dependencies
(installed)"]
    BIN-->|COPY --from=builder|RT["📀 Runtime Image
(slim 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.

[labuser@container ~]$ cat > Dockerfile << 'EOF'
# --- Stage 1: Build ---
FROM python:3.12 AS builder
WORKDIR /build
RUN pip install --no-cache-dir --target=/install flask

# --- Stage 2: Runtime ---
FROM python:3.12-slim
ENV PYTHONPATH=/install
WORKDIR /app
COPY --from=builder /install /install
COPY app.py .
CMD ["python", "app.py"]
EOF

Build the multi-stage image.

[labuser@container ~]$ docker build -t python-multi .
[+] Building 6.2s (10/10) FINISHED                              docker:default
...
 => => naming to docker.io/library/python-multi                           0.0s

Run it to verify it works in the background and test it.

[labuser@container ~]$ docker run -d -p 8080:8080 --name test-app python-multi
[labuser@container ~]$ sleep 2
[labuser@container ~]$ curl localhost:8080
[labuser@container ~]$ docker stop test-app && docker rm test-app

Hello

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

[labuser@container ~]$ docker images | grep -E "python-single|python-multi"
python-single         latest    1a2b3c4d5e6f   2 minutes ago    1.02GB
python-multi          latest    f1g2h3i4j5k6   15 seconds ago   165MB

Observe that python-multi is dramatically smaller because the heavy Python build tools (python:3.12) are not present in the final image — only the installed dependencies and application code were copied over into the lightweight python:3.12-slim image.


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.

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

Verify the pip package manager is present in this intermediate stage.

[labuser@container ~]$ docker run --rm python-builder-only pip --version
pip 24.0 from /usr/local/lib/python3.12/site-packages/pip (python 3.12)

Now, try running a build tool like gcc (often required for compiling python packages) on your intermediate builder image.

[labuser@container ~]$ docker run --rm python-builder-only gcc --version
gcc (Debian 12.2.0-14) 12.2.0

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

[labuser@container ~]$ docker run --rm python-multi gcc --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: "gcc": 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. Practice commands, debug broken containers, and complete timed challenges with instant task validation.

FREE LAUNCH OFFER Get all premium Docker labs for FREE until July 31st! (No Credit Card Required)

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