If I run gradlew bootBuildImage the image is built every time as this task has no outputs

> Task :bootBuildImage
Caching disabled for task ':bootBuildImage' because:
  Caching has been disabled for the task
Task ':bootBuildImage' is not up-to-date because:
  Task has not declared any outputs despite executing actions.
Building image ...

The rebuilding of the image takes 8 seconds vene if nothing has changed. So it is worth caching. If i run tests against the image and I am only changing some tests, I don't want the image to be rebuild every time.

I doesn't help to add a fixed timestamp to get a repeatable build

springBoot {
    buildInfo {
        properties {
            time = "1895-01-01T18:00:00.0Z"
        }
    }
}

What does help in gradle is to add

tasks.bootBuildImage {
    onlyIf {
        !tasks.bootJar.get().state.skipped
    }
}

credits to gyoder at Stackoverflow

But this does not take into account the following changes: - changing the task configuration of bootBuildImage itself does not trigger a rebuild - deleting the image from the docker daemon

I don't know if it is wise to ask the docker daemon if the image is already available, but the gradle docker plugin does exactly that. It saves the imageId to the build/.docker Directory, looking up this ID in the docker daemon and does not rebuilt the image if the image is already available:

As the task bootBuildImage does connect to the Docker daemon anyway, it might be worth asking the daemon if the image is already available.

private final Spec<Task> upToDateWhenSpec = new Spec<Task>() {
    @Override
    public boolean isSatisfiedBy(Task element) {
       File file = getImageIdFile().get().getAsFile();
       if (file.exists()) {
           try {
               String fileImageId;
               try {
                    fileImageId = Files.readString(file.toPath());
               } catch (IOException e) {
                    throw new UncheckedIOException(e);
               }
              List<String> repoTags = getDockerClient().inspectImageCmd(fileImageId).exec().getRepoTags();
              if (!getImages().isPresent() || repoTags.containsAll(getImages().get())) {
                   return true;
               }
           } catch (DockerException ignored) {
         }
       }
      return false;
   }
};

Code

Comment From: wilkinsona

This is intentional as there's no way for the image building process to know that everything truly is up-to-date. Depending on the builder that's being used, passing the exact same jar through the image building process could result in a different image because the builder has picked up a change to one of the dependencies that it embeds in the image.

If you're happy for the image to be considered up-to-date based purely on whether the input jar was up-to-date, opting in with some additional configuration such as you shared above is the right thing to do.

Comment From: kicktipp

Thanks for the detailed explanation. But in my understanding a reproducible build is a good idea, so I should set an exact version number for the builder

tasks.bootBuildImage {
     builder.set("paketobuildpacks/builder-jammy-full:0.0.48")
}

Is it still true that the build might differ? I don't think so. It is only if ":latest" is used, which is - of course - the default. So an idea might be to cache the result only if the builder is given a tag other than "latest"

In my understanding the plugin produces the exact same image if a version other than "latest" is used for the builder. By the way. The docs are promoting build reproducibility when describing the property of createdDate

Anyway, I am happy with the solution, but maybe I have convinced you of the benefits of a reproducible build.