Docker & Spring Boot
Docker allows you to package an application with its dependencies, into a light weight, portable container that can run on almost any environment. You can think of a Docker container as a run time, a mini virtual machine that encapsulates your application and its dependencies.
In order to run a container you need a Docker image. An image is like a template that defines everything that will exist within the container. You can almost think of an container as a run time instance of the image it was created from. In this post we’ll define and build 3 slightly different Docker images that run a simple Java app.
Installing Docker
If you haven’t already done so you’ll need to install Docker. The official documentation is pretty good so following it step by step should see you up and running in about 15 minutes.
I’ve installed Docker on Windows and Ubuntu but to be honest I prefer running it on Ubuntu and have found it a bit more reliable than with Docker Toolbox on Windows.
brianh@brianh-VirtualBox:~/apps$ docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world c04b14da8d14: Already exists Digest: sha256:0256e8a36e2070f7bf2d0b0763dbabdd67798512411de4cdcf9431a1feb60fd9 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker Hub account: https://hub.docker.com For more examples and ideas, visit: https://docs.docker.com/engine/userguide/
Sample Code
We’re going to look at 3 slightly different ways of building a docker image to run a simple Spring Boot app. We’ll start a container from each of the 3 images and call the application health check to make sure the app is up and running.
The Boot app itself couldn’t be simpler, consisting of just an Application class annotated with @SpringBootApplication. This is enough to enable auto configuration and act as the application entry point. We don’t even need to define our own health check as Boot provides one out of the box.
package com.blog.samples.boot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Dockerfile Definition
A Docker file is a set of instructions or steps that tells Docker how to build an image. The Dockerfile below defines steps to build, package and run our app.
FROM anapsix/docker-oracle-java8 # Install maven RUN apt-get update -y RUN apt-get install -y maven # Creating working directory WORKDIR /app # Add src to working directory ADD pom.xml /app/pom.xml ADD src /app/src # Build JAR RUN mvn package -DskipTests=true # Start app ENTRYPOINT ["java","-jar","/app/target/docker-sample-1-0.1.0.jar"]
We’ll walk through the Dockerfile line by line and explain whats happening.
- Line 1 – FROM instruction tells Docker what base image we want to use as a starting point for our image. I’ve used anapsix/docker-oracle-java8 which is a lightweight image for Java 8 running on Ubuntu.
- Line 4 – RUN instruction tells Docker to run a command, in this case apt-get update -y to update the apt package list in preparation for the next step.
- Line 5 – tells Docker to run apt-get install -y to download and install maven on the image. The image will use Maven to build our app.
- Line 8 – WORKDIR command tells Docker to create a working directory on the image. This directory will be used by the ADD and RUN commands that are defined below.
- Line 11 – ADD command tells Docker to add the application POM from the host machine, to the app directory on the image.
- Line 12 – tells Docker to add the application source from the host machine to /app/src on the image. At this point the image has everything it need to build the project.
- Line 15- tells Docker to run the mvn package -DskipTests=true command from the /app directory on the image. Maven will download all required dependencies and build an executable jar in the /app/target directory.
- Line 18 – ENTRYPOINT tells Docker what command to run when the container is started. The comma separated list of values consists of an executable (java in our instance) and a number of parameters. The entry point defined here tells Docker to run the executable JAR from the /app/target directory.
Creating the Image
brianh@brianh-VirtualBox:~/apps/docker-spring-boot/docker-sample-1$ docker build -t "docker-sample-1" . Sending build context to Docker daemon 12.83 MB Step 1 : FROM anapsix/docker-oracle-java8 ---> a8a9dcb0ac64 Step 2 : RUN apt-get update -y ---> Running in 5d477ccb8f46 Ign http://archive.ubuntu.com trusty InRelease Get:1 http://ppa.launchpad.net trusty InRelease [15.5 kB] Get:2 http://archive.ubuntu.com trusty-updates InRelease [65.9 kB] Get:3 http://archive.ubuntu.com trusty-security InRelease [65.9 kB] Hit http://archive.ubuntu.com trusty Release.gpg Hit http://archive.ubuntu.com trusty Release // Lots of output from apt update and maven build removed for brevity [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 9:06:42.103s [INFO] Finished at: Thu Jul 21 16:24:04 UTC 2016 [INFO] Final Memory: 21M/107M [INFO] ------------------------------------------------------------------------ ---> b362beb8e90d Removing intermediate container 512fd61457e6 Step 8 : RUN ls /app/target ---> Running in 7eba00207f50 classes docker-sample-1-0.1.0.jar docker-sample-1-0.1.0.jar.original generated-sources maven-archiver maven-status ---> 613a5f6ef5bb Removing intermediate container 7eba00207f50 Step 9 : ENTRYPOINT java -jar /app/target/docker-sample-1-0.1.0.jar ---> Running in e43707589d45 ---> 2e4f625b102e Removing intermediate container e43707589d45 Successfully built 2e4f625b102e
Docker runs each instruction in the Dockerfile step by step. Step 1 in this instance runs quickly because I already have this image cached locally. When you’re building this for the first time you likely wont have the anapsix/docker-oracle-java-8 image, so Docker will pull it from the Docker Hub repository. Subsequent builds will use the local cached image and as a result will run much quicker. For each step Docker does the following
- creates a new intermediate container
- runs the command inside that container
- commits the change as a new image layer
- removes the intermediate container and moves to the next step
The new image consists of multiple layers stacked one on top of the other, one for each instruction in the Dockerfile. Run the docker images command to see the newly created image.
brianh@brianh-VirtualBox:~/apps$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE docker-sample-1 latest 2e4f625b102e 33 minutes ago 936 MB anapsix/docker-oracle-java8 latest a8a9dcb0ac64 3 weeks ago 784.5 MB
Note the Image ID is the same as that output at the end of the build. To see the various layers that make up the new image run the docker history command as follows.
brianh@brianh-VirtualBox:~/apps$ docker history docker-sample-1 IMAGE CREATED CREATED BY SIZE COMMENT 2e4f625b102e 37 minutes ago /bin/sh -c #(nop) ENTRYPOINT ["java" "-jar" " 0 B 613a5f6ef5bb 37 minutes ago /bin/sh -c ls /app/target 0 B b362beb8e90d 37 minutes ago /bin/sh -c mvn package -DskipTests=true 37.51 MB 0f3103ac18be 9 hours ago /bin/sh -c #(nop) ADD dir:82830cfed5011783b44 1.276 kB 73ccd6348460 9 hours ago /bin/sh -c #(nop) ADD file:cbad7ca7f8efa76f28 1.349 kB 22f2ab199dd7 9 hours ago /bin/sh -c #(nop) WORKDIR /app 0 B 5e3e8435f2b4 9 hours ago /bin/sh -c apt-get install -y maven 92.07 MB 6c352c184d38 9 hours ago /bin/sh -c apt-get update -y 21.9 MB a8a9dcb0ac64 3 weeks ago /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jvm/ 0 B <missing> 3 weeks ago /bin/sh -c apt-get update && DEBIAN_FRONTEND= 583.6 MB <missing> 3 weeks ago /bin/sh -c apt-key adv --keyserver keyserver. 25.18 kB <missing> 3 weeks ago /bin/sh -c echo "deb http://ppa.launchpad.net 65 B <missing> 3 weeks ago /bin/sh -c echo "oracle-java8-installer share 2.677 MB <missing> 3 weeks ago /bin/sh -c #(nop) ENV LC_ALL=en_US.UTF-8 0 B <missing> 3 weeks ago /bin/sh -c #(nop) ENV LANG=en_US.UTF-8 0 B <missing> 3 weeks ago /bin/sh -c locale-gen en_US.UTF-8 1.621 MB <missing> 3 weeks ago /bin/sh -c #(nop) MAINTAINER Anastas Dancha " 0 B <missing> 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B <missing> 3 weeks ago /bin/sh -c sed -i 's/^#s*(deb.*universe)$/ 1.895 kB <missing> 3 weeks ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0 B <missing> 3 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /u 8.841 MB
Note that lines 3 to 11 list the image layers that were added as a result of each instruction executed in our Dockerfile.
Running the Container
brianh@brianh-VirtualBox:~/apps/docker-spring-boot/docker-sample-1$ docker run -p 8080:8080 docker-sample-1 . ____ _ __ _ _ /\ / ___'_ __ _ _(_)_ __ __ _ ( ( )___ | '_ | '_| | '_ / _` | \/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |___, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.2.7.RELEASE) 3051 [main] INFO com.blog.samples.boot.Application - Starting Application v0.1.0 on 81828366ca4e with PID 1 (/app/target/docker-sample-1-0.1.0.jar started by root in /app) 3382 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@66429cee: startup date [Thu Jul 21 17:15:31 UTC 2016]; root of context hierarchy 6707 [main] INFO o.s.b.f.s.DefaultListableBeanFactory - Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]] 8176 [main] INFO o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.1.3.Final 10328 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http) 11235 [main] INFO o.a.catalina.core.StandardService - Starting service Tomcat When the container starts it runs the ENTRYPOINT command java -jar /app/target/docker-sample-1-0.1.o.jar specified in the Dockerfile, The a
When the container starts it runs the ENTRYPOINT command java -jar /app/target/docker-sample-1-0.1.o.jar specified in the Dockerfile, The application will start on container port 8080 and Docker will bind to port 8080 on the host.
Testing the Application
brianh@brianh-VirtualBox:~/apps/docker-spring-boot/docker-sample-1$ curl -i localhost:8080/health HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Application-Context: application Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 21 Jul 2016 17:15:54 GMT
Other Examples
Example two is a simplified version of the first example, and simply adds the app JAR to the container and runs the app. In this instance you build and package the app on the host and simply copy the JAR into the container. This keeps the image slightly lighter as it doesn’t have to create a maven repository like the first example did.
FROM anapsix/docker-oracle-java8 # Creating working directory WORKDIR /app # Add src to working directory ADD target/docker-smaple-2-0.1.0.jar /app/docker-sample-2-0.1.0.jar # Start app ENTRYPOINT ["java","-jar","/app/docker-sample-2-0.1.0.jar"]
Example 3 is a slightly different variation again. Rather than using the ADD command to copy the application artifact from the host machine, we use pass a URL to the ADD command to pull the artifact from a repository. I’ve used S3 in this example but you could pull your app from a CI server like Team City, Jenkins or anywhere else you please.
FROM anapsix/docker-oracle-java8 # Creating working directory WORKDIR /app # Pull artifact from repo and add to working directory ADD https://s3-us-west-2.amazonaws.com/docker-boot-artifact/docker-sample-3-0.1.0.jar /app/docker-sample-3-0.1.0.jar # Start app ENTRYPOINT ["java","-jar","/app/docker-sample-3-0.1.0.jar"]
Leave A Comment