Create your FREE Codefresh account and start making pipelines fast. Create Account

Webinar: Creating an Efficient Docker Build Pipeline for Java Apps

6 min read

In our last webinar, we showed how to create an efficient Docker build pipeline for Java apps. We found that a lot of “Docker for Java developers” tutorials out there unintentionally encourage some Docker bad practices. So we had our Cheif Researcher, Alexei Ledenev, demonstrate how to craft the perfect Java-Docker build flow to consistently produce small, efficient, and secure Docker images.

View the webinar on-demand to learn:

  • The Docker Builder Pattern
  • How to define all pipeline steps and Dockerize them
  • How to pass build context between steps
  • How to Automate your build pipeline
  • Java tips/ best practices for each step

Link to Alexei’s slides

Highlights of Alexei’s Talk:

Common Pitfalls

Most new Docker users start with a “Naive Approach” when Dockerizing their application and use a familiar VM build and install flow. In fact, there are many blog posts and articles teach you to do this. They usually start with some kind of Linux image, and first install all required packages, Oracle JDK, etc. Then they copy all project files into this box, run maven install, and define the command line or how the app should be executed. This is a straight forward process but what you’re actually left with is a huge image size. In this case, the image is 1.3 GB in size and took me 30 minutes to build on my laptop.

# start from ubuntu
FROM ubuntu:14.04

# add required packages and java repository
RUN apt-get update && apt-get install -y python-software-properties software-properties-common
RUN add-apt-repository ppa:webupd8team/java

# install Oracle JDK 8 with auto-accept license agreement
RUN echo "oracle-java8-installer shared/accepted-oracle-license-v1-1 boolean true" | debconf-set-selections
RUN apt-get update && apt-get install -y oracle-java8-installer maven

# add ALL project files
ADD . /usr/local/app

# build Java application with Maven (fetch packages, compile, test and deploy)
RUN cd /usr/local/app && mvn install

# define default command to run the application
CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=test", "/usr/local/app/target/spring-boot-rest-example-0.3.0.war"]Anther approach “standard” most people use is take image library, prepared images, install maven, when do one trick -add own maven project file, and install all dependencies, for every build docker tries to recreate this layer, if file had not been changed it will use cache from the previous build, in this case if didn’t download diff dependencies it will be
Another approach many people use is the “Standard Approach.” Meaning they take an official Docker image provided (by a company or community), in this example, it’s an OpenJDK image, they install maven, do one little trick -add a pom.xml file, and then install all dependencies. This step is important because each command in a Dockerfile creates an additional layer. Docker will try to re-create this layer with each build. But if you add the pom.xml file it will use the cache from the previous build if nothing has changed.  This allows us to slightly reduce the image size to 1.2 GB. It’s still big but slightly smaller than the last and now our build time is around 15 minutes.
FROM java:8 

# Install maven
RUN apt-get update
RUN apt-get install -y maven

WORKDIR /code

# Prepare by downloading dependencies
ADD pom.xml /code/pom.xml
RUN ["mvn", "dependency:resolve"]
RUN ["mvn", "verify"]

# Adding source, compile and package into a fat jar
ADD src /code/src
RUN ["mvn", "package"]

EXPOSE 4567
CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=test", "target/spring-boot-rest-example-0.3.0.war"]
It’s important to understand containers are not VMs. 

“A Linux container is nothing more than a process that runs on Linux. It shares a host kernel with other containerized processes.” –Joe Fernandes, Senior Director, OpenShift Product Management, Red Hat

Once you think of your application as a process, you need to ask -what is the bare minimum that is required to run my application? Using Docker technology you can pack all the files and tools needed to create your application process (the resource files, configuration files, etc). You can isolate the entire process in a separate container so that your app feels like it’s running alone.

What is the bare minimum required to run Java App?

