DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

  1. DZone
  2. Refcards
  3. Java Application Containerization and Deployment
refcard cover
Refcard #400

Java Application Containerization and Deployment

How to Get Started With Packaging, Shipping, and Delivering Java to Production

Application containerization provides a way to combine all required app resources into a single, standardized, easily manageable package. And for Java applications, containerization helps solve the majority of challenges related to portability and consistency. This Refcard walks readers step by step through Dockerfile creation for Java apps, container image builds, deployment strategies, and more.

Download Refcard
Free PDF for Easy Reference

Brought to You By

Microsoft
refcard cover

Written By

author avatar Mark Heckler
Principal Cloud Advocate, Java/JVM Languages, Microsoft
Table of Contents
► Introduction ► Why Containerize Java Apps? ► Creating Dockerfiles for Java Applications ► Using Spring Boot Plugins for Containerization ► Building Container Images for Native Applications ► Deployment Strategies for Java Applications in Containers ► Building Java Applications for Continuous Patching ► Conclusion
Section 1

Introduction

Containerization of applications provides a way to combine all required application resources — including program and configuration files, environment variables, networking settings, and more — in a single, standardized, easily manageable package.

Multiple functionally identical containers can be started, run, managed, and terminated from a single container image, ensuring consistency from the point of image creation onward. Containers can run across vastly different operating platforms, from local machines to globally scalable cloud environments and everything in between. Pipelines can be built to transition between them with ease.

While there are numerous benefits to app containerization, many roll up into a single word: consistency.

Section 2

Why Containerize Java Apps?

One early promise of Java was "Write Once, Run Anywhere," or "WORA." And while Java achieved a form of that with its Java Virtual Machine (JVM), there were still a fair number of externalities that got in the way of a truly seamless experience.

Containerization solves nearly all of those externalities. While 100% may be an elusive goal in any pursuit, the ability to package a Java app's executable and all of its required dependencies and supporting properties (configuration, etc.) gets us to an effective 100% level of portability and consistency.

Section 3

Creating Dockerfiles for Java Applications

Many developers begin their containerization efforts by poring over the official Dockerfile reference documentation. To get great results immediately, let's cover the key points, create some images, and build out from there.

Choosing an OS and JDK Builds for Containerization

There are various schools of thought on this, but if you're just beginning to work with containerization, starting with a smaller, but full, operating system (OS) is a great first step. We'll address other options (e.g., distroless) shortly.

As a general rule, the more you include in the OS layer, the larger the container image and the greater the attack surface for security exploits will be. Trusted sources are also a critical consideration. If using a full OS build, eclipse-temurin (based upon Ubuntu) or Alpine base layers are solid recommendations.

Any build of OpenJDK will run your JVM-based Java app, and Eclipse Temurin is one of many good options. If, however, you want dedicated production support for any Java issues you may discover, choosing a commercially supported build provides it. 

Basic Dockerfile Structure for Java Apps

The minimum viable Dockerfile for a basic Java application looks something like this:

Shell
 
​x
1
FROM eclipse-temurin:latest
2
​
3
COPY java-in-the-can-0.0.1-SNAPSHOT.jar /app.jar
4
​
5
EXPOSE 8080
6
​
7
CMD ["java", "-jar", "/app.jar"]


Save the above text (using your application's name in the COPY directive) in a file called Dockerfile in a directory with your Java application (.jar) file.

In the above Dockerfile, we provide the essential information to build the container image:

  • The higher-level, base image FROM which the application container image is built
  • The command to COPY (and in this example, rename) the .jar file into the image
  • Any specific port(s) to EXPOSE for the app to listen for connection requests (if necessary)
  • The command (CMD) to run the app on container startup

Execute the following command from the directory containing your Dockerfile and .jar file:

Shell
 
1
1
docker build -t <app-image-name> .


Note that the docker daemon (or Docker Desktop on Mac/Windows, Podman, etc.) must be running prior to running image creation and other container commands. Also, don't forget the . at the end of the command; it refers to the current directory where the Dockerfile can be found.

Run the resultant application container in this manner, substituting the container image name you created above:

Shell
 
1
1
docker run -p 8080:8080 <app-image-name>


Choosing a Distroless OS+JDK Base Image

The best achievable optimization for most use cases, both in size and attack surface, may be provided by a "distroless" base image. While a Linux distribution (distro) is indeed included in a distroless base image, it is stripped of any files not specifically required for the purpose at hand, leaving a fully streamlined OS and, in the case of a distroless Java image, the JVM.

Here is an example of a Dockerfile that uses a distroless Java base image:

Shell
 
7
1
FROM mcr.microsoft.com/openjdk/jdk:21-distroless
2
​
3
COPY java-in-the-can-0.0.1-SNAPSHOT.jar /app.jar
4
​
5
EXPOSE 8080
6
​
7
CMD ["-Xmx256m", "-jar", "/app.jar"]


Note that this Java-optimized base image preconfigures the ENTRYPOINT for the java command, so the CMD instruction is used to provide command-line arguments for the JVM launcher process.

Using Multi-Stage Builds to Reduce Image Size

Multi-stage builds provide the means to reduce the size of container images if you have files required for the build that aren't required for the final output. For the purposes of this reference, that really isn't the case because the JVM and the app's .jar file and dependencies are provided preconfigured for the creation of the image.

As you might imagine, there are very common circumstances where this becomes advantageous. Typically, applications are deployed to production using build pipelines configured to create artifacts based upon triggers on a source repository. This is one of the best use cases for multi-stage builds: The build pipeline creates a build container with the appropriate tools, uses it to create the artifacts (e.g., .jar file, config files), and then copies those to a fresh container image without additional tooling unnecessary for production. This sequence of actions roughly parallels what we did manually earlier, automated for consistent and optimal results.

Managing Environment Variables

There are multiple ways to supply input values to the container and application for use in startup or execution. A good practice to adopt is to specify all values possible within the Dockerfile itself using ENV, ENTRYPOINT, or CMD directives. All of these values can be overridden at the time of container initialization if needed.

Note that caution should be exercised when overriding existing environment variables as this can change application behavior in unexpected and undesirable ways.

Example of configuring Java-specific options using ENV:

Shell
 
1
1
ENV JAVA_OPTS="-Xmx512m -Xms256m"


The same concept works for app-specific variables:

Shell
 
1
1
ENV APP_GREETING="Greetings, Friend!"


Example of application-specific values configured using ENTRYPOINT:

Shell
 
1
1
ENTRYPOINT ["java", "$JAVA_OPTS", "-jar", "your-app.jar"]


Example using CMD:

Shell
 
1
1
CMD ["java", "-Xmx256m", "-jar", "/app.jar"]


You may have noticed that both ENTRYPOINT and CMD can be used to execute a Java application. Like every other technical (and non-technical) option, there are pros and cons for each of these two directives. Both will result in your Java application running if done properly.

Generally speaking, the CMD directive is used for Java applications so that OS signals can be processed by the app for supported hook mechanisms — e.g., SIGTERM for java.lang.Runtime.addShutdownHook. This isn't absolutely necessary, of course, and cases can (and often are) made for using both ENTRYPOINT and CMD to facilitate runtime parameter passing for providing/overriding specific behaviors, for example. The two aren't mutually exclusive.

Section 4

Using Spring Boot Plugins for Containerization

If you use Spring Boot to develop your Java applications, containerization is a much simpler affair. Whether using Maven or Gradle as your project build tool, creating a container image is as simple as executing a pre-defined goal.

  • If using Maven as your build tool, you can create a container image with the application by invoking the build-image goal: ./mvnw spring-boot:build-image
  • If using Gradle as your build tool, you can create a container image with the application by invoking the bootBuildImage goal: ./gradlew bootBuildImage

In most cases, it's neither necessary nor desirable to customize the image creation (e.g., image layer definitions), but to do so, please refer to the Packaging OCI Images section of the Spring Boot Maven or Gradle Plugin documentation.

Section 5

Building Container Images for Native Applications

Developers have the option to deliver Java apps using the JVM or as a native, OS-specific executable. The following sections offer some considerations for choosing and, if you decide to build container images using native apps, how to do so with the least amount of friction.

Java Native Executable With GraalVM

GraalVM enables the creation of a native executable/binary Java application, performing all compilation and optimization at build time rather than leveraging the JVM to do some optimization while running the application bytecode.

As with all choices, there are tradeoffs. Compiling to bytecode vs. a native executable is a matter of seconds vs. minutes, and the runtime optimizations performed by the JVM disappear with a native executable since code can't be dynamically rewritten at runtime (a feature enabled by the JVM).

Where a native executable outshines a JVM-based Java application is in file size, memory requirements, and start-up time. A native app is much smaller, requires fewer resources, doesn't require the JVM to be present, and starts dramatically more quickly. These are very important considerations in many production environments as smaller apps (and thus their containers) result in lower platform resource requirements, and start-up time measured in milliseconds vs. several seconds can increase availability, scalability, and options for system design and deployment that can result in significant cost savings.

There are a few options for building fully executable, OS-native Java applications, depending upon your framework and tooling choices. Once you have a native executable/binary application, however, you can create a Dockerfile similar to this one to serve as the template for your native app container image:

Shell
 
9
1
FROM alpine:latest
2
​
3
WORKDIR /app
4
​
5
COPY java-in-the-can /app/
6
​
7
EXPOSE 8080
8
​
9
CMD ["/app/java-in-the-can"]


If you're using Spring Boot, you can use the GraalVM Maven or Gradle plugin to compile your application to an OS-native app and create the container image in a single command.

Maven

First, add this dependency to the <build><plugins> section of your pom.xml and save the file:

Shell
 
4
1
<plugin>
2
    <groupId>org.graalvm.buildtools</groupId>
3
    <artifactId>native-maven-plugin</artifactId>
4
</plugin>


To build the native application and container image, run this command from your project root directory:

Shell
 
1
1
./mvnw -Pnative spring-boot:build-image


Gradle

Similarly, add this dependency to the plugins {} section of your build.gradle file and save it:

Shell
 
1
1
id 'org.graalvm.buildtools.native'


To build the native application and container image, run this command from your project root directory:

Shell
 
1
1
./gradlew bootBuildImage


Considerations for Smaller Image Sizes and Faster Start-up Times

You've likely noticed that the order of the above sections have generally trended toward producing leaner container images with faster startup. Many decisions may involve organizational criteria or choices (e.g., deployment standards) that tip the scales toward or against certain choices, but as a general rule, the path to container image optimization follows this order:

  1. Select a smaller base image (OS distro and JVM)
  2. Select a distroless image with JVM
  3. If possible for your toolchain (e.g., Spring Boot), leverage purpose-built tooling
  4. Leverage lean distro or distroless image with native executable application
Section 6

Deployment Strategies for Java Applications in Containers

Important considerations for your application extend beyond its packaging into an app container image. Deployment and maintenance decisions come next and are critical to your application getting to, and remaining in, production.

Single-Container Deployment

For applications that are largely self-contained, deploying to production can be as simple as a single command, assuming the deployment target is ready to accept a containerized application. Even in cases where supporting resources must be created prior to app deployment, this typically results in a small number of directives being issued via command-line or web portal.

When an application comprises multiple container deployments, interprocess dependencies may require the containers be deployed in a particular order to ensure availability or minimize churn or chatter. To accomplish these goals, an orchestrated deployment is required.

Orchestrated Deployments

Orchestrated deployments can be significantly more complex than single container deployments and correspondingly deliver more capabilities. As a result of both characteristics, more platform tier options may merit consideration for orchestrated deployments than for single container deployments. These tiers range from lower-level Kubernetes platforms that provide extensive flexibility and a correspondingly higher level of effort from the developer to full platforms that do much of the heavy lifting to securely configure and integrate multiple containers and/or services.

The target platform you choose will inform your choices of deployment tools (e.g., scripts, portals, infrastructure configuration tools). Very generally speaking, the platform target you choose should be the simplest possible to deploy and maintain your application and its related services. Other important considerations include comparative costs among deployment targets for all of the application's required containers/services, your organization's established practices/pipelines, etc.

Path to Go Live for Java Applications on Azure

Platform options exist both within Azure and with other major cloud providers. For this example, we are focusing on two primary paths in Azure that cover use cases for Java applications of varying complexity and performance requirements.

  • Deploying to Azure Container Apps (ACA) – This path focuses on ease of deployment with serverless/scale-to-zero capabilities, ideal for applications with fluctuating traffic and dynamic scaling needs. Orchestration among containers and services is baked in, making ACA suitable for single or multiple container deployments.
  • Deploying to Azure Kubernetes Service (AKS) – This path provides unrivaled flexibility and is suitable for very complex, large-scale applications that demand the ultimate in configurable orchestration across multiple containers with robust infrastructure management.

For more guidance on choosing between ACA and AKS, check out this guide.

Section 7

Building Java Applications for Continuous Patching

Production deployment isn't finished when the app goes live; developers must ensure applications remain secure, up to date, and available. Key patching considerations include the following:

  • Regular patching – establish a non-disruptive, predictable frequency for routine patches (e.g., monthly or quarterly) to update libraries and dependencies
  • Flash patching – provide guidance on when flash patches are required, typically in response to critical vulnerabilities or urgent security updates

Components of container images that will require patching include:

  • Base OS container image
  • Additional OS packages, if applicable
  • Application runtime (e.g., JVM versions) if not included in the base image
  • Application dependencies/libraries
  • Application performance monitoring (APM) agent binaries

It's up to developers and their organizations to determine and rigorously maintain a patching strategy that protects apps, system infrastructure, and data. Please refer to this guidance to help formulate your specific strategy.

For more details about creating a patching plan and some useful tools for implementation, please review this content:

  • https://learn.microsoft.com/en-us/azure/container-apps/revisions
  • https://learn.microsoft.com/en-us/cli/azure/containerapp/patch?view=azure-cli-latest
Section 8

Conclusion

Containerization enables developers to combine all required application resources and supporting services in one or more container image(s) and to deploy, run, and manage them more easily. Done properly, containerization enables security and consistency from the point of image creation onward. Containers can run across vastly different operating platforms, from local machines to globally scalable cloud environments. Pipelines can be built to transition between them with ease. As a result, developers build and run the same artifact that supports production workloads, reducing conflicts and streamlining tuning and troubleshooting.

If you're new to containers, start small and build locally to gain knowledge and a stable footing, then "build out" by incorporating more container best practices, build pipelines, and suitable cloud platforms to grow toward a powerful production model for app deployment.

Additional considerations and resources:

  • Tools
    • Docker
    • Podman
    • Minikube
    • Skaffold
  • Observability
    • Container and app observability
    • JVM OSS based tools: Java Flight Recorder, Java Mission Control, Visual VM, Jitwatch
    • Open Telemetry-based solutions
    • Application performance monitoring

Like This Refcard? Read More From DZone

related article thumbnail

DZone Article

GitHub Copilot's New AI Coding Agent Saves Developers Time – And Requires Their Oversight
related article thumbnail

DZone Article

Intro to RAG: Foundations of Retrieval Augmented Generation, Part 2
related article thumbnail

DZone Article

Monolith: The Good, The Bad and The Ugly
related article thumbnail

DZone Article

AI Speaks for the World... But Whose Humanity Does It Learn From?
related refcard thumbnail

Free DZone Refcard

Java Application Containerization and Deployment
related refcard thumbnail

Free DZone Refcard

Introduction to Cloud-Native Java
related refcard thumbnail

Free DZone Refcard

Java 15
related refcard thumbnail

Free DZone Refcard

Java 14

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: