Secure an App Using a Multi-Stage Build
How to build more secure apps using a dev image for the builder stage in a multi-stage build
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.
-
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.5 python:3.13.5-dev Compressed size 22 MB 218 MB SBOM packages 23 73
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.
Create Docker Compose file
Create a project directory and save the code below to a new docker-compose.yml
file:
Create a Python project
Create a subdirectory and name it app
. Add the following to it:
-
Save the following sample script as a new
main.py
file. -
Save a
requirements.txt
file to list the Python packages that the project depends on. Python’s default package installerpip
uses it. For our simple example, save only:
Create the Dockerfile
In the same directory, save the code below to a new Dockerfile
:
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
.
Review the project directory
Your project directory should now look like this:
Authenticate to the Minimus registry
Before building the app, authenticate to the Minimus registry using your Minimus token:
Build the app
You are now ready to build the app using Docker Compose:
Docker Compose will build the app from the app
folder. Once built, you will see a confirmation:
Run and test the app
Run the app:
Test the app by sending it a request:
You should get a response with the current date and time.
Clean up
Once ready to clean up, run the following command to remove the container:
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:
- The Dotnet SDK image is often used as the builder with the ASP.NET image for the runtime stage.
- OpenJDK is often used for building Java applications while OpenJRE (open-source Java Runtime Environment) is used to run them. See our quick start tutorial