Marcello DeSales
Marcello DeSales

Reputation: 22319

Generic Docker Image and Dockerfile for SpringBoot Apps using Gradle/Maven

According to https://spring.io/guides/gs/spring-boot-docker/, we can create Docker Images for SpringBoot applications using hard-coded name and version of the application. For instance:

src/main/docker/Dockerfile

FROM frolvlad/alpine-oraclejdk8:slim
VOLUME /tmp
ADD gs-spring-boot-docker-0.1.0.jar app.jar
RUN sh -c 'touch /app.jar'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

However, changing the name or the version of the app breaks the Docker Build command that you place in your build.gradle task.

build.gradle

task buildDocker(type: Docker, dependsOn: build) {
  push = true
  applicationName = jar.baseName
  dockerfile = file('src/main/docker/Dockerfile')
  doFirst {
    copy {
      from jar
      into stageDir
    }
  }
}

The command gradle buildDocker builds an image by staging the Dockerfile and the executable Jar from the app, and executing "docker build".

Question

Considering the names are static in Dockerfile, How can I change this setup to not break my builds once I change the version, or even the name my SpringBoot application when building the docker image?

Upvotes: 3

Views: 5116

Answers (2)

Marcello DeSales
Marcello DeSales

Reputation: 22319

Generic Multi-stage Dockerfile for SpringBoot Apps with Maven

  • I have worked on new versions of a generic Dockerfile for Springboot apps using multi-stage builds instead of maintaining multiple dockerfiles https://docs.docker.com/develop/develop-images/multistage-build
    • This uses Maven. Similar approach can be done for Gradle.
  • This runs the test cases and then builds the runtime image
    • Supporting splitting the downloads of dependencies from running the tests
    • Builds the test stage separately docker build -t tests --target builder .
    • Runs unit tests by default using code coverage
    • Runs integration tests from the switch mvn -s settings.xml -Dtest="!*IT,!*IntegrationTest" -P jacoco test.
  • Supports providing the JAVA_PARAMS and JAVA_OPTS for debugging and anything required
    • If you deploy this image in Docker Swarm or Kubernetes Helm.
  • Provide settings.xml to point to your Maven repo server (public or private)

The build below will do the following:

#
# Build stage to for building the Jar
#
FROM maven:3.2.5-jdk-8 as builder
MAINTAINER [email protected]

# Only copy the necessary to pull only the dependencies from Intuit's registry
ADD ./pom.xml /opt/server/pom.xml
# As some entries in pom.xml refers to the settings, let's keep it same
ADD ./settings.xml /opt/server/settings.xml

WORKDIR  /opt/server/

# Prepare by downloading dependencies
RUN mvn -s settings.xml -B -e -C -T 1C org.apache.maven.plugins:maven-dependency-plugin:3.0.2:go-offline

# Run the full packaging after copying the source
ADD ./src /opt/server/src
RUN mvn -s settings.xml install -P embedded -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -B -e -o -T 1C verify

# Building only this stage can be done with the --target builder switch
# 1. Build: docker build -t config-builder --target builder .
# When running this first stage image, just verify the unit tests
# Overriden them by removing the "!" for integration tests
# 2. docker run --rm -ti config-builder mvn -s settings.xml -Dtest="*IT,*IntegrationTest" test
CMD mvn -s settings.xml -Dtest="!*IT,!*IntegrationTest" -P jacoco test

#
# Build stage with the runtime jar and resources
#
FROM openjdk:8-jre-slim

