A Complete Guide To Deploying Java Applications In Docker

0
121

Explore the entire life cycle of deploying Java applications via Docker — from understanding the basics and writing the initial Dockerfile to building container images and managing complex multi-container deployments using Docker Compose. You can use this guide to streamline the deployment pipeline of a Spring Boot microservice, a traditional Java SE program, and even an enterprise web application.

Consistency, scalability, and portability have become must-haves in software development. Because of Java’s robustness, platform independence, and vast ecosystem, it has been the cornerstone of enterprise and backend applications. But rolling out Java applications on multiple environments — develop, test, and production –can cause dependency hell among other issues.

Docker offers a new method of containerisation on the packaging and delivery end. It wraps a Java application with its dependencies in a thin container that is portable and can be run anywhere — on a local machine, in some test environment, or on a cloud-hosted production server.

The problem that Docker solves

An all-too common and annoying problem for the software developer is: “It works on my machine.” The Java application may be working flawlessly on the developer’s system, say with JDK 17 on macOS, but in QA or production it may be misbehaving according to different platform OSs and Java versions. Such variations break builds, or improperly configure sources or runtime situations.

These could be environment-related bugs — the worst kind to locate and fix. Teams spend hours establishing how an issue can be replicated; in fact, it’s not really because of faulty code but an inconsistent setup. This slows development cycles, delaying releases, and increasing deployment risks.

Docker resolves all that by allowing developers to pack the Java application with the specific runtime, libraries, environment variables and configurations into the container image that simply runs the same way in any environment where Docker is installed –however inefficiently configured it may be in development, test, and production instruments.

With Docker there is no need for installing Java inside the working environment or even resolving dependencies. The container already goes with whatever the app needs for execution. This prevents environmental bugs, allowing faster onboarding of developers and testing. Simply put, Docker makes it certain that a Java application runs smoothly everywhere, thus taking care of reliability and deployment ease.

Figure 1: Java application deployment

Why use Docker for Java applications?

Imagine developing a Java game on your computer which functions perfectly when you play it but fails on your friend’s system. The reason for this failure lies in the differences between your computer and your friend’s system, which can exhibit Java version discrepancies and missing files.

Software development faces this issue frequently. Perfect code may operate without issues on your computer yet could encounter problems on different machines because the system configurations vary.

Docker functions as a container system for Java applications. A perfect analogy is delivering a complete meal inside a lunchbox instead of merely sharing a recipe for a sandwich. Docker creates a container that houses your app together with all the essential runtime components including Java code, configuration settings and the required files.

Any person who wants to launch your Java application needs to access your lunchbox. The application functions in an identical way for all users across all locations since it eliminates file loss and version discrepancies.

Here’s an example. Imagine you’ve created a school project—a Java program that calculates grades. It runs perfectly on your laptop since you have Java 17 installed. However, when your teacher tries to run it on her computer with Java 11, it throws an error. Now, if you had used Docker, you could package your program along with Java 17 in a single container. This way, it wouldn’t matter what version of Java your teacher has—your app would run smoothly because it carries its own Java environment.

Table 1: Types of Java applications

Application type

Description

Example use case

Standalone app

Runs independently on a desktop or server

Grade calculator, PDF reader

Web application

Runs on a server and serves web pages over HTTP

Online banking system, e-commerce site

Microservice

Small, self-contained service in a larger system

Product catalogue in e-commerce

Table 2: Common Java packaging formats

File type

Full form

Used for

Runs on

.jar

Java ARchive

Standalone apps, libraries, Spring Boot

Java runtime (JRE/JDK)

.war

Web ARchive

Traditional web apps (servlets, JSPs)

Java EE servers (Tomcat, JBoss)

.ear

Enterprise Archive

Enterprise apps with multiple modules

Full Java EE servers

Table 3: Popular frameworks and their packaging

Framework / Style

Package type

How it runs

Plain Java (No framework)

.jar

Run via java -jar filename.jar

Spring Boot

.jar

Self-contained; has embedded server (Tomcat/Jetty)

Jakarta EE / Java EE

.war / .ear

Deployed on app servers like WildFly or GlassFish

Setting up your Java project

