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 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.
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.
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.
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, build time is secondary
- easy to understand and extend for developers
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 main three files I created. And underneath, I will explain what made me create it that way.
- 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,
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.
GitHub — botscripter/hello-world: Java hello-world application with spring boot
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or…
Detailed implementation explanations
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.
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
I added these four programs to make it easier to operate, debug and secure the application for production runs.
- 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-initruns 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.
- 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?
- 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, 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
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:
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 🤣.
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
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.
MultiReleaseExceptionseems to be because
jdepscan not handle classes in different jars that have the same name, such as
module-info.classBut are stored in a different
- 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)
MultiReleaseExceptionis missing its exception message since it's thrown as part of an asynchronous task, which wraps it in an
ExecutionExceptionWhich then leads to
jdepsnot 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:
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
I’m aiming to create living content; I will regularly review all my articles and keep them up to date. Loving our now standard way of working, which is agile, I’d love to receive feedback, either positive or negative points are always welcome; therefore, please leave a comment.
I’m also constantly trying to live from the earnings I make by doing stuff I sincerely love ❤️. Writing tech articles 📝 become one of those things I’m passionate about. If my article supported you, I would be honoured to receive a few claps and a follow.
Your support would enable me to spend even more time writing such articles and boost my motivation. Follow me here on Medium for more tech articles and bonus material 😃.
Following is the link to support me or to say thanks by buying me a coffee: https://ko-fi.com/botscripter
References, Useful links
- 10 best practices to build a Java container with Docker
- Which Version of JDK Should I Use?
- Docker Hub: Public Image Registry
- Introducing dumb-init, an init system for Docker containers
- Which Container Images To Use — Distroless Or Alpine?
- Layered Jars Spring Boot
- jlink, Java Platform, Standard Edition Tools Reference
- jdeps, Java Platform, Standard Edition Tools Reference
- Spring Boot Docker