Multi-stage Docker Builds for Efficient Jakarta EE Deployments with Payara
Published on 17 Jul 2025

Enterprise Jakarta EE applications require extensive tooling during development - Maven for dependency management, full JDKs for compilation, and various build utilities. However, production environments need only the compiled application and runtime server like Payara Server or Payara Micro. Multi-stage Docker builds bridge this gap by separating build and runtime concerns, producing dramatically smaller and more secure container images for Payara deployments.
The Problem with Single-Stage Builds
Traditional single-stage Dockerfiles force you to include everything needed for both building and running your application. A typical Jakarta EE image might contain:
- Full JDK (300MB+)
- Maven or Gradle (100MB+)
- Source code and test files
- Intermediate build artifacts
- Build caches and temporary files
- Jakarta EE runtime like Payara Server
This results in bloated images often exceeding 1GB for simple applications. These oversized containers slow down deployments, consume unnecessary bandwidth, and expand the attack surface by including build tools in production.
Multi-stage Architecture
Multi-stage builds use multiple FROM statements within a single Dockerfile. Each stage serves a specific purpose, and you can selectively copy artifacts between stages while discarding everything else.
For Jakarta EE applications, this typically involves:
- Build Stage: Compile source code using Maven with full JDK
- Runtime Stage: Deploy the compiled application on a lean Payara image
Implementing a Basic Multi-stage Build
Creating a multi-stage build requires understanding how to structure your Dockerfile for maximum efficiency. The key is separating the build environment from the runtime environment while maintaining clean artifact transfer between stages. Let's examine a practical example for a standard Jakarta EE web application that demonstrates these principles with Payara Server.
# Build Stage
FROM maven:3.9.9-amazoncorretto-21-debian-bookworm AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:resolve
COPY src ./src
RUN mvn clean package -DskipTests
# Runtime Stage
FROM payara/server-full:6.2025.5-jdk21
COPY --from=builder /build/target/myapp.war $DEPLOY_DIR/
This approach noticeably reduce the final image size.
Optimizing Build Performance
One of Docker's most powerful features is its intelligent layer caching system, but many developers don't take full advantage of it. When Docker builds an image, it caches each instruction as a separate layer. If nothing has changed in a particular layer, Docker simply reuses the cached version instead of rebuilding it from scratch. This means the order of operations in your Dockerfile can dramatically impact build times, especially during development when you're rebuilding frequently.
# Build Stage
FROM maven:3.9.9-amazoncorretto-21-debian-bookworm AS builder
WORKDIR /build
# Copy and resolve dependencies first
COPY pom.xml .
RUN mvn dependency:go-offline
# Copy source and build only when code changes
COPY src ./src
RUN mvn clean package -DskipTests
Notice the mvn dependency:go-offline command? When source code changes, Docker reuses the cached dependency layer, significantly accelerating build times.
Advanced Configuration Management
Real-world production environments rarely run with default configurations. Your applications need database connection pools, custom logging settings, security configurations, and environment-specific properties. The beauty of multi-stage builds is that you can prepare all these configurations during the build phase, ensuring your runtime image contains exactly what it needs without any manual intervention during deployment. For example, you can bundle a custom domain.xml file for your Payara Server with build as shown below.
FROM maven:3.9.9-amazoncorretto-21-debian-bookworm AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:resolve
COPY src ./src
COPY config/domain.xml ./config/
RUN mvn package -DskipTests
FROM payara/server-full:6.2025.5-jdk21
# Copy application
COPY --from=builder /build/target/myapp.war $DEPLOY_DIR/
# Copy custom configuration
COPY --from=builder /build/config/domain.xml $PAYARA_DIR/glassfish/domains/domain1/config/
# Set production JVM options
ENV JAVA_OPTS="-Xmx2g -Xms1g -XX:+UseG1GC"
Handling External Dependencies
Enterprise Jakarta EE applications often extend beyond basic framework components, requiring additional libraries such as database drivers, messaging clients, or specialized connectors. These dependencies pose a unique challenge in containerization because they must be available at runtime but shouldn't bloat your build environment. The multi-stage approach allows you to manage these dependencies efficiently by downloading them during the build phase and selectively copying only the required JARs to your runtime image.
FROM maven:3.9.9-amazoncorretto-21-debian-bookworm AS builder
WORKDIR /build
COPY pom.xml .
# Download application and external dependencies
RUN mvn dependency:resolve
RUN mvn dependency:copy-dependencies -DoutputDirectory=lib
COPY src ./src
RUN mvn package -DskipTests
FROM payara/server-full:6.2025.5-jdk21
COPY --from=builder /build/target/myapp.war $DEPLOY_DIR/
COPY --from=builder /build/lib/*.jar $PAYARA_DIR/glassfish/domains/domain1/lib/
Security Hardening
Multi-stage builds provide inherent security benefits by eliminating build tools and development dependencies from production images, but you can further enhance security through deliberate configuration choices. Running containers as non-privileged users, implementing health checks, and maintaining proper file ownership are essential practices that reduce attack vectors and improve overall container security posture.
FROM payara/server-full:6.2025.5-jdk21
USER payara
COPY --from=builder --chown=payara:payara /build/target/myapp.war $DEPLOY_DIR/
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s \
CMD curl -f http://localhost:8080/myapp/health || exit 1
Payara Micro Considerations
Microservices architectures benefit significantly from Payara Micro's lightweight footprint and rapid startup times. Unlike the full Payara Server, Payara Micro is designed specifically for cloud-native deployments and containerized environments. When implementing multi-stage builds with Payara Micro, you can achieve even smaller final images while maintaining the full Jakarta EE capabilities your applications require.
FROM maven:3.9.9-amazoncorretto-21-debian-bookworm AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:resolve
COPY src ./src
RUN mvn package -DskipTests
FROM payara/micro:6.2025.5-jdk21
COPY --from=builder /build/target/myapp.war $DEPLOY_DIR/
CMD ["java", "-jar", "payara-micro.jar", "--deploy", "/opt/payara/deployments/myapp.war", "--port", "8080"]
Payara Micro images are significantly smaller than full server images, making them ideal for containerized microservices.
Build Automation and CI/CD Integration
Modern development workflows require integrations between your containerization strategy and continuous integration pipelines. Multi-stage builds work particularly well with automated build systems because they provide consistent, reproducible results while maintaining clear separation between development and production concerns. Implementing proper build automation ensures your team can deploy efficiently while maintaining quality standards.
#!/bin/bash
APP_NAME="myapp"
VERSION=${1:-latest}
REGISTRY="your-registry.com"
echo "Building ${APP_NAME}:${VERSION}"
docker build -t ${REGISTRY}/${APP_NAME}:${VERSION} .
echo "Pushing to registry"
docker push ${REGISTRY}/${APP_NAME}:${VERSION}
# Tag as latest if building main branch
if [[ "${CI_COMMIT_REF_NAME}" == "main" ]]; then
docker tag ${REGISTRY}/${APP_NAME}:${VERSION} ${REGISTRY}/${APP_NAME}:latest
docker push ${REGISTRY}/${APP_NAME}:latest
fi
Performance Monitoring and Optimization
Track the impact of your multi-stage builds:
# Compare image sizes
docker images | grep myapp
# Monitor build times
time docker build -t myapp .
# Check layer cache efficiency
docker build --progress=plain -t myapp .
Most Jakarta EE applications see 60-80% size reductions and 40-60% faster deployment times with properly configured multi-stage builds.
Best Practices Summary
Structure your Dockerfile with stable dependencies first, followed by frequently changing source code. Use specific version tags for base images to ensure reproducible builds across environments. Keep build artifacts minimal - copy only what's necessary for runtime. Include health checks and proper logging configuration for production readiness.
Consider using .dockerignore files to exclude unnecessary files from build contexts:
.git/
target/
*.md
.gitignore
Dockerfile
Common Pitfalls to Avoid
Don't copy entire directories indiscriminately - be explicit about what moves between stages. Avoid installing unnecessary packages in runtime stages. Remember that environment variables set in build stages don't automatically carry over to runtime stages.
Conclusion
Multi-stage Docker builds represent a paradigm shift toward more efficient container strategies. Through a separation of build and runtime concerns, you create smaller, more secure, and faster-deploying Jakarta EE applications with Payara Server. The investment in restructuring your build process pays immediate dividends in deployment efficiency and long-term maintainability.
The combination of Jakarta EE's enterprise capabilities with Payara's performance and Docker's containerization efficiency creates a solid foundation for modern application deployments. Ready to get started? Download Payara Server Community for your development environment and explore multi-stage builds with your own Jakarta EE applications. When you're ready to deploy to production, Payara Enterprise provides the enterprise-grade support, security patches, and performance optimizations your mission-critical applications demand.
Related Posts
What's New In The Payara Platform August 2025 Release?
Published on 06 Aug 2025
by Luqman Saeed
0 Comments
The Payara Monthly Catch - July 2025
Published on 31 Jul 2025
by Chiara Civardi
0 Comments