Before you dive into putting your Java application into a Docker container, it’s essential to ensure that your project is well-prepared and neatly organised. Here’s a step-by-step guide to help you get your Java project ready for Docker deployment.

Choose your Java application type: If you’re working on a simple Java program, you may want to create a standalone JAR file. For web applications, particularly those using frameworks like Spring Boot, you’ll usually build a runnable JAR that comes with an embedded web server. If you’re dealing with older Java EE applications, you may have WAR files that require a separate application server. Understanding your project type is key to figuring out how to package it for Docker.

Build your application: Utilise build tools like Maven or Gradle to compile your code and handle dependencies.

For Maven, run:

mvn clean package

This command cleans previous builds and packages your app into a JAR or WAR file in the target/ directory.

For Gradle, use:

./gradlew build

Test locally: Before moving to Docker, run your Java app locally to confirm it works as expected. For example, run a Spring Boot app with:

java -jar target/your-app.jar

Prepare configuration files: Get your configuration files ready. Make sure to externalise your configurations—think database URLs, ports, and API keys—into separate files or environment variables. This approach allows you to tweak settings without having to rebuild your Docker image.

Set up a .dockerignore file: Just like .gitignore, create a .dockerignore file to exclude unnecessary files (e.g., target folders, logs, local IDE configs) from your Docker build context.

A well-prepared Java project with a clean, runnable build artifact and externalised configurations is the foundation for smooth Docker deployment. Once your app builds and runs locally without issues, you’re ready to move on to creating your Docker image.

Table 4: Best practices for Dockerizing Java apps

Best practice

Description

Why it matters

Use official and lightweight base images

Start with trusted images like openjdk:17-jdk-alpine.

Keeps image size small and secure.

Use multi-stage builds

Build your app in one stage; copy only the final JAR to the runtime image.

Reduces image size and removes unnecessary tools.

Externalise configuration

Use environment variables or config files instead of hardcoding settings.

Makes containers flexible across environments.

Use .dockerignore

Exclude unnecessary files (e.g., IDE configs, logs) from the build context.

Speeds up builds and keeps images clean.

Minimise number of layers

Combine commands to reduce layers in your Dockerfile.

Smaller, more efficient images.

Run as non-root user

Create and switch to a non-root user before running the app.

Improves container security.

Expose only necessary ports

Open only the ports your app needs.

Limits potential security vulnerabilities.

Add health checks

Define health checks to monitor app status inside the container.

Enables auto-restart on failure in orchestration.

Log to standard output

Configure app to write logs to console (stdout) instead of files.

Simplifies log management and collection.

Keep images updated

Regularly update base images and dependencies to patch vulnerabilities.

Ensures latest security and performance fixes.

Creating a Dockerfile for Java

Let’s create a Dockerfile for a Java application packaged as a runnable JAR using a lightweight Java 17 runtime image:

FROM openjdk:17-jdk-alpine

Set the working directory inside the container to /app:

WORKDIR /app

Copy the built JAR file from the host machine into the container at /app/myapp.jar:

COPY target/myapp.jar /app/myapp.jar

Run the Java application when the container starts:

CMD [“java”, “-jar”, “myapp.jar”]

Here’s how you can use this file.

Build your Java app using Maven (creates target/myapp.jar):

mvn clean package

Build the Docker image from your project directory (where this Dockerfile is):

docker build -t my-java-app .

Run the Docker container and map port 8080 (adjust if your app uses a different port):

docker run -p 8080:8080 my-java-app

Now your Java app runs inside a container and is accessible at http://localhost:8080.

Table 5: Common pitfalls and how to avoid them

Common pitfall

Description

How to avoid it

Using large base images

Large images make your container bulky and slow to download.

Use lightweight images like openjdk:17-jdk-alpine and multi-stage builds.

Ignoring .dockerignore

Including unnecessary files bloats your build context and image size.

Create a .dockerignore file to exclude
irrelevant files.

Hardcoding configuration inside image

Embedding environment-specific settings reduces flexibility and forces rebuilds.

Use environment variables or external config files.

Running containers as root

Running as root poses security risks if the container is compromised.

Create and run as a non-root user in the Dockerfile.

Over-exposing ports

Exposing unnecessary ports increases the attack surface.

Expose only the ports your app needs.

