Inline Buildpacks: Creating Docker Images the Easy Way
Using buildpacks can be as easy as dropping a project.toml
into your app, and adding some custom build logic. But unlike a Dockerfile
, the resulting image can benefit from powerful features like rebase and advanced caching.
In this post, you’ll learn how to use a simple inline buildpack to build a Docker image for a Python app. Unlike other examples that use off-the-shelf buildpacks from Heroku or Paketo, this tutorial will rely only on your custom inline buildpack.
Getting your app
Let’s start with a simple Python app. You can use your own, or clone my example repo:
git clone https://github.com/jkutner/python-inline-buildpack
Move into the app directory, and you’re ready.
Creating your buildpack
Create a project.toml
in the root directory of your app, and put the following code in it:
[_]
schema-version = "0.2"
[io.buildpacks]
builder = "heroku/builder:24"
[[io.buildpacks.group]]
id = "uv-buildpack"
[io.buildpacks.group.script]
api = "0.10"
inline = """#!/bin/bash
curl -LsSf https://astral.sh/uv/install.sh | sh
cp $HOME/.local/bin/uv .
./uv sync
"""
Before you build an image, take a look at what this script is doing. First, it defines the builder image you’ll use, heroku/builder:24
, which contains several buildpacks you could have used instead of your own custom buildpack. But that’s not the goal of this tutorial, so you’re override those Heroku buildpacks.
Your custom buildpack is defined in the io.buildpacks.group
table. The most important part is the inline
key, which defines the script that will build your app (this is equivilent to a Dockerfile
in some sense). The inline
script installs uv
with curl
, saves uv
to the workspace directory so that it’s available at runtime, and then uses uv
to install your dependencies.
Great! Now you can build your image.
Building an image
Install the Pack CLI and run the following to create a Docker image from your repo:
pack build my-py-app
When this has finished, you’ll be able to run the image with this command:
docker run -p 8080:8080 py-test ./uv run src/python_app.py
Now you can view your app running at http://localhost:8080
.
That was pretty simple, but you might wonder why this is better than just creating an equivilent Dockerfile
and running docker build
. Well, let’s discuss that.
Why is this better?
The code in the project.toml
you created is comparable to a Dockerfile
, but it provides all of the powerful features of buildpacks including:
- Rebasing the image (i.e. updating the base image without rebuilding)
- Advanced caching mechanisms that can improve build performance
- Composibility with off-the-shelf buildpacks (you can mix your custom buildpack in with other builpacks)
- Reproduces the same app image digest by re-running the build
These are all standard buildpack features that you can learn more about at https://buildpacks.io/.
Advanced configuration
The inline buildpack you created earlier is pretty rudimentary. It doesn’t cache uv
or the .venv
dir it creates. That’s a bit of a disadvantage over Dockerfile
on the surface, but the buildpack interface offers you much more advanced caching mechanisms.
For example, you could add the following to the end of the script (instead of copying the uv
binary to the working directory):
uv_layer="$1/uv"
mkdir -p $uv_layer/bin
cp $HOME/.local/bin/uv $uv_layer/bin/uv
echo "[types]" >> "${uv_layer}.toml"
echo "launch = true" >> "${uv_layer}.toml"
This creates a new layer called "uv"
and defines it’s layer metadata. Once you’ve created this layer, you can customize how it’s cached, and if it should be visible to other buildpacks.
Now you have all the power of Cloud Native Buildpacks at your finger tips, and you didn’t even need to create a full-blown buildpack! The down-side is that you can’t publish and share this buildpack, but that might be ok if it’s very specific to your app. The example here is pretty useful in general, but there are other times where you may just want to run a script that’s included with your app.
To learn more, see the official documentation on inline buildpacks.