Buildpacks turn application source code into OCI container images without requiring every team to maintain a Dockerfile. They detect supported applications, supply runtimes and dependencies, produce reusable layers, and expose build metadata such as SBOMs.
This article covers how buildpacks function, what their benefits are as compared to Dockerfiles, and how to use them with various programming languages, including Java, Python, Go, and Node.JS.
How Buildpacks Work
A buildpack is a software that turns the application source code into a runnable production-ready container image. To build a container image of your application, you don’t need to install the runtime, compile the application beforehand, or configure the build environment. The buildpacks will handle everything. But how?
In short, buildpacks detect what kind of application you have and what it needs to build and run. Then, they execute a build using a set of components required for your app. But that is a short story, and the beginner’s tutorial on using buildpacks will be quite short, just two commands. But in truth, there’s quite a lot going on under the hood to make this ‘magic’ happen.
First, we need to distinguish several parts of the concept. A buildpack is not just one universal tool, but a set of software solutions:
- A buildpack knows how to recognize and gather everything an application needs to build and run.
- A builder packages buildpacks, lifecycle tooling, and build/run-image configuration. The builder stack consists of two images: the build image and the run image. The build image provides the build environment, which is a containerized environment where buildpacks are executed, the run image offers the environment for the application image during runtime.
- A buildpacks platform executes the buildpack lifecycle. It supplies source code, buildpacks/builder, cache, registry credentials, environment variables, then invokes the lifecycle and handles the resulting image. In this article, we will use pack CLI as the buildpacks platform.
- A lifecycle coordinates the build and exports an OCI image. It executes the actual CNB build phases:
- Analyze,
- Detect,
- Restore,
- Build,
- Export.
Let’s look at these phases in more detail.
The analyze phase
During the analyze phase, the lifecycle checks whether an earlier version of the target image exists and reads its metadata. It also verifies that it can write the new image to the target registry.
The detect phase
During the detect phase, a group of buildpacks is tested against the source code, and the first group deemed fit for the code is selected for building the app. After the buildpack detects the necessary indicators, it returns a contract of what is required for creating an image and proceeds to the next phase.
The restore phase
During the restore phase, the lifecycle uses the previous-image metadata, cache, and selected buildpack group to restore reusable layers. Restored layers are placed in the layers directory. This way, buildpacks can reuse them instead of downloading dependencies or rebuilding unchanged artifacts.
The build phase
During the build phase, the buildpack transforms the codebase, fulfilling the contract requirements composed earlier.The lifecycle runs the selected buildpacks’ build executables in order, passing each one the part of the build plan relevant to it. Buildpacks create layers for runtimes, dependencies, compiled application output, environment configuration, launch processes, and SBOM data.
The export phase
During the export phase, the lifecycle assembles the final OCI image from the run image, application source, and layers marked for launch. It sets the image entrypoint, writes image metadata, and stores cacheable layers for later builds; cache-only layers stay out of the final runtime image.
Benefits of Buildpacks
Cloud Native Buildpacks take away most of the plumbing development and engineering teams have to go through on a regular basis when working with Dockerfiles.
When dealing with Dockerfiles, developers need to define the whole process of building a container image: choose base images, install the dependencies, compile the application, define the entrypoint and startup commands, ideally, optimize for performance and security.
For platform teams, Dockerfiles pose more of a maintainability problem, when a platform team may need to update dozens of Dockerfiles, each written differently, test them all, and ping teams for merges. Also, standards are harder to enforce in this scenario.
Buildpacks take away most of this burden. Not in a sense that they let you sweep image security, performance, or maintenance under the rug. Those concerns still exist. Instead, buildpacks move common container-building logic into reusable, language-aware components managed centrally. Developers provide application code and configuration, and buildpacks create a production-ready image with best practices of security and efficiency in place. Platform teams can maintain shared builders and buildpacks rather than repairing every repository’s Dockerfile independently.
That brings several advantages.
Enhanced Developer Productivity
A developer can package an application without becoming an expert in Dockerfile syntax, layer ordering, base-image selection, or multi-stage builds.
That matters more for medium-to-large enterprises, when dozens of teams deploy fairly standard services but do not need to customize every OS package or build command. The platform team can maintain a supported builder, while application teams spend their time on application code and configuration rather than reinventing Dockerfile best practices.
Consistent Images Across Teams
Buildpacks apply the same defaults for supported applications: runtime installation, dependency resolution, launch configuration, image layering, and, depending on the builder, non-root execution.
This helps to reduce the usual chaos where every repository has its own Dockerfile dialect, base image, and workarounds. Platform teams can standardize runtimes, build settings, and image policies through the builder instead of reviewing and repairing each service separately.
Better Caching and Faster Rebuilds
Buildpacks split an image into layers for things like the runtime, dependencies, and application code. When only the application changes, dependency layers can often come from cache instead of being downloaded and rebuilt.
That usually speeds up local and CI builds and cuts unnecessary registry storage and network traffic. The exact result depends on the buildpack and the application, but the cache strategy is built into the process rather than left for every Dockerfile author to design from scratch.
Centralized and Fast Runtime Updates
Buildpacks keep application layers separate from the runtime base image. When the run image receives an update, such as an OS security patch, you can replace those base layers without rebuilding the application or downloading its dependencies again.
That process is called rebasing. For a compatible buildpack-built image, rebasing is much faster than a full rebuild. This advantage becomes even more evident when several applications use the same run image and an urgent operating-system patch lands. The application code and layers created during the build stay as they are. Only the run-image layers change.
When a runtime, build dependency, or base image needs an update, the platform team can update the relevant buildpack or builder rather than opening pull requests across every repository.
Language-Aware Builds
Buildpacks understand the conventions of supported ecosystems. They can inspect an application, select the matching buildpacks, install the required runtime, resolve dependencies, compile the code when needed, and configure how the application starts.
For common stacks, this removes a lot of Dockerfile boilerplate. It also makes it less likely that teams use different build patterns.
Customization
A builder is made up of smaller buildpacks. Platform teams can add, remove, replace, or reorder buildpacks to meet their own requirements. They can also create their own buildpack.
That gives teams an optimal middle ground in-between total freedom of Dockerfiles and buildpack-promoted standardization. They are not locked into a completely fixed build process, but they also do not have to maintain a custom Dockerfile for every service. For standard applications, that is often enough flexibility.
Stronger Security Defaults
Buildpacks can make secure choices easier to standardize.
Instead of every team maintaining its own Dockerfile, a platform team can define approved runtimes, base images, package sources, certificates, and user settings in shared builders and buildpacks. Applications containerized with buildpacks also run as non-root by default, which limits the damage if a process is compromised.
Updates become more controlled as well. When a runtime or base image needs a fix, maintainers can update the shared build path instead of asking every team to edit a Dockerfile. Images still need to be rebuilt or rebased, but the work becomes a centralized platform operation.
Buildpacks can also generate SBOMs, making it easier to see what ended up in an image and use that data for scanning, audits, and incident response.
Buildpacks vs Dockerfiles
Buildpacks and Dockerfiles represent two approaches to building application container images. The goal of this article is not to paint Dockerfiles black. In some cases, a Dockerfile might be more suitable for your requirements.
Dockerfiles give you fine-grained control over the build process and thus, ultimate customization. You specify which libraries, dependencies, packages, and configuration are needed for your application, which might be paramount for specific, non-standard scenarios or complex workloads.
Buildpacks take away some of these customization capabilities, but in turn, give you standardization across services, which otherwise is hard to reach and maintain manually.
In summary, the differences between buildpacks and Dockerfiles can be described as follows.
|
Feature |
Dockerfile |
Buildpacks |
|
Build approach |
Explicit |
Automated |
|
Control |
Full control over every layer |
Configuration within supported patterns |
|
Best for |
Custom, legacy, unusual workloads |
Conventional supported applications |
|
Languages |
Any language or stack |
Supported languages and frameworks |
|
Base image |
Chosen and maintained per app |
Defined centrally by the builder/run image |
|
Dependencies |
Installed manually |
Detected and installed automatically |
|
Build commands |
Written and maintained manually |
Supplied by language-aware buildpacks |
|
Caching |
Depends on Dockerfile design |
Dependency-aware layers by default |
|
Image size |
Fully tunable, but manual work |
Depends on builder and buildpacks |
|
Security defaults |
Team must implement them |
Shared defaults, including non-root execution |
|
Runtime updates |
Update Dockerfiles across repositories |
Update shared builder or buildpacks |
|
Rebasing |
Rebuild image after base-image change |
Replace compatible run-image layers |
|
SBOMs |
Add separate tooling |
Generated and attached during the build |
|
Consistency at scale |
Varies between repositories |
Standardized across supported apps |
|
Customization |
Unlimited |
Modular, but bounded by buildpack support |
A Guide to Using Buildpacks
To demonstrate how buildpacks work under the hood, we will use Paketo buildpacks as an example, an open-source project that implements Cloud Native Buildpacks specifications and supports the most popular languages.
We will also use pack CLI as the buildpacks platform.
Prerequisites:
- pack CLI
- Docker
- A functioning application, which can be as simple as “Hello World!”
The setup is the same regardless of the application language.
Install pack CLI for your operating system. For Linux and macOS, you can use Homebrew:
brew install buildpacks/tap/pack
For Windows, you can use Chocolatey or Scoop. For instance, with Scoop:
scoop install pack
You can also run pack in a container. For other installation methods, refer to the documentation.
Make sure that Docker or a Docker-compatible daemon is up and running because pack needs it for running the build process in isolation.
For the application, you can use your existing program, write a simple demo, use samples provided by buildpacks or written for this tutorial. For the latter case, run:
git clone https://github.com/code-with-bellsoft/buildpacks-demo.git
In the following section, I will refer to these samples.
The most basic command for building a container image will look like this:
pack build <image-name> --path <path-to-source> --builder <builder>
Where --path points to the source directory, and --builder specifies a builder. Multiple builders from various providers are available. In this tutorial, I will use one of the standard Paketo builders based on Ubuntu Noble, and the BellSoft’s builder based on minimalistic Alpaquita Linux.
You can inspect the selected candidate locally:
pack builder inspect <builder>
That shows the included buildpacks, lifecycle version, build image, and run image. A builder is not merely “a list of languages.” It bundles the ordered buildpacks plus the build and runtime base images, so the OS stack and architecture support matter too.
Instead of specifying a builder every time, you can set a default builder like this:
pack config default-builder <builder>
Then, the command for building a container image may be as simple as
pack build <image-name> --path <path-to-source>
How to Use Buildpacks with Java
Clone the repo with the samples or prepare your own app.
To build an image from source with the standard Paketo builder, run
pack build hello-java --builder paketobuildpacks/ubuntu-noble-builder --env BP_JVM_VERSION=25 --path buildpacks-demo/hello-java
Alternatively, you can use BellSoft builder with Liberica JDK Lite optimized for cloud and lightweight Alpaquita musl that comes with two libc flavors, optimized musl and glibc. Using this builder may help you reduce the final image size.
To build a Java container image with musl-based Alpaquita, run:
pack build hello-java --builder bellsoft/buildpacks.builder:musl --env BP_JVM_VERSION=25 --path buildpacks-demo/hello-java
To build a Java container image with glibc-based Alpaquita, run:
pack build hello-java --builder bellsoft/buildpacks.builder:glibc --env BP_JVM_VERSION=25 --path buildpacks-demo/hello-java
To run the containerized application with Docker, use the following command:
docker run --rm -p 8080:8080 -e PORT=8080 hello-java
To verify that the application has started successfully and listens on localhost:8080, run:
curl -i http://localhost:8080
HTTP/1.1 200 OK
Date: Tue, 30 Jun 2026 09:34:22 GMT
Content-type: text/plain; charset=utf-8
Content-length: 15
Hello from Java
Popular Java frameworks such as Spring support buildpacks out-of-the-box, so you don’t even have to install the buildpack platform. To learn how to use and configure buildpacks with Spring Boot, see this article.
You can configure the underlying JVM to achieve optimal results for your specific scenario.
If you want to use another Java version, use the BP_JVM_VERSION environment variable. For instance, BP_JVM_VERSION=11 will install the newest release of JDK and JRE 11.
Also, Paketo buildpacks use Liberica JVM as the Java runtime by default. You can select another available runtime, only check that it provides what you need. For instance, some distributions provide only JDK or a limited choice of Java versions.
In addition, you can change the JDK type. The buildpack uses JDK at build-time and JRE at runtime. Specifying the BP_JVM_TYPE=JDK option will force the buildpack to use JDK at runtime.
The BP_JVM_JLINK_ENABLED option runs the jlink tool with Java 9+, which cuts out a custom JRE and helps to reduce the final image size:
pack build hello-java --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-java --env BP_JVM_VERSION=25 -env BP_JVM_JLINK_ENABLED=true
In this case, jlink generates a custom JRE with the following default options: --no-man-pages, --no-header-files, --strip-debug, --compress=1. To change the options, use the BP_JVM_JLINK_ARGS environment variable.
If you deploy a Java application to an application server, the buildpack uses Apache Tomcat by default. You can select another server, TomEE or Open Liberty. For instance, run the following command to switch to TomEE:
pack build hello-java --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-java --env BP_JVM_VERSION=25 -env BP_JAVA_APP_SERVER=tomee
The whole list of JVM configuration options can be found in the documentation for the respective buildpack.
How to Use Buildpacks with Python
Clone the repo with the samples or prepare your own app.
To build an image from source with the standard Paketo builder, run
pack build hello-python --builder paketobuildpacks/ubuntu-noble-builder --path buildpacks-demo/hello-python
To run the containerized application with Docker, use the following command:
docker run --rm -p 8080:8080 -e PORT=8080 hello-python
To verify that the application has started successfully and listens on localhost:8080, run:
curl -i http://localhost:8080
HTTP/1.1 200 OK
Date: Tue, 30 Jun 2026 09:34:22 GMT
Content-type: text/plain; charset=utf-8
Content-length: 15
Hello from Python
You can customize the build using the available options. For instance, you can make the buildpack install the watchexec tool that watches for file changes and run a command when it detects modifications. For that, set the BP_LIVE_RELOAD_ENABLED to true:
pack build hello-python --builder paketobuildpacks/ubuntu-noble-builder --path buildpacks-demo/hello-python --env BP_LIVE_RELOAD_ENABLED=true
For more information about containerizing Python applications and available options, see the documentation.
How to Use Buildpacks with Go
Clone the repo with the samples or prepare your own app.
To build an image from source with the standard Paketo builder, run
pack build hello-go --builder paketobuildpacks/ubuntu-noble-builder --path buildpacks-demo/hello-go
Alternatively, you can use BellSoft builder with lightweight Alpaquita musl that comes with two libc flavors, optimized musl and glibc.
To build a Go application locally with the musl-based BellSoft builder, run the following command:
pack build hello-go --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-go
To build a Go container image with glibc-based Alpaquita, run:
pack build hello-go --builder bellsoft/buildpacks.builder:glibc --path buildpacks-demo/hello-go
To run the containerized application with Docker, use the following command:
docker run --rm -p 8080:8080 -e PORT=8080 hello-go
To verify that the application has started successfully and listens on localhost:8080, run:
curl -i http://localhost:8080
HTTP/1.1 200 OK
Date: Tue, 30 Jun 2026 09:34:22 GMT
Content-type: text/plain; charset=utf-8
Content-length: 15
Hello from Go
You can customize the build using the available options. For instance, you can enable restarting the binary process when files in the application working directory change. For that, set the BP_LIVE_RELOAD_ENABLED to true:
pack build hello-go --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-go --env BP_LIVE_RELOAD_ENABLED=true
For more information about containerizing Go applications and available options, see the documentation.
How to Use Buildpacks with Node.js
Clone the repo with the samples or prepare your own app.
To build an image from source with the standard Paketo builder, run
pack build hello-node --builder paketobuildpacks/ubuntu-noble-builder --path buildpacks-demo/hello-node
Alternatively, you can use BellSoft builder with lightweight Alpaquita musl that comes with two libc flavors, optimized musl and glibc. Using this builder may help you reduce the final image size.
To build a Node.js application locally with the musl-based BellSoft builder, run the following command:
pack build hello-python --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-node
To build a Node.js container image with glibc-based Alpaquita, run:
pack build hello-node --builder bellsoft/buildpacks.builder:glibc --path buildpacks-demo/hello-node
To run the containerized application with Docker, use the following command:
docker run --rm -p 8080:8080 -e PORT=8080 hello-node
To verify that the application has started successfully and listens on localhost:8080, run:
curl -i http://localhost:8080
HTTP/1.1 200 OK
Date: Tue, 30 Jun 2026 09:34:22 GMT
Content-type: text/plain; charset=utf-8
Content-length: 15
Hello from Node.js
You can customize the build using the available options. For instance, you can enable heap memory optimization. For that, set the BP_NODE_OPTIMIZE_MEMORY option to true:
pack build hello-python --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-node --env BP_NODE_OPTIMIZE_MEMORY=true
For more information about containerizing Node.js applications and available options, see the documentation.
Retrieving a Software Bill of Materials
Software supply chains consist of numerous libraries, tools, and processes used to develop and run applications. A software bill of materials (SBOM) lists all library dependencies utilized to build a software artifact. SBOMs enable the developers to monitor the version of software components, integrate security patches promptly, and keep vulnerable libraries out.
When you use buildpacks, an SBOM for your container image is generated by default, so you don’t have to use additional tools or specify the command explicitly.
Buildpacks can generate SBOMs in different formats, including Syft, SPDX, and CycloneDX JSON formats.
You can use the pack CLI to retrieve an SBOM for a specified image:
pack sbom download <image-name> --output-dir /tmp/demo-app-sbom
The SBOM files will be in the specified output directory. You can also download an SBOM of an image in the remote registry using the --remote option.
Conclusion
Buildpacks give teams a standardized way to turn source code into production-ready OCI images without maintaining a Dockerfile for every application. They handle common work such as runtime selection, dependency installation, image layering, sensible security defaults, SBOM generation, caching, and base-image updates through rebasing.
Although applications with unusual operating-system requirements, custom build flows, or unsupported tooling may still need the control a Dockerfile provides, for conventional services, buildpacks can remove a large amount of repeated container work while giving platform teams a more consistent, maintainable, and secure build path.






