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.    
Follow the links above to install docker on your OS of choice. Once you're done, open a terminal window and run docker run hello-world to check that your docker install is working as expected.   

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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/

This command pulls the hello-world image from the Docker hub repository and uses the image to start a container. If you see the output shown above you'll know your docker installation was successful.

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. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 10 - ADD command tells Docker to add the application POM from the host machine, to the app directory on the image.
  • Line 11 - 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 

Now that we've defined a Dockerfile lets put it to work by building an image. Run the docker build command specifying an image name and the location of the Dockerfile. For example docker build -t "docker-sample-1" .

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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.

1
2
3
4
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. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 

Now that we've built the image we're ready to start a container using the docker run command. When the container starts it will launch the Java app on container port 8080. We need to tell the Docker container to map port 8080 to a port on the host machine so that we can access the application running inside the container. We do this using the -p 8080:8080 argument as part of the docker run command.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 application will start on container port 8080 and Docker will bind to port 8080 on the host.

Testing the Application

To test that the app is up and running we can call the app health check using a simple curl command. We should see some activity in the logs and receive a HTTP 200 response.

1
2
3
4
5
6
7
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  

Next we'll look at 2 more Docker examples that are slight variations of the one above. I won't describe these in the same detail but feel free to pull them from Github and have a play around.

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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"]



Source Code  

The source code for each of these examples is on Github and split into 3 separate projects. Pull the code down, play around with it and if you have any comments, questions or suggestions just leave a note below.

Comments

Popular posts from this blog

Health Checks, Metrics & More with Spring Boot Actuator

Spring Web Services Tutorial

Spring Boot & Amazon Web Services (EC2, RDS & S3)

Spring JMS Tutorial with ActiveMQ

Axis2 Web Service Client Tutorial

REST Endpoint Testing With MockMvc

Java Concurrency - Multi Threading with ExecutorService

An Introduction to Wiremock

Spring Batch Tutorial

Externalising Spring Configuration