Forgetting health checks

Without health checks, unhealthy containers may not be detected or restarted.

Add Docker health checks for automatic monitoring.

Logging to files inside container

Writing logs to container files complicates log management and can cause storage issues.

Log to standard output (console) instead.

Neglecting image updates

Using outdated base images and dependencies can cause security vulnerabilities.

Regularly update base images and rebuild containers.

Figure 2: Building and running a Java application with Docker

Building and running the Docker image

Once you’ve set up your Dockerfile and your Java application is neatly packaged as a JAR, the next step is to build a Docker image and run it as a container. Building the image means that Docker takes all the instructions from your Dockerfile and creates a snapshot that includes your Java app along with the necessary Java runtime environment. Think of this image as a portable package that you can run on any machine with Docker installed, without having to stress about differences in the environment. To build the image, you’ll need to run a Docker command in your project directory. This command reads your Dockerfile, copies your Java app into the image, and sets everything up just the way you specified. Once the build is complete, you’ll have an image named whatever you decided to tag it — for instance, my-java-app.

When it comes to running the Docker container, you’re essentially launching an instance of that image. As the container starts up, it runs your Java app in a controlled and isolated environment. You can also map the container’s internal ports to your computer’s ports, allowing you to access your app from a browser or API client as if it were running directly on your machine. Lastly, you can easily check which containers are currently running with a simple Docker command. This is a great way to keep an eye on your apps and manage them effectively.

Using Docker to build and run your Java application guarantees that it behaves the same way everywhere, making development, testing, and deployment a whole lot easier and more reliable.

To build the Docker image from the current directory, tag it as my-java-app:

docker build -t my-java-app

Run a container from the image, mapping container port 8080 to host port 8080:

docker run -p 8080:8080 my-java-app

List all running containers to verify your app is running (optional):

docker ps

Multi-stage Docker builds (optional, for optimised images)

When you’re putting together a Docker image for your Java application, it typically includes all the necessary files and tools for both building and running the app. However, this can lead to a hefty final image size since build tools like Maven or Gradle, along with the source code, don’t really need to be part of the image that’s deployed in production. That’s where multi-stage builds come in handy. They allow you to use one stage to compile and build your Java app, and then you can just copy the final runnable files (like the JAR) into a much smaller image that only has what’s essential for running the app. The end result? A leaner, cleaner Docker image that takes up less disk space, downloads quicker, and is more secure. Think of it like baking a cake in one kitchen and then only packing the finished cake (leaving behind the baking tools and ingredients) into a box for delivery.

Here’s an example of a multi-stage Dockerfile for a Spring Boot Java app.

Stage 1: Build the app using Maven:

FROM maven:3.8.6-openjdk-17 AS build

Set working directory inside the builder container:

WORKDIR /build

Copy source code and pom.xml files into the container:

COPY pom.xml . COPY src ./src

Build the project and package it as a jar:

RUN mvn clean package –DskipTests

Stage 2: Create a lightweight image for running the app:

FROM openjdk:17-jdk-alpine

Set working directory inside the runtime container:

WORKDIR /app

Copy the JAR built in the first stage into this stage:

COPY --from=build /build/target/myapp.jar ./myapp.jar

Run the Java app when the container starts:

CMD [“java”, “-jar”, “myapp.jar”]

This is how it works:

The first stage (build) uses a Maven image with JDK to compile your app and create the JAR.

The second stage starts from a small Java runtime image (openjdk:17-jdk-alpine) and copies only the built JAR from the first stage.

This way, the final image doesn’t include Maven, source code, or build files — only the essential runtime and your app.

Docker has truly transformed the way we build, ship, and deploy Java applications by offering a consistent and portable environment. When you containerise your Java apps, you can be confident they’ll run smoothly across various machines and cloud platforms. By sticking to best practices like using lightweight images, multi-stage builds, and effective configuration management, you can create Docker containers that are not only efficient but also secure and easy to maintain. As you progress on your Docker journey, diving into advanced topics like Docker Compose for managing multi-container applications, Kubernetes for orchestration, and integrating Docker with CI/CD pipelines will take your development workflow to the next level.

LEAVE A REPLY

Please enter your comment!
Please enter your name here