Site icon Craig Andrews

Improving the Reproducibility of Spring Boot’s Docker Image Builder

Reproducible builds are big wins for security, maintainability, and sanity. If you don’t like it when nothing has changed, yet your build suddenly breaks or doesn’t produce the same output, then improving reproducibility is for you. By default, Spring Boot’s Docker/OCI image building solutions, bootBuildImage (in Gradle) and spring-boot:build-image (in Maven), do not operate reproducibly. Here’s how to improve that situation for bootBuildImage – the same approach applies for spring-boot:build-image with a different syntax that won’t be covered here for sake of brevity.

Why Being Reproducible is Important

Reproducible builds are important and provide benefits in many areas, including:

I previously discussed the benefits of reproducible builds in my Reproducible Builds in Java article.

Benefits of building Docker/OCI images with Buildpacks using bootBuildImage

bootBuildImage was introduced in May 2020 with the release of Spring Boot 2.3. It provides a way to create a Docker image from a Spring Boot application using Cloud Native Buildpacks. This approach has some significant advantages over alternatives approaches (such as crafting a Dockerfile and building the image using docker build, kaniko, or similar tools):

How to use bootBuildImage Reproducibly

By default, bootBuildImage will do a few things that result in non-reproducible behavior:

To solve these problems, the builder, run, and buildpack images can all be explicitly specified including versions and digests. The digest is important because it’s possible for a version to be updated (a new “3.4.0” image could be published – in Docker images, “versions” are just tags and tags are mutable), but a digest is immutable always pointing to the same image.

Here’s the resulting build.gradle excerpt:

tasks.named("bootBuildImage") {
	// See: https://paketo.io/docs/howto/java/
	environment = [
		// run the JLink tool and install a minimal JRE for runtime, reducing both image size and attack surface
		"BP_JVM_JLINK_ENABLED" : "true",
	]
	docker {
		// version and digest pin all image references. This ensures reproducibility.
		// make sure to configure Renovate to keep these image references up to date.
		// if these image references are not kept up to date, any security issues discovered within them will never be fixed.
		// Use a tiny builder and run image (which produce a distroless-like image) to reduce both image size and attack surface.
		builder = "docker.io/paketobuildpacks/builder-jammy-tiny:0.0.33@sha256:e092679d4c4648ee9eaec77fc277952c0104b7c0be8d69d4cf0796f9eeb3bbd9"
		runImage = "docker.io/paketobuildpacks/run-jammy-tiny:0.1.15@sha256:07e64eabb1763b51bf38e9865e31362aa213163865e6a3132e4c4b43e95e273c"
		buildpacks = [
			"gcr.io/paketo-buildpacks/ca-certificates:3.4.0@sha256:18f4c21df483fad2fc947de4f80bbe6b635912ef78b4e9ed10c047eab4dd6f10",
			"gcr.io/paketo-buildpacks/bellsoft-liberica:9.8.0@sha256:17d05fc67e416dc180b1bcc03d69fc8e3cdfeab928c7797d318c52cbad4051f2",
			"gcr.io/paketo-buildpacks/syft:1.20.0@sha256:558356668ce53f4acf4252cc1cd26b7bc6303c4d3353acee60efc4122aaa25d2",
			"gcr.io/paketo-buildpacks/executable-jar:6.5.0@sha256:971568ae093406911d36fb07c35f589ee69b6c7a2d3f6b90e9e4e924f05baa13",
			"gcr.io/paketo-buildpacks/dist-zip:5.4.0@sha256:245717608ea3a379831ffe9f0a40c0ca82a851c9a0e60f36db9f727cf887f36a",
			"gcr.io/paketo-buildpacks/spring-boot:5.19.0@sha256:8071eea13a32f563b2cfb8536d78f8a777f591aa991ca5cceaa37a72537a88dd",
		]
	}
}

As a bonus, I’ve also also set BP_JVM_JLINK_ENABLED which tells the buildpack to use the JLink tool to install a minimal JRE for runtime, another best practice although one not related to reproducibility.

As mentioned in the comments, it’s critical to keep these image references up to date. Here’s a Renovate configuration that could be used as is or merged into an existing renovate.json to do so:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:base"
  ],
  "pinDigests": true,
  "regexManagers": [
    {
      "description": "Update docker references in build.gradle",
      "fileMatch": ["^build.gradle$"],
      "matchStrings": [
        "\"(?<depName>(?:gcr\\.io|docker\\.io)\\/[^:]+?):(?<currentValue>[^@]+)(?:@(?<currentDigest>sha256:[a-f0-9]+))?\""
      ],
      "datasourceTemplate": "docker",
      "versioningTemplate": "docker"
    }
  ]
}

Now enjoy the improved security, maintainability, efficiency, and sanity of reproducible Docker image building using Spring Boot bootBuildImage.

Improving the Reproducibility of Spring Boot’s Docker Image Builder by Craig Andrews is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Exit mobile version