Minimus images come in pairs — the fully distroless image with the smallest attack surface and a dev variant that includes more developer-relevant tools such as package managers, a shell, and more.

When you build an app using a multi-stage build, you can take advantage of an image pair to create a more secure final result. In the Dockerfile, you can use the dev image in the builder stage and switch to the production image for the final stage. This way, the build process can discard all the unnecessary tools and produce a cleaner, more minimal and secure artifact.

If your app can run on a runtime base after it’s compiled, you can make the app even smaller and more secure. Go (golang) is a great example. See our article on how to use a runtime base.

Advantages of a multi-stage build

Using a different base image for the builder stage and the runtime stage has several advantages:

  • The dev image contains more packages and is therefore more likely to contain more vulnerabilities. For example, we can see that the Python production image has fewer vulnerabilities than the Python dev image.

    This behavior is consistent across all images, where many times the production image is completely free of vulnerabilities but the dev image might have a few.

    Compare Vulnerabilities

  • When building an app, there is no advantage to including dev packages that increase the attack surface and are not required to run the app. It is preferable to build the application in stages and reduce the final image in size and attack surface. As a result, the final image will have a small attack surface and fewer vulnerabilities, if any.

    Just to get a sense of the size differences, the compressed size of the python image is just over 22 MB - compared to 218 MB for the python dev image. Similarly, the SBOM for the Python production image lists 23 packages while the Python dev image lists 73 packages.

    python:3.13.5python:3.13.5-dev
    Compressed size22 MB218 MB
    SBOM packages2373

Python example

In this example, we will build a custom Python app using Docker Compose and a multi-stage Dockerfile. The build stage uses latest-dev because it requires the package installer pip. The runtime stage uses the fully distroless image to achieve the most secure app.

1

Create Docker Compose file

Create a project directory and save the code below to a new docker-compose.yml file:

services:
  flask-app:
    build: ./app
    ports:
      - "5000:5000"
2

Create a Python project

Create a subdirectory and name it app. Add the following to it:

  • Save the following sample script as a newmain.py file.

    from flask import Flask
    from datetime import datetime
    
    app = Flask(__name__)
    
    @app.route("/")
    def show_time():
        now = datetime.now()
        return f"Current date and time: {now.strftime('%Y-%m-%d %H:%M:%S')}"
    
    if __name__ == "__main__":
        app.run(host='0.0.0.0', port=5000)
    
  • Save a requirements.txt file to list the Python packages that the project depends on. Python’s default package installer pip uses it. For our simple example, save only:

    flask>=3.1.1
    
3

Create the Dockerfile

In the same directory, save the code below to a new Dockerfile:

# === Build Stage ===
FROM reg.mini.dev/python:latest-dev as builder

WORKDIR /app

RUN python -m venv venv
ENV PATH="/app/venv/bin":$PATH
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

# === Runtime Stage ===
FROM reg.mini.dev/python:latest

WORKDIR /app

COPY main.py main.py
COPY --from=builder /app/venv /app/venv
ENV PATH="/app/venv/bin:$PATH"

ENTRYPOINT ["python", "main.py"]

Note that the builder stage uses reg.mini.dev/python:latest-dev so it can utilize PIP. The runtime stage uses the fully distroless production image - reg.mini.dev/python:latest .

4

Review the project directory

Your project directory should now look like this:

your-project-root/
├── docker-compose.yml
└── app/
    ├── Dockerfile
    ├── main.py
    └── requirements.txt
5

Authenticate to the Minimus registry

Before building the app, authenticate to the Minimus registry using your Minimus token:

docker login reg.mini.dev -u minimus -p {pull-token}
6

Build the app

You are now ready to build the app using Docker Compose:

docker compose build

Docker Compose will build the app from the app folder. Once built, you will see a confirmation:

✔ flask-app  Built
7

Run and test the app

Run the app:

docker compose up

Test the app by sending it a request:

curl http://127.0.0.1:5000

You should get a response with the current date and time.

8

Clean up

Once ready to clean up, run the following command to remove the container:

docker compose down

When to use a multi-stage build

Basically, you should use a multi-stage build whenever the opportunity presents itself.

  • Any compiled or interpreted language is a good candidate for a multi-stage build. This includes (but is not limited to): Python, NodeJS, Rust, PHP, Ruby, etc.
  • Go is a special case as you can run the compiled binary on a minimal runtime base. Learn more
  • Some images come in building pairs that have different names. For example: