For long Java is one of the most popular programming languages that developers use. It’s been there for more than 20 years and the developer community is huge. Java enables you to write many different types of applications: Enterprise applications, Desktop GUI applications, and Mobile applications to name a few. It’s also possible to write Cloud-based applications. Container technology also has its place in this ecosystem. Since Java applications are run through a so-called Java Virtual Machine, this is an ideal technology to containerize your application. To make sure your application runs smoothly, it’s great to understand what the best practices and common conventions are. Java in containers – what’s good to know.
Essential building blocks
First of all, it’s essential to understand that there are multiple definitions of containers within the Java ecosystem. The website of Knowledgeburrow specifies four types of containers in Java including Servlet containers, the Java EE server, and a web container (from the JSP context). Simply said, these are just programming building blocks for your application, but these aspects are different compared to actually running your Java application inside a container.
Most of the essential building blocks to know about running your Java application inside a container can be found at the website of Stackify. Their tutorial covers all major steps from A – Z from preparing your local environment to the configuration of the logging of your application before the famous “Hello World” demo text is presented. Key takeaways from this post are the things you need to do yourself versus the things which are already provided for you. For example, the Spring boot runtime environment which is part of the image is needed as a runtime environment.
Production-grade Java containers
While the previous article provides a great overview of the bits and pieces which are needed to get started, there’s a lot more to explore that enable more fine-grained solutions which are more robust. These are essential to staying ahead of (potential) problems in the future. Think of resource exhaustion, data loss, and information security as well as more convenient ways to collaborate with multiple other developers on the same project.
Snyk provides a list of best practices to follow when you want to build production-grade Java containers. It kicks off with carefully selecting and using the Maven build image. Maven is a popular build tool to configure and build your Java application, directly on your local system as well as inside a container. They also advise focusing on the smallest possible image (think of Alpine Linux and skip Java tests when possible) to do the job. This saves time to build, time to upload and download the image as well as reduces the attack surface since then it contains fewer dependencies which all need to be managed and secured.
Speaking about security, there are tips to properly use sensitive files like settings.xml so they do not leak your username/password for your Maven repository. In the article, you also find a way to run your Java application as a non-root user, which makes things more secure. To adhere to the “separation of concerns” principle, use two images: one to build your application (build containers) and one to actually run it (application container).
Continued here, the article sheds some light on how to safely terminate a containerized Java application as well as properly tear it down. These tips come from a Linux-based environment (think of PID 1 and SIGINT) and help to minimize problems like unexpected behavior or data loss.
And finally, the article concludes with a careful analysis of container image generation tools like Maven plugins or Spring boot to generate container images and runtimes. There is a lot to miss out on if you blindly use these helper tools since they do not always adhere to the best practices discussed earlier.
Choosing the right base image
Every containerized application requires careful consideration of required and optional configuration. In the article Medium, the author put together a lot of great considerations for the most important building blocks. One of the key aspects is selecting the right base image. This image is used as the base for your Java application that is built on top of it. Some of the questions which you should ask yourself are:
- Do you need to pay (extra) for network traffic, storage, and unused files which might happen due to selecting the wrong base image?
- Which patching strategy do you follow? Would you patch unneeded dependencies?
- Which runtime environment does your application require and do you need any native packages for it to run properly?
As mentioned before, the container images should be small and easy to manage. Size has an effect on the following aspects: download and upload time. This is especially relevant in cloud-based environments in which you pay for what you use. But also on the time it takes to scale up and down your container orchestration tool. The development agility is also affected by the size of the image: small means faster development iterations.
Besides these aspects, it’s vital to choose the Java Application Server. Since this is now all handled by containers, the right-based image is essential to serve the requests for your application. Popular frameworks like Spring boot offer examples for Maven & Gradle to get you started.
Pay special attention to the section which highlights Maven as the base image. This is an interesting section since it handles the pros and cons of the very popular base image. It explores subtopics such as where to keep the Maven cache (which stores all (transient) dependencies) that are needed to build your application, the maven onBuild image, and why it’s less useful compared to the main base image. Furthermore, it explores how to use Maven completely from within a container’s perspective. This way, you don’t need to have anything like this installed on your system.
For sure, the big cloud providers can’t be forgotten when it comes to this topic.
- Azure offers recommended strategies and settings for containerizing your application to be used on top of their platforms. It zooms in into the specifics of the memory management of the containers themselves and also of the Java processes. You can find information about establishing a certain baseline as well as moving forward to design and optimize your application for Kubernetes-based deployments. It concludes with guidelines on how to best select, create and/or adjust container images for Java.
- App2Container is an AWS service that allows you to migrate your workloads that run on Virtual Machines to containers running in AWS. It consists of a command line tool to easily script this process, claiming that you do not need any code changes. The target platform is either Fargate or EKS (Kubernetes in AWS). At the heart, Java applications running on Linux are supported. One of the great things about this article is the breakdown of required tasks in so-called Epics. This makes migration manageable for a greater number of applications.
These are just s selection of a huge number of other articles that highlight migration strategies and deployment options.
Java & memory
One of the key things to remember is configuring the right memory settings for your Java applications. There are plenty of options that all have different purposes. Normally you would only do this for the JVM – the Java Virtual Machine under which your container runs. Now you also need to pay attention to how containers would handle the different options you have here. At Dzone, there is an excellent article that discusses the most important topics.
The article is split based on the 3 major options that follow the JVM arguments you must master. Min/MaxRAMFraction, Min/MaxRAMPercentage as well as MX-related arguments.
The strength of the article is based on the following observations:
- It indicates which (pair of) arguments applies to which version of Java. Without this, there is virtually no need to spend more time on the challenge since the Java version is the most important of all.
- There is a deep explanation of how the parameters work including some caveats that go even deeper revealing terminology that easily leads to confusion.
- The limitations of the given arguments, parameters, and settings are explained very well. On top of that, it handles important shortcomings of the JVM that are vital to consider before you make up your mind.
- Practical examples with actual source code and its expected output follow the theory to clarify the options as well as the limitations of any given solution.
Since memory size is vital for the performance of your application, the article is a “must-read”. At the end of every subtopic, there are best practices for the given options. These help you quickly determine how you would handle a specific setting without running into issues later on.
Running rootless & Podman
For a long time, (Java) containers have been running as root. This was widely accepted until a couple of years back. Now that more and more companies pay special attention to security-related aspects, this trend changes. A big accelerator is Podman which offers plenty of options to run more secure containers.
An interesting overview of all kinds of rootless configurations can be found on the website of mbien.dev. Here, you’ll find tips and tricks about Java containers running with Podman:
- Learn how to map volumes for your persistent data along with the handling of user groups inside and outside of the container. Podman has special tools like unshare and parameters like –name to make this happen. All of them work perfectly fine with Java applications.
- Being able to group containers with Pods to let containers talk to each other (in the case of micro-services) in a more secure way. Since rootless containers do not expose Ports directly on the host, this path is a must-have here. You need this to be able to split your application across multiple containers which still depend on each other.
- Java applications need to have proper classpaths to work, especially if (external) dependencies are needed. Class data which is stored in a parent (base) image is mapped into memory once and can then be stored in all child containers which actually depend on the parent image. Podman provides an image tree to visualize the layers in a neat overview.
It’s the memory – again
Special care should be taken to the memory considerations we saw earlier in this article. When running containers rootless, this becomes less important. Basically, it’s essential to restrict the parallelism level of the JVM which is being used. This way you don’t need to set the memory arguments in the previous section. All because Java runs on virtual threads and not physical ones. However, what remains important are the settings of your memory usage in your deployment files such as CPU and memory limits (min and max settings). These act as the boundaries of your memory consumption.
Thankfully, Podman offers solutions for many challenges Java developers face.
Java remains one of the most popular programming languages for developers. Whether they are running their applications on Virtual Machines or in containers. Special care is needed to make sure your Java applications are containerized properly. In this article, we’ve ticked the box for several challenges that developers and operators face. The collection ranges from running Java applications security (as rootless containers) to best practices around memory management as well as selecting and adjusting the needed (base) images.
If you have questions related to this topic, feel free to book a meeting with one of our solutions experts, mail to email@example.com.