Fast builds of Java & Spring Boot applications in Docker

If you’re using Java, Spring, or any other compiled language, you care a lot about build times. Unfortunately, building your application inside Docker is slow because Docker doesn’t take advantage of your build caches. Or can it?

Why build inside your container?

There are two main advantages to building inside a container.

  1. Setting up the same development environment everywhere is a breeze, since everyone just uses the same container definition. With this approach, you can guarantee that every developer on an application is using the exact same version of the compiler/runtime as everyone else.

  2. Eliminate differences between development and production. Not only can each developer use a container, you can also use the same container for production. Moreover, with Docker multi-stage builds, you can even remove the compiler and intermediary artifacts for your production container.

Building a Spring Boot application in Docker

We’re going to compile the sample Hello, World application here: https://github.com/datawire/hello-world-spring-boot. This is a simple Spring Boot application that uses Gradle as its build system.

Building the Docker image takes awhile because Gradle has to download the various Maven dependencies.

$ time docker build .
real    1m4.763s
user    0m0.364s
sys     0m0.328s

Making a one line code change triggers another download of the dependencies, resulting in similar build times to the first compile.

$ time docker build .
real    1m13.042s
user    0m0.376s
sys     0m0.335s

Enabling incremental builds

What if we could somehow get Docker to use a build cache? It turns out there’s a way to do this, although it’s somewhat complicated. You can:

  • Do a regular build of your Docker container
  • Assuming the build is successful, snapshot that container into a new image
  • On a subsequent build, use the snapshot as the base of the build and copy your source (and any other files that might be modified) into the container

This way, any intermediate artifacts generated by your build system can be preserved in the image. We’ve implemented this feature in Forge.

$ time forge build containers
real    1m22.943s
user    0m3.336s
sys     0m0.711s

Now, after a one line code change:

$ time forge build containers
real    0m15.994s
user    0m2.021s
sys     0m0.418s

Using Forge for incremental Java builds

How does this work? Forge relies on metadata specified in a service.yaml file. In the hello-world-spring-boot application, we’ve defined:

containers:
  - dockerfile: Dockerfile
    context: .
    rebuild:
      root: /srv
      command: ./gradlew test build
      sources:
        - build.gradle
        - settings.gradle
        - src

The root property tells Forge the build directory of the application. The command property tells Forge how to build the application. And the sources property tells Forge which files or directories to copy to the new container.

(You’ll see in the actual source that there’s other metadata in the service.yaml file — those aren’t necessary for incremental builds, as they use other portions of Forge functionality.)

Further reading

Interested in using this technique? Forge is packaged as a single binary, and works on Mac OS X and Linux. Get going in 3 minutes.

You’ll also want to investigate using Docker multi-stage builds. In the multi-stage build, your second stage build starts from a clean image, and then copies the artifacts generated in the first stage into a new build. This way, your second stage build doesn’t need to include the compiler or any intermediate artifacts.

In this blog post, we’ve only touched on one small feature of Forge. Forge lets you quickly build and deploy multiple services from source to Kubernetes, with support for templating Kubernetes manifests, multiple deployment profiles, dependency management, and more. Visit https://forge.sh for more information.