Automating Docker Builds With Gradle

Automating Docker Builds With Gradle

Last Updated on November 4, 2020

1. Introduction

We’re all using Docker containers these days to do all sorts of great stuff, but it’s not always obvious how best to automate the building of images and embed good repeatable processes into our projects. In this article you’ll discover one of the best approaches I’ve found to automate Docker in your project. It is to create tasks for the building of images and running of containers within your Gradle project. 

With this approach, anyone checking out your code can immediately start a Docker container without having to read your documentation. If you’re working on a project with multiple source code repositories, having a consistent approach like this makes it a lot easier for people to get to grips with code you’ve written.

2. Overview

When it comes to Docker plugins there are two main players right now, the Palantir Docker Plugin and the Bmuschko Docker Plugin. Technically, they are best described as suites of plugins as there are separate plugins providing specific fine grained functionality.

I generally use the Palantir Docker Plugin as it’s slightly less opinionated in that it let’s you configure your own Dockerfile. This is the plugin I’ll be using in this article. The Bmuschko Docker Plugin, on the other hand, generates the Dockerfile for you or allows you to configure it’s instructions in your build.gradle.

They are both good plugins, so here’s a quick summary of what to expect from each.

Feature DescriptionPalantir Docker PluginBmuschko Docker Plugin
Create Dockerfile task
Build image task
Push image task
Tag image task
Run container task
Stop container task
Remove container task
SummaryMore tasks with more functionality available. Less configuration required. Uses provided Dockerfile, which cannot be generated automaticallyLess tasks available, but you can implement one of the provided custom task types.
Configuration is more opinionated. e.g. you can build an image for a Spring Boot project, without having to define the Dockerfile

3. Step-by-step example

Let’s run through an example, where we’ll create a project that deploys an existing Java application into a Docker image and runs it.

Grab the code
To follow along with this example get the code from this GitHub repository

3.1 Application to deploy

You can download this sample application jar file which runs a Spring Boot application on port 8080. Create an empty directory, put the jar file in it, then try running:

mkdir gradle-docker-example
cd gradle-docker-example
cp <jar file location> .
java -jar springbootify.jar

You’ll then be able to make a request to http://localhost:8080/doit

3.2 Initialising Gradle

Now that you have a sample application in an empty directory, let’s initialise the project with whatever version of Gradle you have installed on your machine. If you don’t have Gradle yet, you can download it from gradle.org.

Tip: To run Gradle you’ll also need a Java installation and a JAVA_HOME environment variable configured. Also, don’t forget to add Gradle to your PATH environment variable.

Run the following command to setup Gradle in this project:

gradle init
<hit enter to all the questions>

You’ll now have a Gradle project setup, along with the build.gradle, which is where we’ll be putting all the configuration for building this project.

3.3. Create the Dockerfile

The Dockerfile is the text file where we’ll put the instructions to tell Docker how to build our image. There’s no file extension, so just create a file with name Dockerfile.

FROM openjdk:12-jdk-alpine
COPY springbootify.jar springbootify.jar
CMD ["java","-jar","springbootify.jar"]

Explanation: in our case we just need 3 instructions:

FROM tells Docker that this image is based on the OpenJDK 12 Alpine base image. This means we’ll be using Alpine Linux, which is lightweight and fast. It’s bundled up with a Java installation so we don’t have to worry about installing it separately.
COPY will copy the application jar file into the image
CMD tells the Docker what command to run when we start a container of this image. In our case, we just want to run java specifying the jar file.

3.4. Building the Docker image with Gradle

Now it’s time for the fun stuff. Let’s configure the Palantir Docker plugin to create an image for the Dockerfile we just created, and then run the container. Open up build.gradle and add this plugin declaration to apply the plugin:

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

Add a project version as we’ll want to use this within the name of our image:

version '0.1.0'

We’ll now specify a configuration for the com.palantir.docker plugin to build the image.

docker {
     name "${project.name}:${project.version}"
     files 'springbootify.jar'
}

Explanation: here we’ll use the project name and version for the name of this image. The files property specifies which files should be available to our Dockerfile, as by default nothing is exposed. Under the hood, the plugin copies these files into a temporary directory, exposing only these files to the docker build command.

We can now try out building the image by running:

./gradlew docker

All done? Run docker images to see that your new image has been built.

3.5 Running the Docker image with Gradle

To run the image as a container, we need to add another plugin:

plugins {
    id "com.palantir.docker" version "0.22.1"
    id "com.palantir.docker-run" version "0.22.1"
}

Info: note how these plugins are fine grained, so you don’t need to import more functionality than you need.

We’ll also need a specific configuration for running the container.

 dockerRun {
    name "${project.name}"
    image "${project.name}:${project.version}"
    ports '8080:8080'
    clean true
}

Explanation: this configures the plugin to create a container with the given name, using an image with the same name we configured in the docker configuration block (i.e. it uses the image we just built).

We’re connecting to port 8080 on the container as this is what our application uses, and exposing that on the host also on port 8080.

It’s useful to include the clean true property so that when you stop the container it gets automatically removed as well.

Now we can run the container:

./gradlew dockerRun

Check out docker ps, which will show your running container.

The application is now available on port http://localhost:8080/doit.

To stop the application, you can run ./gradlew dockerStop.

4. Next steps: pushing your image

Now that you’ve seen how to build images and run containers, it’s worth thinking about how this fits into your full build process. Consider the scenario where you want to build the application on your continuous integration (CI) server. You’ll want to make sure to run:

./gradlew build docker

This will first build your application, which for example, may generate a jar file like we were using in the previous section). It will then build your image. This is great, but to get it deployed somewhere else, it needs to be pushed to a central location so that other Docker installations can access it.

4.1 Setting up an account on Docker Hub

Docker Hub allows you to create public or private repositories where you can store your images. There are many other options available, for example AWS’s Elastic Container Registry (ECR), but Docker Hub is the simplest way to get started.

Tip: a Docker repository is where you put different versions of a specific image (e.g. gradle-docker-example), whereas a Docker registry is where those repositories live (e..g Docker Hub)

Follow these steps to sign up and create your repository, then in the next section you’ll see how to tag and push your image to it.

  1. Go to hub.docker.com and create an account
  2. Go to Repositories and create a private repository called gradle-docker-example

Note down your repository name, e.g. <username>/gradle-docker-example

4.1. Configuring the plugin for pushing

Docker’s mechanism to push an image to a central repository involves first tagging the image with the repository name, then pushing it. 

The Palantir Docker plugin exposes two more types of tasks for this purpose, dockerTag and dockerPush. Let’s extend the dockerConfiguration to specify the tag:

docker {
    name "${project.name}:${project.version}"
    files 'springbootify.jar'
    tag 'DockerHub', "<dockerHub-username>/gradle-docker-example:${project.version}"
}

Replace <dockerHub-username> with your username.

Explanation: there are two values passed to the tag configuration
1) a name to use to refer to this tag.
2) the repository name itself with a version i.e. <repositoryName>:<version>

Run docker tasks and you’ll see there are more tasks available now.

You can see that the DockerHub string we put in the tag configuration is used to dynamically generate the tasks dockerTagDockerHub and dockerPushDockerHub.

Run ./gradlew dockerTagDockerHub and then check the output from docker images:

Now you’ll see we now have 2 versions of the image we created before, with the same image id. The new version of the image has:

  1. An external repository pointing at your Docker Hub account
  2. A tag, normally the version of your application.

In order to push you’ll first have to authenticate with the Docker Hub:

docker login
<enter username & password> 

The last step is to run ./gradlew dockerPushDockerHub. You’ll see an output similar to below.

Tip: Docker pushes are incremental. If, for example, you add a new instruction to your Dockerfile it is intelligent enough to only push changes from that point onwards.

4.2 Pulling the image from another machine

Now your image exists on a central registry you can run it from whatever server you like, with the following commands:

