Spring Boot, Docker and Traefik

How to deploy a dockerized Spring Boot app behind the Traefik reverse proxy

Posted on 19 February 2017

In a previous blog post I showed you how I set up a fully dockerized environment for my VPS hosting my blog. Now I will show you the biggest benefit of this set up: fully automated Spring Boot deployments.

Introduction

So let’s take a step back first. I recently moved my blog to a new VPS where I now run everything in Docker containers: the blog (served by Nginx) itself but also Traefik (the reverse proxy) and the CI tool Jenkins. I set it up so that updates to my blog trigger a build in Jenkins. This build uses JBake to create the static HTML from AsciiDoc, create a Docker container and then deploy that docker container through docker-compose.

In this blog post I’m going to show you the biggest benefit of my set up; I can now easily use this VPS to deploy other applications, temporary or permanent, to my VPS.

The application

So I created a small Whoami application inspired by the Traefik examples. It’s an AngularJS single page application backed by a Spring Boot REST service (with a /whoaim endpoint) that shows you your IP, the headers sent by your browser and some other random information. I also added Spring Actuator to automatically add an /info endpoint. You can see it in action here (as long as I haven’t removed it to make room for something else).

The pom.xml is also pretty standard but I made a few changes to the default spring-boot-maven-plugin configuration:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>${spring.boot.version}</version>
            <configuration>
                <finalName>${artifactId}</finalName>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>build-info</goal>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

The first change is that the <finalName> of the jar is simply going to be whoami.jar. This makes it easier to write a Docker file for it because it won’t include the version. The second change is the build-info goal. This instructs the plugin to create a build-info.properties file on the class-path that includes build information such as the version and time of build. This gets automatically picked up by Spring Actuator to create a public /info endpoint that contains my version and build time. A convenient way to check if the deployed service is current!

Actuator does a lot more than just that! Actuator for example provide health endpoints you can check to see if your services are still healthy. In my day job these are used by Kubernetes to see the status of a service.

Dockerfile

We have a small standard Dockerfile taken from this guide for our service:

FROM frolvlad/alpine-oraclejdk8:slim
VOLUME /tmp
ADD target/whoami.jar app.jar
RUN sh -c 'touch /app.jar'
ENV JAVA_OPTS=""
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
Note
This is a very small 'batteries-excluded' alpine-linux based image!

It is based on a small alpine-linux OracleJDK image and just adds whoami.jar that includes the entire Spring Boot app (thanks to spring-boot-maven-plugin) to the image which is then started.

In case you wonder what the /dev/./urandom bit is for: by default Java uses your OS’s secure random entropy source. On linux this is /dev/random. It collects randomness from sources like device driver noise but can quite easily be exhausted leading to slow startups.

docker-compose.yml

Our docker-compose file is very similar to the previous ones we use together with Traefik:

version: '2'

services:
  whoami:
    image: "nielsutrecht/whoami:${TAG}"
    restart: always
    networks:
      - web
    labels:
      - "traefik.backend=whoami"
      - "traefik.frontend.rule=Host: whoami.localhost, whoami.niels.nu"
      - "traefik.port=80"

networks:
  web:
    external:
      name: traefik_webgateway

So again I instruct it to create a 'whoami' back-end, use port 80 (as I configured in my application.yml, the Tomcat default is 8080) and create front-end rules to route all traffic on whoami.localhost and whoami.niels.nu. The restart: always makes sure my service restarts when the VPS restarts. Check out the documentation for more information on restart policies.

Jenkins job

For this service, since it is a Maven project, I’m just going to use a Jenkins Maven build for convenience. The build setup is a fairly standard Maven build: pull a GIT repo and set the Maven build step to package. I then add a "Run only if build succeeds" "execute shell" post step with the following content:

TAG=0.1.$BUILD_NUMBER
IMG=nielsutrecht/whoami
echo "TAG=$TAG" > .env

sudo docker build -t $IMG:$TAG -t $IMG:latest .
sudo docker-compose up -d

This will add the TAG number to an .env file that’s picked up by docker-compose to substitute the TAG placeholder in the docker-compose.yml above. This is done to force it to deploy the latest version. The tag is simply a fixed version (0.1) with the build number appended.

Note
I added the prefix in case I ever have to reinstall Jenkins / install another CI tool and lose the build numbers

I then run a docker build a docker-compose up to build and run the new version.

For this particular project I won’t build it automatically (I did implement this for my blog in the previous blog post) since that bit isn’t needed for experiments like these. I will however create a full CD pipeline, including the triggers, for anything more persistent or project like Hackathons where we work as a team.

Conclusion

To me it’s clear how much a proper CI/CD set up adds value. It makes it much less cumbersome for me to create a temporary project on my VPS. Aside from the learning experience this makes it even more fun to create experiments because I automated the boring parts.

In the next and final episode I am going to enable Let’s Encrypt HTTPS termination in Traefik.