# Copy from the previous stage
COPY --from=builder /opt/server/target/*.jar /tmp/

# Just rename the built version
RUN mkdir /runtime && \
    find /tmp -name "*.jar" ! -name "*sources*" -exec cp -t /runtime {} + && \
    mv /runtime/*.jar /runtime/server.jar && \
    rm -f /tmp/*.jar

# Port used by the server
EXPOSE 8888

# This is to support HTTPS calls to
RUN apt-get update && apt-get install -y curl ca-certificates
RUN update-ca-certificates && \
   mkdir -p /usr/share/ssl/certs && \
   chmod 755 /usr/share/ssl/certs

# What to execute on docker run
ENTRYPOINT sh -c "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom \
           $JAVA_PARAMS -jar /runtime/server.jar --server.port=8888 $SPRING_BOOT_APP_OPTS"

Build and Run Tests

  • Following the multi-stage build spec, we can now generate only the test image
    • For multiple types of tests (unit, integration), you can split the executions.

So, the building the tests can be done as follows:

$ docker build -t generic-dockerfile:tests --target builder .
Sending build context to Docker daemon  16.82MB
Step 1/9 : FROM maven:3.2.5-jdk-8 as builder
 ---> 95dd59c15f5d
Step 2/9 : MAINTAINER [email protected]
 ---> Using cache
 ---> e4edaeb48381
Step 3/9 : ADD ./pom.xml /opt/server/pom.xml
 ---> Using cache
 ---> b2d6d834b411
Step 4/9 : ADD ./settings.xml /opt/server/settings.xml
 ---> Using cache
 ---> 9b0964db2c9f
Step 5/9 : WORKDIR  /opt/server/
 ---> Using cache
 ---> 542d0bd9d12f
Step 6/9 : RUN mvn -s settings.xml -B -e -C -T 1C org.apache.maven.plugins:maven-dependency-plugin:3.0.2:go-offline
 ---> Using cache
 ---> 3c2d8df6b52e
Step 7/9 : ADD ./src /opt/server/src
 ---> Using cache
 ---> 6d48dd3f9f85
Step 8/9 : RUN mvn -s settings.xml install -P embedded -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -B -e -o -T 1C verify
 ---> Using cache
 ---> 1c109d2026c4
Step 9/9 : CMD mvn -s settings.xml -Dtest="!*IT,!*IntegrationTest" -P jacoco test
 ---> Using cache
 ---> 45eac3094ea4
Successfully built 45eac3094ea4
Successfully tagged generic-dockerfile:tests

Then you can execute the tests:

$ docker run -ti generic-dockerfile:tests
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building spring-cloud-config-server 1.1.6-SNAPSHOT
[INFO] ------------------------------------------------------------------------
  • You can also override the CMD arguments to run the integration tests as documented in the Dockerfile above.

Build and Run app

You can build the runtime image as usual

$ docker build -t generic-dockerfile .

done.
done.
Removing intermediate container e632d7c310f7
 ---> e9391a0ca21d
Step 16/16 : ENTRYPOINT sh -c "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom            $JAVA_PARAMS -jar /runtime/server.jar --server.port=8888 $SPRING_BOOT_APP_OPTS"
 ---> Running in 849ba7ad3212
Removing intermediate container 849ba7ad3212
 ---> 909354984264
Successfully built 909354984264
Successfully tagged generic-dockerfile:latest

Running the application is as simple as the following

$ docker run -ti generic-dockerfile
ThisHost: getLocalHost says localHost="c34b2cedbebf/172.17.0.2" isLoopbackAddress=false
   2018-04-19T19:15:41,180 3166  | INFO  | internal.util.Version.<clinit>#30 ["background-preinit" {}] HV000001: Hibernate Validator 5.2.5.Final
   2018-04-19T19:15:41,470 3456  | INFO  | factory.annotation.AutowiredAnnotationBeanPostProcessor.<init>#155 ["main" {svr=c34b2cedbebf}] JSR-330 'javax.inject.Inject' annotation found and supported for autowiring

Upvotes: 1

Marcello DeSales
Marcello DeSales

Reputation: 22319

Backtracking from the Dockerfile, we could just require to add "app.jar". So, from

ADD gs-spring-boot-docker-0.1.0.jar app.jar

to

ADD app.jar app.jar

This leads to the need of renaming or copying the generated executable Jar. This example renames the executable jar to "app.jar", and so, making it easy for building the docker image. A generic task that can be copied to any SpringBoot app to be built in Gradle can be found below.

build.gradle

/**
 * Generic support for building docker images for SpringBoot Apps
 */
task buildDocker(type: Docker, dependsOn: build) {
  push = false
  applicationName = rootProject.name
  dockerfile = file('src/main/docker/Dockerfile')

  doFirst {
    // Rename the app jar to "app.jar" so that the Dockerfile does not require renames
    copy {
      from "${project.buildDir}/libs"
      into stageDir
      include "${rootProject.name}-${version}.jar"
      rename("${rootProject.name}-${version}.jar", "app.jar")
    }
  }

  doLast {
    println "Run the Docker Container"
    println "docker run -ti -p 8080:8080 $project.group/$applicationName:$version"
  }
} 

The final resulting Dockerfile is as follows:

src/main/docker/Dockerfile

FROM frolvlad/alpine-oraclejdk8:slim
MAINTAINER [email protected]
VOLUME /tmp
ADD app.jar app.jar
RUN sh -c 'touch /app.jar'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

The command "gradle buildDocker" will generate docker images and as bonus, will print the complete command for you to execute the app (note that the default port number is hard-coded and must be changed if you change that value).

Upvotes: 1

Related Questions