docker login
<enter username & password>
docker run -p 8080:8080  <dockerHub-username>/gradle-docker-example:0.1.0

Tip: If you want to try this out on the same server where you built the image, just run docker rm <image-id> before running the above command. That way, docker will not find the image locally, and instead pull it from the Docker Hub like in the output below.

5. Conclusion

Now you’ve seen how to create images, run containers, and push images using simple Gradle Docker tasks.

Due to the simplicity of running these tasks it’s very easy to integrate them into your software development lifecycle, such as during development, continuous integration, or deployment.

To get started right away check out the accompanying GitHub repository, containing all the code from this article.

Get the newsletter

Found this article helpful? Subscribe for monthly updates.

✅ All of my latest articles for the month
✅ Access to video tutorials
✅ Exclusive tips not found on my website

Automating Docker Builds With Gradle

13 thoughts on “Automating Docker Builds With Gradle

  1. Very confused… beyond the hello world application I’m finding palantir utterly useless and a time waster. After 4 days with it I understand it a lot better, but it makes no sense. How on earth would you build 2 image files and run them? These people writing gradle plugin are ruining the potential of gradle by only exposing the underlying properties they happen to care about for their own project and then releasing it and pretending like it could be used as a generalized tool for any project, so far that’s proving NEVER to be the case. It’s getting rediculous… I jumped over maven to learn gradle, but after 2 months it’s clearly an inadequate build system. Using multiple plugins in the same file is a nightmare; each plugin provides no flexibility yet takes longer to configure than the underlying functionality. I am so utterly disappointed, gradle shouldnt be used to manage a software lifecycles unless your building a cookie cutter application like wordpress or something extremely common, in which case its overkill, hence why its useless… if you need a tailored and well engineered software development lifecycles, I am absolutely certain gradle is not the right tool for the job at this point… now I need to decide… do I learn maven or do I just build my own system in a shell script or language. I’m so pissed off I wasted so much time on gradle, dam thing is not what it is advertised to be… way to complicated and not nearly flexible enough, great qualities if your trying to send your software to the morgue.

    1. Hi Wp. It’s so frustrating when something doesn’t turn out how you expect. It sounds like you’ve invested time learning Gradle but not got what you were hoping for.

      The learning curve is very steep. I spent many months on my first Gradle project not really understanding how it works. It was pretty rough, but eventually after sticking with it something clicked. I’ve tried to compress what I’ve learnt so others can pick it up quicker, but it’s far from plain sailing.

      On a practical note, if you want to publish multiple Docker images you can do that by applying the Palantir Docker plugin to multiple subprojects, and having each one build and push an image as you need. Let me know if you need more info on that.

      1. “…if you want to publish multiple Docker images you can do that by applying the Palantir Docker plugin to multiple subprojects”; tried that, it pretty much breaks the kotlin multiplatform plugin which requires its own structures and is far more important than docker to me (though that too has been a nightmare because documentation is written for gradle 3 or something and it suffers the same fundamental design phylosphy problems as every plugin I’ve tried to use)… this is why gradle is such a disappointment, plugins should be plugins they shouldn’t take over your project pretending like only internal consideration matter. I think I like gradle, but the plugins have become my mortal enemy… I might just drop all plugins, even palantir, multiplatform and maybe even java, then write one plugin for all my build logic called wp-lifecycle or something. Anyway, I have started a slack thread to further discuss if interested.

        https://gradle-community.slack.com/archives/CAHSN3LDN/p1594180628258300

  2. Nice Post , Thanks a lot , Just wanted to know with the palantir plugin how do I make it a part of the build process like i will just hit ./gradlew build and the dockker task should run as part of it

    I tried
    build.dependsOn(docker) but it throws

    * What went wrong:
    Could not determine the dependencies of task ‘:build’.
    > Cannot convert extension ‘docker’ to a task.
    The following types/formats are supported:
    – A String or CharSequence task name or path
    – A Task instance
    – A TaskReference instance
    – A Buildable instance
    – A TaskDependency instance
    – A Provider that represents a task output
    – A Provider instance that returns any of these types
    – A Closure instance that returns any of these types
    – A Callable instance that returns any of these types
    – An Iterable, Collection, Map or array instance that contains any of these types

    Basically do not want to run “./gradlew docker” separately , I am using grrovy DSL with gradle , how do I achieve it ?

    1. Hi Tushar,

      The reason you’re having a problem is because the docker task and the docker configuration have the same name, so Gradle is getting confused. Try this instead:

      build.dependsOn(project.tasks['docker'])

      Hope it helps! Tom

  3. Hi Tom,
    Thanks for a detailed documentation. Good job.

    I am a long term Maven user and recently switched to using Gradle.
    Our project’s scope is to develop apps on AWS Workspace where we couldn’t install Docker locally.
    However after going through your article, it seems that we could build the docker image with the Palantir gradle plugin without a native Docker environment.
    Is my understanding correct?

    Thanks again

    1. Hi Dinesh. Thanks for the comment.

      Unfortunately you still need a Docker environment setup wherever you’re running the Gradle build. The Palantir Gradle Docker plugin is simply executing a command on the command line in the background. Take a look at the DockerRunPlugin.groovy source code to see what I mean. e.g.


      dockerStop.with {
      commandLine 'docker', 'stop', ext.name
      }

      Hope that helps.

      Tom

  4. Hello Tom,

    Thanks a lot for the quick and easy to learn post. I wanted to check with you if we can make palantir plugin generic to push at various repositories like docker hub , ECR or any other image repos? Can you please update your post to see how we can push the same image in ECR without a lot gradle code ?

    tag ‘DockerHub’,”asifakb/docker-ci-cd:$bootJar.version”
    tag ‘ECR’,” ? How can we do this in docker { } task?

    docker {
    name “${project.group}/${bootJar.baseName}”
    copySpec.from(tasks.unpack.outputs).into(“dependency”)
    buildArgs([‘DEPENDENCY’: “dependency”])
    tag ‘DockerHub’,”asifakb/docker-ci-cd:$bootJar.version”
    }

    1. Hi Asif. Thanks for the helpful feedback.

      You can push to ECR in a very similar way. Just replace the tag value with the ECR repository URL.

      AWS CLI verison 2
      Run the following command to do the Docker login aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com

      Hope that helps. Tom

  5. Hey Tom,

    Thanks for the wonderful tutorial, following the steps I was able to create the docker image, now I’m trying to convert the following Dockerfile to be used with gradle’s palantir plugin to create image but I’m unable to do so, here is the steps for docker file:
    FROM openjdk:11-jre-slim as builder
    WORKDIR application
    ADD maven/${project.build.finalName}.jar ${project.build.finalName}.jar
    RUN java -Djarmode=layertools -jar ${project.build.finalName}.jar extract

    # spring-boot application image
    FROM openjdk:11-jre-slim
    LABEL PROJECT_NAME=${project.artifactId} \
    PROJECT=${project.id}

    EXPOSE 8080

    WORKDIR application
    COPY –from=builder application/dependencies/ ./
    COPY –from=builder application/spring-boot-loader/ ./
    COPY –from=builder application/snapshot-dependencies/ ./
    COPY –from=builder application/application/ ./
    ENTRYPOINT [“java”, “-Djava.security.egd=file:/dev/./urandom”, “org.springframework.boot.loader.JarLauncher”]

    I’ve successfully created the image using MAVEN’s fabric8 plugin, but I couldn’t find the way to use it with gradle.

    1. Hi Shri. It looks like you’re using the layered approach to building your jar file? I’ve heard this is good for creating Docker images that don’t change so much when you recreate your jar, but I haven’t tried this approach yet. Are you getting a specific error?

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top

Get the newsletter

Found this article helpful? Subscribe for monthly updates.

✅ All of my latest articles for the month
✅ Access to video tutorials
✅ Exclusive tips not found on my website