Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Containers provide a consistent, portable environment for your Java applications across development, testing, and production stages. This article introduces containerization concepts for Java applications and guides you through creating, debugging, optimizing, and deploying containerized Java applications to Azure Container Apps.
In this article, you learn essential containerization concepts for Java developers and the following skills:
- Setting up your development environment for containerized Java applications.
- Creating Dockerfiles optimized for Java workloads.
- Configuring local development workflows with containers.
- Debugging containerized Java applications.
- Optimizing Java containers for production.
- Deploying your containerized Java applications to Azure Container Apps.
By containerizing your Java applications, you get consistent environments, simplified deployment, efficient resource utilization, and improved scalability.
Containers package applications with their dependencies, ensuring consistency across environments. For Java developers, this means bundling the application, its dependencies, the Java Runtime Environment/Java Development Kit (JRE/JDK), and configuration files into a single, portable unit.
Containerization has key advantages over virtualization that makes it ideal for cloud development. In contrast to a virtual machine, a container runs on a server's host OS kernel. This is beneficial for Java applications, which already run in the Java virtual machine (JVM). Containerizing Java applications adds minimal overhead and provides significant deployment benefits.
The container ecosystem includes the following key components:
- Images - the blueprints.
- Containers - running instances.
- Registries - where images are stored.
- Orchestrators - systems that manage containers at scale.
Docker is the most popular containerization platform, and it's well supported in the Azure ecosystem through Azure Container Apps.
This section guides you through installing the necessary tools and configuring your development environment to build, run, and debug containerized Java applications.
To containerize Java applications, you need the following tools installed on your development machine:
- Docker Desktop. Provides the Docker engine, CLI, and Docker Compose for Windows or macOS.
- Visual Studio Code. Available as a free code editor.
- The following Visual Studio Code extensions:
- Docker extension for managing containers.
- Java Extension Pack for Java development.
- Dev Containers for developing inside containers.
Verify your installation by using the following commands:
docker --version
docker compose version
For Java development in containers, configure Visual Studio Code by installing the Java Extension Pack and setting up your JDK. The Dev Containers extension enables you to open any folder inside a container and use Visual Studio Code's full feature set inside that container.
To enable Visual Studio Code to automatically build and connect to a development container, create a .devcontainer/devcontainer.json file in your project.
For instance, the following example configuration defines a Java build:
{
"name": "Java Development",
"image": "mcr.microsoft.com/devcontainers/java:21",
"customizations": {
"vscode": {
"extensions": [
"vscjava.vscode-java-pack",
"ms-azuretools.vscode-docker"
]
}
},
"forwardPorts": [8080, 5005],
"remoteUser": "vscode"
}
This configuration uses Azure's Java development container image, adds essential extensions, and forwards both the application port, 8080
, and the debugging port, 5005
.
A Dockerfile contains instructions for building a Docker image. For Java applications, the Dockerfile typically includes the following components:
- A base image with the JDK or JRE.
- Instructions to copy application files.
- Commands to set environment variables.
- Configurations of entry points.
Choosing the right base image is crucial. Consider these options:
Description | Name | Remarks |
---|---|---|
Azure Java development Image | mcr.microsoft.com/java/jdk:21-zulu-ubuntu |
Full JDK and optimized for Azure |
Azure Java production Image | mcr.microsoft.com/java/jre:21-zulu-ubuntu |
Runtime only and optimized for Azure |
Official OpenJDK development Image | openjdk:21-jdk |
Full JDK |
Official OpenJDK production Image | openjdk:21-jre |
Runtime only |
For development environments, use a full JDK image. For production, use a JRE or distroless image to minimize the size and attack surface of your application.
The Azure Java images come with Azure-specific optimizations and are regularly updated with security patches, making them ideal for applications targeting Azure Container Apps.
The following example shows a simple Dockerfile for a Java application:
FROM mcr.microsoft.com/java/jdk:21-zulu-ubuntu
WORKDIR /app
COPY target/myapp.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
For Spring Boot applications, you can set up your Dockerfile with the following base:
FROM mcr.microsoft.com/java/jdk:21-zulu-ubuntu
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Dspring.profiles.active=docker", "-jar", "app.jar"]
For production deployments, use the JRE image shown in the following example to reduce the size and minimize the attack surface of your application:
FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENV JAVA_OPTS="-Dserver.port=8080"
ENTRYPOINT ["java", ${JAVA_OPTS}, "-jar", "app.jar"]
Containers are meant to execute in various contexts. In this section, you learn a local development flow for use with containers.
Most Java applications interact with databases, caches, or other services. Docker Compose helps you define and orchestrate multi-container applications using a simple YAML configuration file.
Docker Compose is a tool that enables you to perform the following tasks:
- Define multi-container applications in a single file.
- Manage the application lifecycle, including start, stop, and rebuild.
- Maintain isolated environments.
- Create networks for service communication.
- Persist data using volumes.
The following compose.yml file configures a Java application with a PostgreSQL database:
version: '3.8'
services:
app:
build: . # Build from Dockerfile in current directory
ports:
- "8080:8080" # Map HTTP port
- "5005:5005" # Map debug port
environment:
- SPRING_PROFILES_ACTIVE=dev
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/myapp
volumes:
- ./target:/app/target # Mount target directory for hot reloads
depends_on:
- db # Ensure database starts first
db:
image: postgres:13
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=myapp
ports:
- "5432:5432" # Expose PostgreSQL port
volumes:
- postgres-data:/var/lib/postgresql/data # Persist database data
volumes:
postgres-data: # Named volume for database persistence
This file has the following characteristics:
- Services can reference each other by name - for example,
db
in the JDBC URL. - Docker Compose automatically creates a network for the services.
- The Java application waits for the database to start, due to
depends_on
. - The database data persists across restarts using a named volume.
After you create your compose.yml file, manage your application by using the following commands:
# Build images without starting containers
docker compose build
# Start all services defined in compose.yml
docker compose up
# Start in detached mode (run in background)
docker compose up -d
# View running containers managed by compose
docker compose ps
# View logs from all containers
docker compose logs
# View logs from a specific service
docker compose logs app
# Stop all services
docker compose down
# Stop and remove volumes (useful for database resets)
docker compose down -v
A typical Java development workflow using Docker Compose contains the following steps:
- Create the compose.yml file and the Dockerfile.
- Run
docker compose up
to start all services. - Make changes to your Java code.
- Rebuild your application. Depending on the configuration, you might need to restart your containers.
- Test the changes in the containerized environment.
- When you're finished, run
docker compose down
.
For simpler scenarios when you don't need multiple interconnected services, you can use the docker run
command to start individual containers.
The following Docker commands are typical for Java applications:
# Run a Java application JAR directly
docker run -p 8080:8080 myapp:latest
# Run with environment variables
docker run -p 8080:8080 -e "SPRING_PROFILES_ACTIVE=prod" myapp:latest
# Run in detached mode (background)
docker run -d -p 8080:8080 myapp:latest
# Run with a name for easy reference
docker run -d -p 8080:8080 --name my-java-app myapp:latest
# Run with volume mount for persistent data
docker run -p 8080:8080 -v ./data:/app/data myapp:latest
Debugging containerized Java applications is sometimes challenging because your code runs in an isolated environment inside the container.
Standard debugging approaches don't always directly apply, but with the right configuration, you can establish a remote debugging connection to your application. This section shows you how to configure your containers for debugging, connect your development tools to running containers, and troubleshoot common container-related issues.
Debugging containerized Java applications requires exposing a debug port and configuring your IDE to connect to it. You can accomplish these tasks by using the following steps:
To enable debugging, modify your Dockerfile so it contains the following content:
Note
You can modify your container startup command, instead.
FROM mcr.microsoft.com/java/jdk:21-zulu-ubuntu WORKDIR /app COPY target/*.jar app.jar EXPOSE 8080 5005 ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "app.jar"]
Configure Visual Studio Code's launch.json file to connect to the debug port, as shown in the following example:
{ "version": "0.2.0", "configurations": [ { "type": "java", "name": "Debug in Container", "request": "attach", "hostName": "localhost", "port": 5005 } ] }
Start your container with port
5005
mapped to your host, and then launch the debugger in Visual Studio Code.
When containers don't behave as expected, you can inspect your app's logs to investigate the issue.
Use the following commands to troubleshoot your application. Before you run these commands, make sure to replace the placeholders (<...>
) with your own values.
# View logs
docker logs <CONTAINER_ID>
# Follow logs in real-time
docker logs -f <CONTAINER_ID>
# Inspect container details
docker inspect <CONTAINER_ID>
# Get a shell in the container
docker exec -it <CONTAINER_ID> bash
For Java-specific issues, enable the JVM flags for better diagnostics, as shown in the following example:
ENTRYPOINT ["java", "-XX:+PrintFlagsFinal", "-XX:+PrintGCDetails", "-jar", "app.jar"]
The following table lists common issues and corresponding solutions:
Error | Possible solution |
---|---|
Out of memory | Increase container memory limits |
Connection time-outs | Check network configuration for errors. Verify ports and routing rules. |
Permission problems | Verify file system permissions. |
Classpath issues | Check JAR structure and dependencies. |
Java applications in containers require special consideration for optimal performance. The JVM was designed before containers were common. Using containers can lead to resource allocation issues if they aren't properly configured.
You can significantly improve the performance and efficiency of your containerized Java applications by fine-tuning memory settings, optimizing the image size, and configuring garbage collection. This section covers essential optimizations for Java containers, with a focus on memory management, startup time, and resource utilization.
The JVM doesn't automatically detect container memory limits in Java 8. For Java 9+, container awareness is enabled by default.
Configure your JVM to respect container limits, as shown in the following example:
FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
The following JVM flags are important for containerized applications:
-XX:MaxRAMPercentage=75.0
. Sets maximum heap as a percentage of available memory.-XX:InitialRAMPercentage=50.0
. Sets initial heap size.-Xmx
and-Xms
. These flags are also available, but they require fixed values.
Moving containerized Java applications to production requires considerations beyond basic functionality.
Production environments demand robust security, reliable monitoring, proper resource allocation, and configuration flexibility.
This section covers the essential practices and configurations needed to prepare your Java containers for production use. The section focuses on security, health checks, and configuration management, to ensure your applications run reliably in production.
Secure your containerized Java applications by using the following practices:
Default security context. Run your applications as a non-root user, as shown in the following example:
FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu WORKDIR /app COPY target/*.jar app.jar RUN addgroup --system javauser && adduser --system --ingroup javauser javauser USER javauser ENTRYPOINT ["java", "-jar", "app.jar"]
Proactively look for issues. Regularly scan container images for vulnerabilities by using the following command:
docker scan myapp:latest
Base image freshness. Keep your base images up to date.
Secrets management. Implement proper secrets management. For instance, don't hard-code sensitive data into your application, and use a key vault whenever possible.
Restricted security contexts. Apply the principle of least privilege to all security contexts.
File system access. Use read-only file systems wherever possible.
Check application health with probes to ensure your application is running correctly.
For Spring Boot applications, include the Actuator dependency for comprehensive health endpoints, as shown in the following example:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Configure your application to output logs in a format suitable for container environments, like JSON.
This section guides you through preparing your Java containers for Azure Container Apps deployment, and highlights key configuration considerations.
Port configuration. Ensure your container listens on the port provided by Azure, as shown in the following example:
FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu WORKDIR /app COPY target/*.jar app.jar ENV PORT=8080 EXPOSE ${PORT} CMD java -jar app.jar --server.port=${PORT}
Probe for health. Implement health probes for Azure's liveness and readiness checks.
Log configuration. Configure logging to output to
stdout
/stderr
.Plan for the unexpected. Set up proper graceful shutdown handling with time-out configuration. For more information, see Application lifecycle management in Azure Container Apps.