|
| 1 | +--- |
| 2 | +title: "Creating a custom container image for CloudNativePG v2.0" |
| 3 | +date: 2025-06-17 |
| 4 | +draft: false |
| 5 | +image: |
| 6 | + url: |
| 7 | + attribution: |
| 8 | +author: jgonzalez |
| 9 | +tags: |
| 10 | + - blog |
| 11 | + - information |
| 12 | + - programming |
| 13 | + - applications |
| 14 | + - containers |
| 15 | + - postgresql |
| 16 | + - postgres |
| 17 | + - images |
| 18 | + - tutorial |
| 19 | +summary: Creating a container image for CloudNativePG Operator v2.0 |
| 20 | +--- |
| 21 | + |
| 22 | +## Summary |
| 23 | +In an almost [two years old blog post](https://cloudnative-pg.io/blog/creating-container-images/), we explained how |
| 24 | +to build a custom container image for CloudNativePG. After two years, many things have changed in the containers world, |
| 25 | +one of those things was the introduction of [Bake](https://www.docker.com/blog/bake/) in Docker, which allows you to build |
| 26 | +images using a simple configuration file, and it is now our recommended way to build images for CloudNativePG. |
| 27 | + |
| 28 | +We will follow a simple cooking recipe to create a custom container image or a set of container images, since Bake |
| 29 | +allows you to build multiple images at once in a pretty simple way. |
| 30 | + |
| 31 | +## Ingredients |
| 32 | +- A bake file, we will use the one provided in the [CloudNativePG repository](https://github.com/cloudnative-pg/postgres-containers/blob/main/docker-bake.hcl) |
| 33 | +- A Dockerfile, we will provide a simple one, that will use a base image |
| 34 | + |
| 35 | +Cooking time: 10 minutes |
| 36 | + |
| 37 | +## Instructions |
| 38 | + |
| 39 | +### Step 1: Prepare local bake file |
| 40 | +In a local file with name `bake.hcl`, we add the following content, which is a simple bake file that will build a custom image |
| 41 | +```hcl |
| 42 | +extensions = [ |
| 43 | + "pgvector", |
| 44 | +] |
| 45 | +target "myimage" { |
| 46 | + dockerfile-inline = <<EOT |
| 47 | +ARG BASEIMAGE="ghcr.io/cloudnative-pg/postgresql:16.9-standard-bookworm" |
| 48 | +FROM $BASEIMAGE AS myimage |
| 49 | +ARG EXTENSIONS |
| 50 | +USER root |
| 51 | +RUN apt-get update && \ |
| 52 | + apt-get install -y --no-install-recommends $EXTENSIONS && \ |
| 53 | + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \ |
| 54 | + rm -rf /var/lib/apt/lists/* /var/cache/* /var/log/* |
| 55 | +USER 26 |
| 56 | +EOT |
| 57 | + matrix = { |
| 58 | + tgt = [ |
| 59 | + "myimage" |
| 60 | + ] |
| 61 | + pgVersion = [ |
| 62 | + "16.9", |
| 63 | + "17.5", |
| 64 | + ] |
| 65 | + } |
| 66 | + name = "postgresql-${index(split(".",cleanVersion(pgVersion)),0)}-standard-bookworm" |
| 67 | + target = "${tgt}" |
| 68 | + args = { |
| 69 | + BASE_IMAGE = "ghcr.io/cloudnative-pg/postgresql:${cleanVersion(pgVersion)}-standard-bookworm", |
| 70 | + EXTENSIONS = "${getExtensionsString(pgVersion, extensions)}", |
| 71 | + } |
| 72 | +} |
| 73 | +
|
| 74 | +``` |
| 75 | +There's a couple of things that we can remark here: |
| 76 | +* The `extensions` variable is a list of extensions that we want to include in the image, in this case we are using `pg_vector`, |
| 77 | + but you can add any other extension that you want to include in the image. |
| 78 | +* The `dockerfile-inline` variable contains our Dockerfile definition, which cannot be used remotely, will explain more about this later. |
| 79 | +* The `target` and the `tgt` have the same name, you can use what ever you want here as a name |
| 80 | +* The `pgVersion` is a variable list that contains basically, the MAJOR.MINOR version of PostgreSQL |
| 81 | +* The `name` is the name that we will use later to refer to one element of the matrix that we created |
| 82 | +* The `args` is all the arguments passed to the Dockerfile, will talk more about this later. |
| 83 | +* The function `getExtensionsString()` is one that is inherited from a bake file that we reference in the Ingredients section |
| 84 | + |
| 85 | +### Step 2: Build the image |
| 86 | + |
| 87 | +We can now build the image using the following command: |
| 88 | +```bash |
| 89 | +docker buildx bake -f docker-bake.hcl -f cwd://bake.hcl "https://github.com/cloudnative-pg/postgres-containers.git" myimage |
| 90 | +``` |
| 91 | +This will by default, build the image for the bake matrix we previously created and will try to push the image to the registry |
| 92 | +`localhost:5000`, which is the default registry defined for testing environments in the parent bake file, so let's explain the full command first. |
| 93 | + |
| 94 | +As is defined in the [Bake documentation about remote definition](https://docs.docker.com/build/bake/remote-definition/) |
| 95 | +you can use a remote bake definition with all the functions and default targets and attach another local one that you can use to override |
| 96 | +all the default values, in this case the `-f cwd://bake.hcl` is the local file that we created in the previous step, and |
| 97 | +the `-f docker-bake.hcl` is the remote file that we're using to build the image, this is in the git repo. |
| 98 | + |
| 99 | +You can explore more about all the content generated and used inside the bake file with the following command: |
| 100 | +```bash |
| 101 | +docker buildx bake -f docker-bake.hcl -f cwd://bake.hcl "https://github.com/cloudnative-pg/postgres-containers.git" myimage --print |
| 102 | +``` |
| 103 | + |
| 104 | +### Step 3: Push the image to a registry |
| 105 | + |
| 106 | +Now you just need to push the image to a registry, you can do this by using the following command: |
| 107 | +```bash |
| 108 | +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 |
| 109 | +``` |
| 110 | +The previous command will push the images in the following format `your/registry:5000/postgresql-testing:17-standard-bookworm`, |
| 111 | +using the `--print` you can explore the full list of tags created that are in the parent bake file. |
| 112 | + |
| 113 | +### Step 4: Serve the image |
| 114 | + |
| 115 | +You can now let your clusters to use the image that it's built and based on the CloudNativePG operand images provided |
| 116 | +by the CloudNativePG project. |
| 117 | + |
| 118 | +## Deep dive into the Bake and Dockerfile |
| 119 | + |
| 120 | +The simplicity of Bake to bring more stuff is amazing and allows you to create a custom image in a very simple way. |
| 121 | +But, how is this magic happens? Let's take a look at the Bake and the Docker file. |
| 122 | + |
| 123 | +### Bake file |
| 124 | + |
| 125 | +This is the base of our magic, but that magic starts in our postgres-containers repository, where we have a `docker-bake.hcl` file |
| 126 | +that it's being used to build the images provided by the CloudNativePG project, and it's the base for our custom Bake file. |
| 127 | +The `docker-bake.hcl` file contains a lot of functions that are used to build the images, and one of them is the `getExtensionsString()`, |
| 128 | +this one, using the list of extensions we provided, will return a string of the extensions with the correct package name |
| 129 | +for a Debian-based distribution, in our case, Debian Bookworm, so the `pg_vector` extension will be translated into |
| 130 | +`postgresql-16-pgvector` which will be the name of the package of pgvector extensions for PostgreSQL 16 in the Debian |
| 131 | +Bookworm distribution. |
| 132 | + |
| 133 | +Even the variable that are passed in the `args` variable to the Dockerfile are processed by the Bake file, in this case, |
| 134 | +the variables we add, are being added and the ones we overwrite are being overwritten, so we can use the same variables |
| 135 | +and content used in the parent Bake file. |
| 136 | + |
| 137 | +### Dockerfile file |
| 138 | + |
| 139 | +The Dockerfile is simple an inline content, because of the limitations of Bake to overwrite the remote Dockerfile with a |
| 140 | +local one, but it allows us to change the FROM in the image, which means we can create an image that it's directly based |
| 141 | +on the CloudNativePG images, and we can add the extensions we want to use in our image, without building all of them |
| 142 | + |
| 143 | +## There's more! |
| 144 | + |
| 145 | +You may want to avoid building arm64 image by adding the following: |
| 146 | +```hcl |
| 147 | +platforms = ["linux/amd64"] |
| 148 | +``` |
| 149 | +This will overwrite the platforms variable and so, will build only for one platform |
| 150 | + |
| 151 | +Also, if you want to build everything into your own repository and manage the same tags, it's possible, in the future |
| 152 | +we may write another post explaining this |
0 commit comments