Building and Deploying Secure Docker Images for Java Applications Editorial Team, January 1, 2026January 1, 2026 In the world of modern DevOps and cloud-native deployment, Docker has become the de facto standard for packaging and running Java applications. However, a docker build -t myapp . followed by a docker run is often just the starting point. The gap between a working image and a secure, efficient, production-grade image is significant. Vulnerable, bloated containers are a major attack vector and a source of performance issues. This guide walks through the essential practices for building and deploying hardened Docker images for Java applications. Table of Contents Toggle Why Security-First Docker Images MatterPhase 1: Crafting an Optimal DockerfilePhase 2: Advanced Hardening TechniquesPhase 3: Deployment and Runtime SecurityPutting It All Together: A Sample Secure PipelineConclusion Why Security-First Docker Images Matter A standard, naive Docker build often results in images that are hundreds of megabytes or even gigabytes in size, containing a full operating system, build tools, and unnecessary dependencies. This large attack surface includes libraries with unpatched CVEs, default credentials, and often runs processes with excessive privileges. In Java’s case, we might inadvertently include the full JDK for runtime, when only the JRE is needed, or even include the source code. Securing your Docker image is not an afterthought; it’s a foundational aspect of a robust CI/CD pipeline. Phase 1: Crafting an Optimal Dockerfile Let’s start with a typical Dockerfile for a Spring Boot application and refine it step-by-step. The Naive Approach: FROM ubuntu:latest RUN apt-get update && apt-get install -y openjdk-17-jdk COPY . /app WORKDIR /app RUN ./gradlew build CMD ["java", "-jar", "build/libs/myapp.jar"] This has several flaws: it uses a heavyweight base image, installs the full JDK, copies source code before building, and runs as root. See also Serverless Java: Comparing AWS Lambda, Azure Functions, and Google Cloud RunThe Improved, Multi-Stage Build: Multi-stage builds are the cornerstone of creating lean, secure images. They allow you to use one image for building and a separate, minimal image for running. # Stage 1: The Builder FROM eclipse-temurin:17-jdk-jammy as builder WORKDIR /workspace/app # Leverage build cache for dependencies COPY gradle/wrapper gradle/wrapper COPY gradlew . COPY build.gradle . COPY settings.gradle . RUN ./gradlew dependencies --no-daemon # Copy source and build COPY src src RUN ./gradlew bootJar --no-daemon # Stage 2: The Runtime FROM eclipse-temurin:17-jre-jammy:latest # Add a non-root user RUN groupadd -r spring && useradd -r -g spring spring USER spring:spring WORKDIR /app # Copy the built artifact *only* from the builder stage COPY --from=builder /workspace/app/build/libs/*.jar app.jar # Configure JVM for containers: use container-aware memory limits ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app/app.jar"] Key Improvements: Official Base Images: Uses the official eclipse-temurin (Adoptium) images, which are well-maintained and secure. Multi-Stage: The final image is based on the JRE (jre), not the JDK (jdk), drastically reducing size and surface area. Non-Root User: Creates and switches to a non-root user (spring) to limit damage if the container is compromised. Cached Dependencies: Separately copying build files and downloading dependencies before copying source code leverages Docker cache, speeding up builds. JVM Container Support: The JAVA_OPTS enable the JVM to respect container memory limits, not the host machine’s memory. Phase 2: Advanced Hardening Techniques 1. Choose a Minimal Base Image: Consider even smaller alternatives like eclipse-temurin:17-jre-alpine (based on Alpine Linux). However, be cautious: Alpine uses musl libc, which may cause compatibility issues with some native Java libraries. Test thoroughly. For the best balance, the -jre-jammy images (Debian-slim variant) are often ideal. 2. Scan for Vulnerabilities: See also NoSQL with Java: A Guide to Redis, MongoDB, and Cassandra DriversIntegrate vulnerability scanning into your pipeline. Use tools like: Trivy: trivy image myapp:latest Grype: grype myapp:latest Docker Scout: docker scout quickview myapp:latest Run these scans in your CI/CD pipeline (e.g., GitHub Actions, GitLab CI) to fail builds on critical CVEs. 3. Keep Images Up-to-Date: Rebase and rebuild your images regularly to incorporate security patches from the base image. Don’t just patch your app; automate rebuilds to get OS and JRE updates. Use docker build --pull to ensure you fetch the latest base image version. 4. Use .dockerignore: A critical security file. Prevent secrets, local configs, and unnecessary files from being copied into the image. **/.gradle **/.git **/gradle.properties **/secrets.yml **/*.log Dockerfile README.md 5. Optimize the JVM for Containers: Beyond UseContainerSupport, tune your JVM: Set heap limits relative to the container: -XX:MaxRAMPercentage=75.0 (uses 75% of container memory). Consider using the Application Class Data Sharing (AppCDS) feature to improve startup time. For a very fast startup, explore the CRaC (Coordinated Restore at Checkpoint) project for Java. Phase 3: Deployment and Runtime Security Building a secure image is only half the battle. Runtime configuration is equally vital. 1. Run with Least Privilege: We already set a non-root user inside the image. Enforce it on the host: docker run --user 1000:1000 myapp:latest In Kubernetes, set securityContext.runAsNonRoot: true and securityContext.runAsUser. 2. Limit Capabilities: Drop all Linux capabilities and add only those explicitly required. Never run with --privileged. docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp:latest 3. Set Resource Limits: Prevent a single container from exhausting host resources. docker run -m 512m --cpus=1 myapp:latest In Kubernetes, always define resources.requests and resources.limits. 4. Make the Filesystem Read-Only: See also Mid-2026 Java Ecosystem Report: Trends, Salaries, and Popular FrameworksA running container should not need to write to its filesystem except for specific paths (like temp directories or logs). docker run --read-only --tmpfs /tmp myapp:latest Mount volumes for persistent data that needs to be written. 5. Use Secure Secrets Management: Never bake secrets (API keys, passwords) into the image. Use: Docker Secrets: (for Swarm) Kubernetes Secrets: Mounted as volumes or environment variables. External Vaults: HashiCorp Vault, AWS Secrets Manager, injected at runtime. Putting It All Together: A Sample Secure Pipeline Develop: Write code with a secure Dockerfile as above. CI (e.g., GitHub Actions): Build an image using a multi-stage Dockerfile. Run vulnerability scan (Trivy). Fail the build on HIGH/CRITICAL CVEs. Push the image to a registry with a digest tag. Registry: Configure your registry (e.g., AWS ECR, Google GAR) to scan images on push. Deployment (e.g., Kubernetes): Reference the image by its digest for immutability. Apply a Pod Security Standard (like restricted). Configure all runtime security contexts (non-root, read-only root filesystem, dropped capabilities). Use network policies to restrict pod-to-pod communication. Conclusion Building a secure Docker image for a Java application is a deliberate process that extends from the first line of your Dockerfile to the runtime configuration in production. By embracing multi-stage builds, using minimal base images, running as non-root, integrating vulnerability scanning, and applying strict runtime constraints, you significantly reduce your application’s attack surface. This isn’t just about security—it leads to more efficient, faster, and more reliable containers that are truly cloud-native. The investment in these practices pays dividends in stability, compliance, and resilience, making your Java applications not just functional but fortresses in your architecture. Start by refactoring one Dockerfile. Run a vulnerability scan on your current image—the results might be the motivation you need. In the containerized world, security is a layer cake, and it begins with the image you build. Java