How Buildpacks Can Help
Docker (container) images are a great option to ensure your application is portable across different systems. Your image can run on your computer, on bare metal, in the cloud (AWS, Azure, GCP, etc.), and even on newer platforms like render.com or fly.io. A Docker image can be produced by building from a Dockerfile.
Writing a Dockerfile from scratch is certainly feasible and offers great flexibility for customizing your image to meet your needs, especially when specific dependencies are required for your runtime.
However, writing a Dockerfile can be a chore, much like any other code:
- Ensuring your images are secure and auditable
- Optimizing build time by reusing layers
- Avoiding repetition when you have multiple similar projects to build. Updating them one by one is no small task.
Buildpacks offer an interesting alternative to writing your own Dockerfile. Buildpacks can help if:
- You don’t want to maintain Dockerfile(s), which come with their own maintenance efforts
- You want to maintain consistent and easy-to-update images across your different projects, especially in a microservices setup
All the features and comparisons with Docker can be found here. However, for the purpose of this article, we will focus on how Buildpacks can package your Docker image with minimal effort.
Configure Buildpacks
First, we configure the builder for Buildpacks. This step can be done once and should be good enough for most purposes:
pack config default-builder paketobuildpacks/builder-jammy-base
If you are interested in knowing other available builders, you can run:
pack builder suggest
Build Docker Image
Now comes the interesting part. For practical purposes, the only thing you need to provide to Buildpack is how you are going to run your application. This can be done by adding a file named Procfile at the root of your project. Here are some examples for different kinds of projects. I often read the port from the PORT
environment variable to make it flexible, but it is up to you.
Procfile for Python with FastAPI and Uvicorn:
web: uvicorn my_project.main:app --host=0.0.0.0 --port=${PORT}
Procfile for Java with Spring Boot:
web: java -jar path/to/your-app.jar --server.port=${PORT}
Procfile for Node.js:
web: node app.js
Once you have the Procfile, building your image is as simple as running:
pack build "<your-image-name:version>"
Then you can find that image by running:
docker image ls
You can then push it to your container registry just like any regular image.
One thing you might have noticed is that we didn’t mention any build tools (NPM, Yarn, Pip, Poetry, Maven, Gradle, etc.) that you are using. This is because Pack can detect the presence of build configurations in your project and automatically adjust to your build tool. It’s pretty magical, right?
Going Further
As an application developer, you might not need to go beyond the basics described above. But what if you do, especially if you work in a DevOps or platform team providing a consistent developer experience to other teams? You might need to use a private base image or a language/framework not yet supported by Buildpacks.
Well, every component in Buildpacks is customizable, from the Buildpack itself to the Builder (the one you configured at the beginning). You can learn more about these topics in the documentation:
Conclusion
Buildpacks offer an interesting option to reduce maintenance effort on your Docker images and ensure they are consistent, secure, and easy to update. They are easy to use for application developers and provide many options for customization for DevOps/platform teams.
The only inconvenience is that the documentation can be a bit rough, which may be why Buildpacks are somewhat underappreciated. To be honest, I didn’t initially understand the value they bring, but once I tried the tutorial, everything became clear. I believe Buildpacks deserve more attention than they currently receive.
To get started:
- If you are an application developer, follow this guide
- If you work in DevOps/platform teams, have a look at this guide