Nearly two years ago, we shared a blog post on building custom container images for CloudNativePG. Since then, the container ecosystem has evolved significantly—one notable development being the introduction of Docker Bake.
Docker Bake simplifies image builds using a straightforward configuration file, and it’s now our recommended approach for building CloudNativePG images.
In this post, we’ll walk through a simple baking recipe to create a custom container image. With Bake, you can also easily build multiple images in parallel.
To build a custom image we add the following content in a local file with name bake.hcl
:
extensions = [
"pgvector",
]
target "myimage" {
dockerfile-inline = <<EOT
ARG BASE_IMAGE="ghcr.io/cloudnative-pg/postgresql:16.9-standard-bookworm"
FROM $BAS_EIMAGE AS myimage
ARG EXTENSIONS
USER root
RUN apt-get update && \
apt-get install -y --no-install-recommends $EXTENSIONS && \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \
rm -rf /var/lib/apt/lists/* /var/cache/* /var/log/*
USER 26
EOT
matrix = {
tgt = [
"myimage"
]
pgVersion = [
"16.9",
"17.5",
]
}
name = "postgresql-${index(split(".",cleanVersion(pgVersion)),0)}-standard-bookworm"
target = "${tgt}"
args = {
BASE_IMAGE = "ghcr.io/cloudnative-pg/postgresql:${cleanVersion(pgVersion)}-standard-bookworm",
EXTENSIONS = "${getExtensionsString(pgVersion, extensions)}",
}
}
There are a few important points to highlight:
extensions
variable is a list of extensions that we want to include in the image. In our recipe we are using
pgvector
, but you can add others as needed.dockerfile-inline
variable contains our Dockerfile definition, which cannot be used remotely. We will explain
why later.target
and the tgt
values share the same name, but you can use any name you prefer.pgVersion
variable is a list specifying the PostgreSQL version(s) in MAJOR.MINOR format.name
field is used to identify individual entries in the matrix we’ve defined.args
variable contains the arguments passed to the Dockerfile—more on this later.getExtensionsString()
function is inherited from the base Bake file mentioned in the Ingredients sectionWe can now build the image using the following command:
docker buildx bake -f docker-bake.hcl -f cwd://bake.hcl "https://github.com/cloudnative-pg/postgres-containers.git" myimage
This will build the image for the bake matrix we previously created, and will try to push the image to the registry at
localhost:5000
, which is the default registry defined for testing environments in the parent Bake file. Let’s explain
the full command:
As outlined in the Bake documentation on remote definitions, you can use a remote Bake file that includes functions and default targets, then attach a local Bake file to override any default values as needed.
In the command above, -f cwd://bake.hcl
is the local file that we created in Step 1, and
-f docker-bake.hcl
is the remote file in the git repo, that we’re using to build the image.
You can explore more about all the content generated and used inside the Bake file by appending the --print
flag, as
in the following command:
docker buildx bake -f docker-bake.hcl -f cwd://bake.hcl "https://github.com/cloudnative-pg/postgres-containers.git" myimage --print
Now you just need to push the image to a registry. You can do this by using the following command:
registry=your/registry:5000 docker buildx bake -f docker-bake.hcl -f cwd://bake.hcl "https://github.com/cloudnative-pg/postgres-containers.git" myimage --push
The previous command will push the images in the following format: your/registry:5000/postgresql-testing:17-standard-bookworm
.
Using the --print
flag you can explore the full list of tags created that are in the parent Bake file.
You can now use the image that we’ve built for your clusters.
The simplicity of Bake to do even more stuff is amazing, and allows you to create custom images easily.
The magic starts with our postgres-containers repository,
where we have a docker-bake.hcl
file that is being used to build the images for the CloudNativePG project.
It’s the base for our custom Bake file.
The docker-bake.hcl
file contains a lot of functions that are used to build the images. One of them is the getExtensionsString()
.
This function, given the list of extensions we provided, will return a string of the extensions with the correct package name
for a Debian-based distribution, in our case, Debian Bookworm.
For example, the pgvector
extension will be translated into
postgresql-16-pgvector
, which is the name of the package for pgvector extensions for PostgreSQL 16 in the Debian
Bookworm distribution.
When we add elements to, for example, the args
variable, those elements are processed by the Docker bake command, and will be
merged, meaning that the new elements will be added, and the existing ones will be overwritten.
The Dockerfile is defined as a heredoc string due to Bake’s limitation in overriding a remote Dockerfile with a local one. However, this approach still lets us modify the FROM directive, allowing us to base our image directly on the CloudNativePG images and add only the extensions we need—without rebuilding everything.
By default, images are built for both amd64
and arm64
architectures, which is the recommended setup for most users.
However, if you want to build images only for one specific architecture, saving some space, you can override the
platforms
variable in your local Bake file.
platforms = ["linux/amd64"]
If you’d like to build everything into your own repository while managing the same tags, that’s also possible. We may cover that in a future post.