In this example, we need to package the following in order to create the process to run our Java application:
  1. Base Image with C Runtime and Posix shell (Alexei recommends Alpine)
  2. Java Runtime Environment (OpenJDK JRE)
  3. Application byte-code and resources (app.jar)
  4. 3rd Party Libraries (lib/*.jar)
  5. Optionally HTTP server (Alexei recommends Tomcat, Jetty, or Netty, not enterprise servers since their design wasn’t built for containers)

Why is your Docker image size important?

  • Time to build
  • Network latency
  • Storage
  • Service availability and elasticity
  • Security
  • Development agility

 Docker Builder Pattern

Alexei explained how to use the Docker builder pattern to separate our build tools and runtime.

Java Docker Builder

Our Java Docker builder should contain the following:

  1. Base Image with C Runtime and Posix shell (Alpine)
  2. Java Development Kit (OpenJDK)
  3. Javac or other JVM compiler (Scala, Kotlin, …)
  4. Build Management Tool (Maven, SBT, Gradle, …)
  5. Linters, code scanners, test frameworks, test tools,
 Once you Dockerize your build environment you can deploy it and use it and run your app on any machine without the need to install anything manually. This eliminates the “it works on my machine” problem.

Maven Builder Dockerfile

Our builder Dockerfile looks like this:
FROM openjdk:8-jdk-alpine

RUN apk add --no-cache curl tar bash

ARG MAVEN_VERSION=3.3.9
ARG USER_HOME_DIR="/root"

RUN mkdir -p /usr/share/maven && \
  curl -fsSL http://apache.osuosl.org/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz | tar -xzC /usr/share/maven --strip-components=1 && \
  ln -s /usr/share/maven/bin/mvn /usr/bin/mvn

ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
# speed up Maven JVM a bit
ENV MAVEN_OPTS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"

ENTRYPOINT ["/usr/bin/mvn"]

# make source folder
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# install maven dependency packages (keep in image)
COPY pom.xml /usr/src/app
RUN mvn -T 1C install && rm -rf target

# copy other source files (keep in image)
COPY src /usr/src/app/src
Once we capture our build process, we can consistently produce the results (it captures the source code at a specific point in time so we won’t be impacted by any changes).

Java App Dockerfile

Our Java application Dockerfile is pretty simple because we are only including the bare minimum we need to run it. In this example, we only need the following 3 lines of code.
FROM openjdk:8-jre-alpine

COPY spring-boot-*.war /app.war

CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=test", "/app.war"]
So now our builder image is only 263 MB, and our runtime image is 143 MB. This is almost 10x smaller the 1st image.

Build Pipeline Orchestration

Once you have both Dockerfiles ready, you can automate your build pipeline.
Alexei explains how to automate it with a Makefile like this:
…

builder:
    docker build -t $(NS)/builder:mvn -f Dockerfile.build .

mvn-package: builder
    docker run -it --rm -v $(shell pwd)/target:/usr/src/app/target $(NS)/builder:mvn package -T 1C -o -Dmaven.test.skip=true

mvn-test: builder
    docker run -it --rm -v $(shell pwd)/target:/usr/src/app/target $(NS)/builder:mvn -T 1C -o test

docker: 
    docker build -t $(NS)/$(REPO):$(VERSION) target

build: builder
    make docker

push:
    docker push $(NS)/$(REPO):$(VERSION)

release: build
    make push -e VERSION=$(VERSION)

…

default: build

Demo

In the demo, Alexei shows how to easily automate your pipeline using a Docker-native CI/CD like Codefresh to consistently produce small, efficient, and secure Docker images.

(See demo – 32 minutes 25 seconds of the webinar)

He shows two ways to automate your pipeline in Codefresh.

 

  1. Using a Codefresh YAML file like this:
    version: '1.0'
    
    steps:
    
      mvn_builder:
        type: build
        description: create Maven builder
        dockerfile: Dockerfile.build
        image_name: alexeiled/mvn-builder
    
      mvn_test:
        description: run unit tests 
        image: ${{mvn_builder}}
        commands:
          - mvn -T 1C -o test
      
      mvn_package:
        description: package application WAR 
        image: ${{mvn_builder}}
        commands:
          - mvn package -T 1C -o -Dmaven.test.skip=true
    
      build_image:
        type: build
        description: create Docker image with application WAR
        dockerfile: Dockerfile
        working_directory: ${{main_clone}}/target
        image_name: alexei-led/sbdemo
    
      image_push:
        type: push
        description: push to DockerHub
        candidate: '${{build_image}}'
        tag: ‘${{CF_BRANCH}}'
    
  2. Using the Codefresh UI
The Codefresh cache allows us to speed up the build process. After all the tests have passed, Codefresh takes the produced VAR files and packages them into a final Docker image. It then tags and labels the image with the git branch and commit info, and pushes it to Alexei’s private Codefresh registry (or any registry you choose).

Additional Resources

Dan Garfield

Dan is the Co-Founder and Chief Open Source Officer at Codefresh. His work in open source includes being an Argo Maintainer, and co-chair of the GitOps Working Group. As a technology leader with a background in full-stack engineering, evangelism, and communications, he led Codefresh's go-to-market strategy and now leads open source strategy. You can follow him at twitter.com/todaywasawesome

Leave a Reply

* All fields are required. Your email address will not be published.