Matured Dockerfile for Spring Boot
A demonstration of a layered, production-proof dockerfile for secure spring boot applications
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
- have a decent understanding of Docker.
If not, please read first: https://medium.com/javarevisited/top-5-free-courses-to-learn-docker-for-beginners-best-of-lot-b2b1ad2b98ad - have advanced knowledge about Spring Boot development.
*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.
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.
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:
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.
- The
MultiReleaseException
seems to be becausejdeps
can not handle classes in different jars that have the same name, such asmodule-info.class
But are stored in a differentMETA-INF/versions/xxx
directory. (JDK-8277165) - 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)
- The
MultiReleaseException
is missing its exception message since it's thrown as part of an asynchronous task, which wraps it in anExecutionException
Which then leads tojdeps
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:
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.
- How to Harden Your Containers With Distroless Docker Images,
by Gaurav Agarwal - Which Container Images To Use — Distroless Or Alpine?,
by Tanmay Deshpande
References, Useful links
- 10 best practices to build a Java container with Docker
https://snyk.io/wp-content/uploads/10-best-practices-to-containerize-Java-applications-with-Docker.pdf - Which Version of JDK Should I Use?
https://whichjdk.com/ - Docker Hub: Public Image Registry
https://hub.docker.com/ - Introducing dumb-init, an init system for Docker containers
https://engineeringblog.yelp.com/2016/01/dumb-init-an-init-for-docker.html#:~:text=dumb%2Dinit%20is%20simple%20to,on%20any%20signals%20it%20receives - Which Container Images To Use — Distroless Or Alpine?
https://itnext.io/which-container-images-to-use-distroless-or-alpine-96e3dab43a22 - Layered Jars Spring Boot
- https://spring.io/blog/2020/01/27/creating-docker-images-with-spring-boot-2-3-0-m1
- https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3 - jlink, Java Platform, Standard Edition Tools Reference
https://docs.oracle.com/javase/9/tools/jlink.htm#JSWOR-GUID-CECAC52B-CFEE-46CB-8166-F17A8E9280E9 - jdeps, Java Platform, Standard Edition Tools Reference
https://docs.oracle.com/javase/9/tools/jdeps.htm#JSWOR690 - Spring Boot Docker
https://spring.io/guides/topicals/spring-boot-docker/
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.