Build springboot application together with docker image

Docker is a quite nice tool for build images and running them in a docker context. As a first step I will create a springboot application with an in memory database that provides a webservice interface for simple adding, deleting and displaying user data. This application will be compiled, packaged and married with a docker image withing maven process. The dockerfile is also created within maven by filtering a dockerfile template with application properties.

After successful maven build this image will be executed in a docker container, the port for the webservice will be mapped to the outer world and the logfile of the application will appear in the host system. The complete setup looks like this:
overview

Versions

component version download
Ubuntu 14.04 http://www.ubuntu.com/download/desktop
Java Hotspot 1.8.0_72 http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
Docker 0.10.2 https://docs.docker.com/
Maven 3.3.9 https://maven.apache.org/download.cgi

The required project setup is following:
eclipse-project

The pom.xml simply consists of a minimal springboot context and spotify’s docker plugin. Additionally there is an maven-ant execution for filtering the Dockerfile and to attach the dockerfile as build artifact with buildhelper-maven plugin. Furthermore for this example the dependency to H2 in memory database is set to provide a development ready persistence library.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.3.3.RELEASE</version>
	</parent>
	<groupId>eu.christophburmeister.playground</groupId>
	<artifactId>userservice</artifactId>
	<version>0.1-SNAPSHOT</version>

	<build>
		<plugins>
			<plugin>
				<artifactId>maven-antrun-plugin</artifactId>
				<executions>
					<execution>
						<id>process-docker-files</id>
						<phase>process-resources</phase>
						<configuration>
							<target>
								<echo message="processing the parametrized dockerfile." />
								<filter token="application.jar" value="${project.build.finalName}.jar" />
								<filter
									filtersfile="${project.build.directory}/classes/application.properties" />
								<copy todir="${project.build.directory}/docker-resources"
									filtering="true">
									<fileset dir="${project.basedir}/src/docker" />
								</copy>
							</target>
						</configuration>
						<goals>
							<goal>run</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>

			<!-- By default the plugin will try to connect to docker on localhost:2375. -->
			<!-- Set the DOCKER_HOST environment variable to connect elsewhere. -->
			<plugin>
				<groupId>com.spotify</groupId>
				<artifactId>docker-maven-plugin</artifactId>
				<version>0.4.2</version>
				<configuration>
					<imageName>${project.artifactId}</imageName>
					<dockerDirectory>${project.build.directory}/docker-resources</dockerDirectory>
					<resources>
						<resource>
							<targetPath>/</targetPath>
							<directory>${project.build.directory}</directory>
							<include>${project.build.finalName}.jar</include>
						</resource>
					</resources>
				</configuration>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>build</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>build-helper-maven-plugin</artifactId>
				<executions>
					<execution>
						<id>attach-dockerfile</id>
						<phase>package</phase>
						<goals>
							<goal>attach-artifact</goal>
						</goals>
						<configuration>
							<artifacts>
								<artifact>
									<file>${project.build.directory}/docker-resources/Dockerfile</file>
									<type>txt</type>
									<classifier>dockerfile</classifier>
								</artifact>
							</artifacts>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</dependency>
	</dependencies>

</project>

First the model that we’re building on. This is a simple Pojo that is going to represent a table in the relational database, mapped by Hibernate.

package eu.christophburmeister.playground.userservice;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "users")
public class User {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private long id;

	@NotNull
	private String email;

	@NotNull
	private String name;

	/** Ctor. */
	public User() {
		// nop
	}

	public User(long id) {
		this.id = id;
	}

	public User(String email, String name) {
		this.email = email;
		this.name = name;
	}

	public long getId() {
		return id;
	}

	public void setId(long value) {
		this.id = value;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String value) {
		this.email = value;
	}

	public String getName() {
		return name;
	}

	public void setName(String value) {
		this.name = value;
	}
}

The repository (or DAO layer) gets into the app via CrudRepository extension. This repository will be seamlessly mapped to a jpa implementation. In this case, the dependency section in pom.xml will push the H2 in memory db into the classpath, from where the configuration in application.properties will pick up the correct class for the datasource.

package eu.christophburmeister.playground.userservice;

import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, Long> {

}

The Controller is the heart of each application that gets requests via webservices. The guys from spring have extremely simplified the handling and writing of these Controllers, so the below lines contain complete implementation of the three operations in this service. The UserRepository for accessing the data is seamlessly pushed into this Controller via Autowiring mechanism.

package eu.christophburmeister.playground.userservice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import org.apache.log4j.Logger;

@Controller
public class UserController {

	@Autowired
	private UserRepository userRepository;

	private static Logger logger = Logger.getLogger(UserController.class);

	/**
	 * /create --> Create a new user and save it in the database.
	 * 
	 * @param email
	 *            User's email
	 * @param name
	 *            User's name
	 * @return A string describing if the user is succesfully created or not.
	 */
	@RequestMapping("/create")
	@ResponseBody
	public String create(String email, String name) {
		logger.info("received create-request with email " + email + " and name " + name);
		User user = null;
		try {
			user = new User(email, name);
			userRepository.save(user);
		} catch (Exception ex) {
			return "Error creating the user: " + ex.toString();
		}
		return "User succesfully created! (id = " + user.getId() + ")";
	}

	/**
	 * /delete --> Delete the user having the passed id.
	 * 
	 * @param id
	 *            The id of the user to delete
	 * @return A string describing if the user is succesfully deleted or not.
	 */
	@RequestMapping("/delete")
	@ResponseBody
	public String delete(long id) {
		logger.info("received delete-request with id " + id);
		try {
			User user = new User(id);
			userRepository.delete(user);
		} catch (Exception ex) {
			return "Error deleting the user:" + ex.toString();
		}
		return "User succesfully deleted!";
	}

	/**
	 * /show --> show all users in the database. just for demonstration. Of
	 * course this could be made nicer!
	 * 
	 * @return A string that contains all the users in the database.
	 */
	@RequestMapping("/show")
	@ResponseBody
	public String show() {
		logger.info("received show-request");
		Iterable<User> users = null;
		try {
			users = userRepository.findAll();
		} catch (Exception ex) {
			return "Error deleting the user:" + ex.toString();
		}

		String usersStringified = "<table border='1'><tr><td>id</td><td>name</td><td>email</td></tr>";
		for (User user : users) {
			usersStringified += "<tr><td>" + user.getId() + "</td><td>" + user.getName() + "</td><td>" + user.getEmail()
					+ "</td></tr>";
		}
		usersStringified += "</table>";
		return usersStringified;
	}
}

Finally the entrypoint for the springboot application:

package eu.christophburmeister.playground.userservice;

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);
	}

}

The application.properties is the central place for all relevant springboot configuration values. In this case the datasource and some logging values are defined here. Note, that I rely on springboot’s internal logging mechanism where the default logfile name is „spring.log“ that will be created in /var/log. This becomes quite important when handling this application within a docker container.

# the application itself
server.port = 8080

# the datasource
spring.datasource.url = jdbc:h2:mem:tmpdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName = org.h2.Driver

# jpa/hibernate
spring.jpa.show-sql = false
spring.jpa.hibernate.ddl-auto = update
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.database-platform = org.hibernate.dialect.H2Dialect

logging.level.root=INFO
logging.path = /var/log

Finally the parametrized Dockerfile. This file can be enriched with multiple parameter that can be replaced (filtered) while maven build. However, this file is used by spotify’s docker-maven-plugin to package the application into a docker image.
First of all the base image is set to Ubuntu, the maintainer is me and the springboot application executable jar is added as resource. As our application needs a specific Java Runtime, we update our apt lists, install wget and then fetch the desired jdk from Oracle. Of course, we could rely on OpenJDK, but this is just to show, how a docker image can be created.
After wgetting the jdk, it is getting unzipped and installed. Finally the application port (filtered by maven using the application.properties of springboot) is exposed, so inside the container, the application can be reached. When starting a container with this image, this exposed port will be mapped to another port, so the application in the container can be reached from outside the container.

# Set the base image to Ubuntu
FROM ubuntu

# Set the maintainer
MAINTAINER Christoph Burmeister

# Add the application
ADD /@application.jar@ /

# Update the repository sources list
RUN apt-get update

# Install wget
RUN apt-get -fy install wget

# Create jdk directory and enter that directory
RUN mkdir /opt/jdk
RUN cd /opt/jdk

# Download Java package from Oracle
RUN wget --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u5-b13/jdk-8u5-linux-x64.tar.gz

# Untar the package
RUN tar -zxf jdk-8u5-linux-x64.tar.gz -C /opt/jdk

# Set Oracle's Java as default
RUN update-alternatives --install /usr/bin/java java /opt/jdk/jdk1.8.0_05/bin/java 100

# Show java version
RUN java -version

# Expose the application port
EXPOSE @server.port@

# Set default container command
ENTRYPOINT ["java", "-jar", "/@application.jar@"]

After running this project with „maven clean install“ the normal maven lifecycle is executed and the docker-maven-plugin will use the docker daemon to create the image and to commit the image to the local docker repository. Note, therefor the docker daemon must be startet and reachable from Maven. This is only suitable on environments where the buildprocess runs on the same machine as the docker installation and the maven executing user is in the docker group. Otherwise it’s getting complicated with executing maven as root user. Normally one will rely on a separate docker repository (aka docker registry) to hold images and pull them on different environments. This will be topic of one of my next posts.

The last part of the maven output looks like this:

[...]
Step 11 : RUN java -version
 ---> Running in b31800781b3d
java version "1.8.0_05"

Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

 ---> e4ab99f78e95
Removing intermediate container b31800781b3d
Step 12 : EXPOSE 8080
 ---> Running in 5b79084bed95
 ---> 6e36d155d80c
Removing intermediate container 5b79084bed95
Step 13 : ENTRYPOINT java -jar /userservice-0.1-SNAPSHOT.jar
 ---> Running in b63ed26add0a
 ---> 717df7ce7063
Removing intermediate container b63ed26add0a
Successfully built 717df7ce7063
[INFO] Built userservice
[INFO] 
[INFO] --- build-helper-maven-plugin:1.9.1:attach-artifact (attach-docker-artifacts) @ userservice ---
[INFO] 
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ userservice ---
[INFO] Installing /home/christoph/jtools/eclipse-workspace/user-service/target/userservice-0.1-SNAPSHOT.jar to /home/christoph/jtools/apache-maven-3.3.9-repo/eu/christophburmeister/playground/userservice/0.1-SNAPSHOT/userservice-0.1-SNAPSHOT.jar
[INFO] Installing /home/christoph/jtools/eclipse-workspace/user-service/pom.xml to /home/christoph/jtools/apache-maven-3.3.9-repo/eu/christophburmeister/playground/userservice/0.1-SNAPSHOT/userservice-0.1-SNAPSHOT.pom
[INFO] Installing /home/christoph/jtools/eclipse-workspace/user-service/target/docker-resources/Dockerfile to /home/christoph/jtools/apache-maven-3.3.9-repo/eu/christophburmeister/playground/userservice/0.1-SNAPSHOT/userservice-0.1-SNAPSHOT-dockerfile.txt
[INFO] Installing /home/christoph/jtools/eclipse-workspace/user-service/target/docker-resources/run-container.sh to /home/christoph/jtools/apache-maven-3.3.9-repo/eu/christophburmeister/playground/userservice/0.1-SNAPSHOT/userservice-0.1-SNAPSHOT-runner.sh
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 02:28 min
[INFO] Finished at: 2016-03-10T23:05:51+01:00
[INFO] Final Memory: 40M/493M

with „docker images“ you can find the committed images in the local registry:

christoph@apollo:~/jtools/eclipse-workspace/userservice$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
userservice         latest              717df7ce7063        About an hour ago   725.7 MB
<none>              <none>              f5650b6634e5        About an hour ago   725.7 MB
<none>              <none>              9c0691e8b9f3        6 hours ago         725.7 MB
<none>              <none>              d3539f696790        6 hours ago         725.7 MB
<none>              <none>              8da820cc1d4c        6 hours ago         725.7 MB
<none>              <none>              838f220a7a78        24 hours ago        726.7 MB
<none>              <none>              a5ab37b7a297        25 hours ago        727.2 MB
[...]

So our desired image id is the latest userservice: 717df7ce7063. With this information we can create our docker run command:

run -d -v /home/christoph/logs:/var/log -p 127.0.0.1:18080:8080 717df7ce7063

What das this mean?

  • run -> ok that is simply the start command
  • -d -> detached mode, the container is created and started and then detached from the console
  • -v -> volume mapping. The directory /var/log inside the container is mapped to /home/christoph/logs on the host. This is important for the logs that are created by the application in /var/log inside the container (/path/on/host:/path/in/container)
  • -p -> port mapping. This means, that port 8080 inside the container is mapped to 18080 outside the container and additionally this is only reachable from 127.0.0.1. You might change this to 0.0.0.0 when running this in production. Otherwise the service in the container can only be reached from localhost.
  • 717df7ce7063 -> the id of our image

When the container has been started without errors, you can use the docker CLI to check that it is really running by calling „docker ps“

christoph@apollo:~/jtools/eclipse-workspace/user-service$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                       NAMES
ea67c08320e8        717df7ce7063        "java -jar /userservi"   58 minutes ago      Up 58 minutes       127.0.0.1:18080->8080/tcp   loving_hodgkin

The container with our image 717df7ce7063 is running and has the id ea67c08320e8.
With this id you can for example inspect the container via docker CLI:

christoph@apollo:~/jtools/eclipse-workspace/user-service$ docker inspect ea67c08320e8
[
    {
        "Id": "ea67c08320e8111ca22fa57ae4beda7ddf8c7c01eca40960fd98537aad0b572b",
        "Created": "2016-03-10T22:21:19.035290073Z",
        "Path": "java",
        "Args": [
            "-jar",
            "/userservice-0.1-SNAPSHOT.jar"
        ],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
[...]

or you can simply stop the container via „docker stop ea67c08320e8“.

But to the application itself: When you point your browser to „http://localhost:18080/show“ you will get an empty table as there are no users yet in the H2 in memory database our application is using for „persistence“.
show_empty

Adding several users by calling http://localhost:18080/create?name=value&email=value will fill this database.
show_full

And thanks to the volume mapping we can also watch the processing of our application in logfile that can be found on the host in /home/christoph/logs

christoph@apollo:~/jtools/eclipse-workspace/user-service$ tail -f /home/christoph/logs/spring.log 
2016-03-10 22:21:49.648  INFO 1 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 34 ms
2016-03-10 22:21:49.726  INFO 1 --- [http-nio-8080-exec-1] e.c.p.userservice.UserController         : received show-request
2016-03-10 23:38:29.521  INFO 1 --- [http-nio-8080-exec-4] e.c.p.userservice.UserController         : received show-request
2016-03-10 23:39:20.578  INFO 1 --- [http-nio-8080-exec-7] e.c.p.userservice.UserController         : received create-request with email luke@rebels.com and name luke
2016-03-10 23:39:34.787  INFO 1 --- [http-nio-8080-exec-8] e.c.p.userservice.UserController         : received create-request with email luke@alderaan.eu and name leia
2016-03-10 23:40:00.556  INFO 1 --- [http-nio-8080-exec-9] e.c.p.userservice.UserController         : received delete-request with id 2
2016-03-10 23:40:03.182  INFO 1 --- [http-nio-8080-exec-10] e.c.p.userservice.UserController         : received show-request
2016-03-10 23:40:24.059  INFO 1 --- [http-nio-8080-exec-2] e.c.p.userservice.UserController         : received create-request with email leia@alderaan.com and name leia
2016-03-10 23:40:40.255  INFO 1 --- [http-nio-8080-exec-3] e.c.p.userservice.UserController         : received create-request with email jarjar@noreply.com and name jarjar
2016-03-10 23:40:44.693  INFO 1 --- [http-nio-8080-exec-4] e.c.p.userservice.UserController         : received show-request

This complete docker environment is heavily interesting, so further posts around this topic will come 😉