Building Docker image with Gradle

In the world of cloud-native applications - Docker and containerization is a must. Jar (or war) file as a final artifact is no longer an option. What you need to provide to easily deploy an application is Docker image in Docker registry. In short - you need to build Docker image and push it to registry. Fortunately, well known tools like Gradle can significantly help with this task.

There are a few tools you can use to achieve that with Gradle:

I will show how to prepare Gradle configuration with each of the tools above and compare them.

Docker CLI from Gradle task

Building Docker image and pushing it to image repository with Docker CLI is very easy. Because of that, it seems like a pretty simple to create a couple of tasks in Gradle to execute Docker commands. When you have fat JAR file built with Gradle for your application, all you need to do is:

  1. Prepare Dockerfile
  2. Add task to Gradle copying built JAR file to the place available for Docker build command
  3. Add task to Gradle invoking docker build -t yourimage .
  4. Add another task to Gradle with docker push yourimage

Seems like an easy task, but there are a few nuisances here and there.

Example

These are the tasks added to build.gradle to build and push Docker image.

[...]
task copyJarToDockerDir(type: Copy) {
    from "$buildDir/libs"
    include "**/*.jar"
    into "$buildDir/docker"
}

task prepareDockerDir(type: Copy) {
    from "$projectDir/docker"
    include "**/*"
    filter { it.replaceAll('<%=name%>', project.name) } // makes Dockerfile more generic
    into "$buildDir/docker"
}

task buildDockerImage(type:Exec) {
    workingDir "$buildDir"
    commandLine "docker", "build", "-t", "maniekq/${project.name}:${project.version}", "docker"
}

task pushDockerImage(type: Exec) {
    commandLine "docker", "push", "maniekq/${project.name}:${project.version}"
}

copyJarToDockerDir.dependsOn build
buildDockerImage.dependsOn prepareDockerDir
buildDockerImage.dependsOn copyJarToDockerDir
pushDockerImage.dependsOn buildDockerImage

As you can see above there are dependencies between the tasks to make sure JAR file is ready before copying it to docker directory and image is build before pushing image.

See full example on GitHub.

If you want to build Docker image locally run:

./gradlew pushDockerImage

To create and push image to Docker repository run:

./gradlew buildDockerImage

Docker Gradle Plugin from Palantir

Palantir Gradle plugin handles basic stuff for you. You can avoid implementing Gradle tasks yourself and managing dependencies between them. All you need to do is configuring few options and you get functionality very similar to the CLI approach presented above. You get this simplicity at the cost of flexibility, but for most cases this is a good deal.

Example


plugins {
    id 'com.palantir.docker' version '0.22.1'
}

docker {
    name "maniekq/${project.name}:${project.version}"
    dockerfile file('docker/Dockerfile')
    files 'docker/entrypoint.sh', "$buildDir/libs/"
}
docker.dependsOn build
[...]

See full example on GitHub.

If you want to build Docker image locally run:

./gradlew docker

To create and push image to Docker repository run:

./gradlew dockerPush

Jib

Using Google Jib makes preparing and pushing Docker image pretty easy. If you were preparing Dockerfile yourself previously, using it may seem a bit awkward. It looks like another level of abstraction which can make specific configuration harder. However, Jib allows detailed configuration and at the end of the day, it feels quite natural to use configuration of image in Gradle script, instead of preparing separate Dockerfile.

What is more - Jib is not an abstraction above Docker. It can create and push images without having Docker installed on your machine at all. This may be useful for simplifying your CI pipeline.

Jib is able to pick up some configuration which needs to be configured in Gradle anyway. One example is main class of your app. You don't need to specify your application main class for Jib if you have Spring Boot application or using Jar configuration in Gradle.

One more distinctive feature of Jib is that it will not package your application as runnable JAR. Instead it will put your dependencies as JAR files and compiled classes exploded. This feature allows better separation between Docker image layers and can potentially save you some time on building images.

Example

Basic configuration for Jib is pretty straight forward.


plugins {
    id 'com.google.cloud.tools.jib' version '1.8.0'
}

jib {
    from {
        image = 'adoptopenjdk:11.0.5_10-jre-hotspot-bionic'
    }

    to {
        image = "maniekq/${project.name}:${project.version}"
    }
}
[...]

See full example on Github.

If you want to build Docker image locally run:

./gradlew jibDockerBuild

To create and push image to Docker repository run:

./gradlew jib

Summary

Although, building image directly with Docker CLI seems to be an easy task, there are quite a few difficulties doing that from Gradle. In my opinion, this makes it error-prone and harder to maintain.

Palantir Gradle plugin solves a lot of these issues. It is simple to use and looks very familiar for anyone specifying Dockerfile earlier. Palantir also has additional functionalities for Docker Compose tasks and running Docker images if you need that.

I was quite sceptical with Jib at the beginning. It was mainly because of additional complexity from introducing another abstraction level with image manifest in Gradle. Preparing Dockerfile seemed to be very simple and something I have used to. However, after working with Jib for more time, I have changed my mind. It is very simple to use and at the same time flexible with configuration options. Even having image manifest completely in Gradle, instead of Dockerfile, seems to be better option, because it allows to use parameters from Gradle in this manifest easily.

In my opinion, if building and pushing Docker image is all you need - Jib is the way to go.

Comments

comments powered by Disqus