Skip to content

Conversation

@nMoncho
Copy link

@nMoncho nMoncho commented Sep 19, 2025

Hello,
I'd like to submit this PR to allow users create Distroless images. This was mentioned/requested in #1105.

After playing around with the keys provided by the DockerPlugin, I think this is the easiest approach to maintain. My original approach duplicated a lot of code from the existing plugin. With the approach outlined in this PR the work is mostly done by defining presets for existing Settings and Tasks.

I didn't add any tests nor documentation, as I hope this can be the starting point of a conversation.

override lazy val globalSettings: Seq[Setting[?]] =
Seq(
dockerBaseImage := "gcr.io/distroless/java",
dockerEntrypoint := Seq("/usr/bin/java"),
Copy link
Author

@nMoncho nMoncho Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I didn't want to override the entrypoint defined by the image, but by default is java -jar, expecting a single jar as CMD. This limits its use to simple JARs with no libraries, or to FatJAR/UberJARs.

With this entrypoint we can then specify arguments in the CMD.

distrolessDebuggerPort := None
)

override lazy val projectSettings: Seq[Setting[?]] = Seq(dockerCommands := {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Distroless images cannot run any script, like the on coming from BashStartScriptPlugin. This forces most of the configuration to be done at build time.
For example, the JDWP debugger must be specified when building so its corresponding cli argument can be included in the CMD command in the Dockerfile

a.getOrElse(Int.MaxValue) < b.getOrElse(Int.MaxValue)
}

val cmd = if (dockerCmd.value.isEmpty) {
Copy link
Author

@nMoncho nMoncho Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my original approach I duplicated dockerCmd to be an Option[Seq[String]]. This way users could define an empty CMD, or leave alone with a None. This in turn allowed this code to know explicitly what the user wanted.

With this preset, even if the user wants an empty CMD, the plugin will still calculate it and include in the Dockerfile. This, I feel, is a decent compromise. With a distroless image the CMD would probably always be defined to at least specify which JAR the user wants to run.

Otherwise, in the else branch, we expect the user to know exactly what they are doing.

* @return
* FROM command
*/
private final def makeFromAs(dockerBaseImage: String, name: String): CmdLike =
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Methods like this are duplicated from DockerPlugin. I didn't want to introduce a refactor where the methods are made public, as I wasn't sure if that would be the desired approach.

private final def makeExposePorts(
exposedPorts: Seq[Int],
exposedUdpPorts: Seq[Int],
jdwpPort: Option[Int]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has a slight change from the one in DockerPlugin. If the JDWP port is specified, then it's added to the exposed list.

* discovered main classes
* @return
*/
private def defineMainClass(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is similar to CommonStartScriptGenerator.generateStartScripts but here we require a single main class as it has to be defined at build time. The user cannot specify it at runtime like it was possible with the script.

* Uses the `Universal / mappings` to generate the `Docker / mappings`. This overrides the `Docker / mappings` to
* skip scripts
*/
def mapGenericFilesToDocker: Seq[Setting[?]] = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an override on the method defined in DockerPlugin. Here we override the mappings to exclude binaries, or scripts, as they cannot be run on a distroless container.

dockerLabels.value.map(makeLabel) ++
dockerEnvVars.value.map(makeEnvVar) ++
makeExposePorts(dockerExposedPorts.value, dockerExposedUdpPorts.value, distrolessDebuggerPort.value) ++
Seq(makeEntrypoint(dockerEntrypoint.value), makeCmd(cmd))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commands defined by the plugin are a subset of the ones available on DockerPlugin.
These are the most important changes:

  • There is not multistage build. Since this was based on DockerPermissionStrategy, which in some cases it requires a RUN while building, it doesn't make sense in this context as there is no way to run anything in a distroless image to change permissions.
  • There are no exposed volumes. Currently that's implemented with several RUN commands. I guess that if it would be desired, then defining just VOLUME without the RUNs would be possible.
  • There are no makeUser or USER. This is due to how users are provided/created by the base distroless image. There are different tags for root and nonroot users.

@muuki88 muuki88 added docker minor release drafter version labels Sep 23, 2025
@muuki88
Copy link
Contributor

muuki88 commented Sep 23, 2025

Hi @nMoncho

Thanks a lot for your contribution 🥳 ❤️ I'll try to take a look ASAP.

@nMoncho
Copy link
Author

nMoncho commented Dec 30, 2025

This PR started to failed due to what's mentioned in #1679. Specifically #1679 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docker minor release drafter version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants