Matured Dockerfile for Spring Boot

A demonstration of a layered, production-proof dockerfile for secure spring boot applications

Nikola Stanković
Viascom Publications
7 min readJun 26, 2022

--

Meanwhile, I've invested hours intending to create a production-proof Dockerfile. However, even when I thought the container image had arrived at its final version, I encountered a situation where something was missing/not right implemented or simply not supporting my debug investigations on production bugs. — Following my version of a matured Dockerfile.

Preface

The following article is my production-proof Dockerfile for layered Spring Boot applications with a custom-built JRE depending on the required Java modules using jlink with jdeps, explaining what I added or altered. Of course, I do not guarantee that the Dockerfile perfectly matches your needs, but it should provide you with a framework of ideas and serve you as a mature starting point.

Preconditions

Before we can jump in, there are some preconditions. And I assume that if you found this guide, you do

*I'm no advocate nor paid to list these links here. I don't even earn anything if you click on them. I googled them or had them in my bookmarks. So it is truly here to assist you, chosen by my personal bias as a developer. Feel free to get the required know-how anywhere else, as you desire; disclaimer end.

Quality conditions

I'm aiming to fulfil these quality conditions with the solution described in this article:

  • small image size
  • official support and/or active community
  • low in vulnerabilities and fast in resolving them
  • easy automation, f.e. health checks, log aggregation
  • developer convenience for debugging, thread analyzing possibilities
  • security concerns addressed/ hardening and best practices applied
  • nearly no downtime, fast release time, and build time is secondary
  • easy to understand and extend for developers

Implementation

The following implementation will result in a 92.3 MB small image, where my application placed inside the container makes about 20 MB.

Let's jump into the four main files I created. And underneath, I will explain what made me create it that way.

  • Dockerfile
  • build_application.sh: The purpose of this file is to do everything the application requires to build. The custom JRE is made by determining the used java modules and extracting the jar as the last step to use the spring boot layers.
  • run_application.sh: The purpose of this file is to do everything the application requires to run. Sets wanted environment variables and runs the dumb-init and the application.
  • packages.list: This file contains line by line every Linux package which has to be additionally installed.
Layered Production-ready Dockerfile for Spring Boot Applications
Production-ready Application Build Bash Script for layered Spring Boot Applications
Production-ready Entrypoint Bash Script for layered Spring Boot Applications
Packages file with all additional programs to install

Hello World Application

I used for demonstration purposes my simple spring boot hello-world rest api. Feel free to use it to test anything out if you need a simple program to run.

Detailed implementation explanations

Alpine Linux

I chose Alpine Linux as the base image primarily because it has fewer vulnerabilities compared f.e. to a debian-buster-slim image. Secondly, as I tried to keep the image as small as possible, Alpine comes in handy with his around 5 MB image size.

https://snyk.io/advisor/docker/alpine/3.16.0
https://snyk.io/advisor/docker/debian/buster-slim

Docker+Bash

I used a combination of Docker and Bash to build and run my spring boot application. By doing that, I'm convinced I've achieved having simple technologies that are easy to understand and extend and still powerful with many examples and how-tos on the internet. But mainly as we can have one docker layer for our execution.

jq, curl, dumb-init, jattach, openssl

I added these five programs to make it easier to operate, debug and secure the application for production runs.

  • curl: Alpine Linux, which we use as our base image, doesn’t contain curl but wget. Sure we could switch to this one, but having as a quality goal developer simplicity and an active community, I chose curl, as I encountered during my career more powerful curls than wget calls. Visit the following site for a deeper discussion: What is the Difference Between wget vs curl?
  • dumb-init: Omitting an init system often leads to incorrect handling of processes and signals and can result in problems such as containers which can't be gracefully stopped or leaking containers which should have been destroyed. In most cases, signals won't be adequately handled, and orphaned zombie processes aren't correctly reaped. dumb-init runs as PID 1, acting like a simple init system. It launches a single process and then proxies all received signals to a session rooted at that child process.
  • jq: needed to efficiently implement health checks as we often serve REST-based web services, and spring boot's actuator also responds with JSON. jq is a lightweight and flexible command-line JSON processor. Find more information on jq-s official site: https://stedolan.github.io/jq/
  • jattach: All-in-one jmap + jstack + jcmd + jinfo functionality in a single tiny program. No installed JDK is required, it works with just JRE. Supports Linux containers. I use this program to be able to investigate bugs like memory issues. https://github.com/apangin/jattach
  • openssl: OpenSSL is an open-source command line tool that is commonly used to generate private keys, create CSRs, install your SSL/TLS certificate, and identify certificate information. I use this command-line tool to check my Java KeyStore files. https://www.openssl.org/

By having all of these tools in place, I'm able to run my application, and as my spring boot application has actuator enabled, I can run the following command to implement a health check:

Health check:

health check curl for spring boot applications with actuator

Non-root images

Don't run your applications as root. Docker defaults to running the process in the container as the root user, a precarious security practice. Instead, use a low-privileged user and proper filesystem permissions. This is why my user "exie" was born. I do enjoy this name 🤣.

Known errors

Missing modules

With the approach described in this article of creating a custom JRE by stripping modules, there is always the disadvantage of stripping too much away. It is resulting in a missing module issue.

If this happens to you, please read the log messages carefully to find out which module could not be determined automatically by jdeps and add it manually to the variable REQUIRED_JAVA_MODULES in the build_application.sh file.

In my case, I have often been missing the jdk.crypto.ec module, as I got the following exception: SSLHandshakeException.

The next StackOverflow article helped me out: https://stackoverflow.com/questions/55439599/sslhandshakeexception-with-jlink-created-runtime

MultiReleaseException

If you encounter the com.sun.tools.jdeps.MultiReleaseException: You can either upgrade and use JDK 18 (not recommended, as it is not an LTS version) or get the patched jdeps.

  1. The MultiReleaseException seems to be because jdeps can not handle classes in different jars that have the same name, such as module-info.classBut are stored in a different META-INF/versions/xxx directory. (JDK-8277165)
  2. The fact that this exception is sometimes suddenly not occurring seems to result from a race condition in the code that checks for the above; classes of the same name have multiple versions. (JDK-8277166)
  3. The MultiReleaseException is missing its exception message since it's thrown as part of an asynchronous task, which wraps it in an ExecutionExceptionWhich then leads to jdeps not reporting the exception correctly. (JDK-8277123)

These issues have been fixed, and a patched version of jdeps is available as part of the early access build for JDK 18 at: http://jdk.java.net/18/ (starting from build 26)

COPY fails in multi-stage build: layer does not exist

Multi-stage build fails when a specific sequence of COPY commands is given. It always works if you run it without the option — no-cache a second time. I only experienced it in Linux environments. On Windows, it works just fine.

The following GitHub issues gave me an ugly workaround: https://github.com/moby/moby/issues/37965

Applying the workaround results in the following ugly code:

Ugly workaround for Dockerfile

Additional topics

Consider distroless

Maybe my production fit is not yours. And there is more to consider than just my noted quality conditions. Consider reading the following two articles to try out another approach.

Feedback and updates matter 📝☕. Enjoy my articles? Show support with claps, follows, and coffee donations. I keep all content regularly updated.

Support: ko-fi.com/niksta | Discord: devhotel.io

Disclosure: This article was assisted by ChatGPT (OpenAI) and refined using Grammarly for spelling and style. Visuals created with Midjourney’s AI tool.

--

--

Nikola Stanković
Viascom Publications

Discover the trail to production-grade coding of containerised Spring Boot applications with Kotlin, covering performance, cloud integration and security.