🚀 Executive Summary
TL;DR: The article analyzes methods for creating Spring Boot Docker images, highlighting the conflict between developer convenience and operational control. It concludes that while the Maven plugin is easy for local development, a multi-stage Dockerfile is superior for production due to explicit control, security, and reproducibility, with a hybrid layered JAR approach offering advanced build caching.
🎯 Key Takeaways
- The Spring Boot Maven plugin (`spring-boot:build-image`) offers convenience for local development using Cloud Native Buildpacks but acts as a ‘black box,’ lacking operational control and transparency for production security and maintenance.
- A multi-stage Dockerfile is the recommended industry standard for production Spring Boot images, providing explicit control over base images, build processes, and resulting layers, leading to smaller, more secure, and reproducible artifacts.
- The hybrid approach, combining Spring Boot’s layered JAR feature with a Dockerfile, optimizes Docker build caching by copying distinct layers (dependencies, application) individually, significantly speeding up subsequent builds when only code changes.
Deciding between the Spring Boot Maven plugin and a dedicated Dockerfile for image creation isn’t just a technical choice; it’s a philosophical one that impacts your entire CI/CD pipeline, security posture, and team collaboration. For production-grade systems, a well-crafted multi-stage Dockerfile provides the explicit control, security, and optimization that plugins simply can’t match.
Maven Plugin vs. Dockerfile: The Spring Boot Civil War You Didn’t Know You Were In
I still remember the 2 AM page. A critical vulnerability, ‘Log4Shell’ level bad, was just announced in the OpenJDK base image we were using. The security team was breathing down our necks for a patch ETA. On most services, it was easy: update the `FROM` line in the Dockerfile, run the pipeline, and deploy. But for one specific service, `auth-service-v3`, the team couldn’t find a Dockerfile. A junior dev, trying to be helpful and fast, had used the `spring-boot:build-image` Maven goal. It was a black box. We had no direct control over the base image or the build process. We spent the next three hours reverse-engineering the plugin’s behavior to force an update instead of the 15 minutes it should have taken. That night, I swore off magic build plugins for anything touching production.
The Real Problem: Developer Convenience vs. Operational Control
This whole debate boils down to a classic conflict. On one side, you have the developer experience. The Spring Boot Maven plugin is incredibly convenient. You type one command, `mvn spring-boot:build-image`, and like magic, you get a decent, layered Docker image. It’s fantastic for getting a new developer up and running on their local machine.
On the other side, you have the operational requirements of a production system: security, reproducibility, optimization, and transparency. As a DevOps engineer, “magic” is my worst enemy. I need to know exactly what’s in an image, how it was built, what its base OS is, and how its layers are structured. When a security scanner flags `prod-user-api-01` with a critical CVE, I need a clear path to remediation, not a plugin’s opaque, auto-generated configuration.
Solution 1: The “It’s 5 PM on a Friday” Method (The Maven Plugin)
Let’s be real, sometimes you just need to get something running locally. This is where the plugin shines. You add it to your `pom.xml` and you’re off to the races.
How it works: You add the plugin configuration to your POM, and it uses Cloud Native Buildpacks under the hood to analyze your project and create a Docker image with reasonably optimized layers.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>docker.io/my-org/${project.artifactId}:${project.version}</name>
</image>
</configuration>
</plugin>
</plugins>
</build>
Then you just run `mvn spring-boot:build-image`.
My Take: This is a great tool for local development and rapid prototyping. It lowers the barrier to entry. But I consider it a “hack” for production. Relying on this in your CI/CD pipeline is like letting your car’s manufacturer decide what brand of oil you use, forever. You’re giving up control for a little bit of initial convenience.
Solution 2: The “Grown-Up” Method (The Dedicated Dockerfile)
This is the industry standard for a reason. A `Dockerfile` is a declarative, version-controlled recipe for your image. It’s transparent, explicit, and gives your operations team the fine-grained control they need.
How it works: We use a multi-stage build. The first stage (the ‘builder’) uses a full JDK and Maven to build the application `.jar`. The second, final stage starts from a minimal Java Runtime Environment (JRE) base image and copies only the built application jar into it. This results in a much smaller, more secure final image because it doesn’t contain the entire build toolchain.
# Stage 1: Build the application using Maven
FROM maven:3.8.5-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
# Build the application, skipping tests for the image build
RUN mvn clean package -DskipTests
# Stage 2: Create the final, lean image
FROM eclipse-temurin:17-jre-focal
WORKDIR /app
# Copy the built jar from the builder stage
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
# Set the entrypoint to run the application
ENTRYPOINT ["java", "-jar", "app.jar"]
Pro Tip: Always use a specific base image tag (like `eclipse-temurin:17-jre-focal`) instead of `latest`. This ensures your builds are reproducible. Using `latest` is a recipe for a surprise “it broke in production” call when the base image is updated without you knowing.
Solution 3: The “Peace Treaty” (Hybrid: Jib or Layered JARs with Dockerfile)
Sometimes you want the best of both worlds: the intelligent layering of a plugin with the explicit control of a Dockerfile. This is the advanced move.
How it works: The Spring Boot plugin can be configured to unpack the JAR into distinct layers (dependencies, spring-boot-loader, snapshot-dependencies, application). You can then use a `Dockerfile` to copy these layers individually. This gives you fantastic caching in your Docker builds. A code change will only invalidate the final, tiny ‘application’ layer, making subsequent builds lightning fast.
First, configure your `pom.xml` to create the layered structure:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
Then, your `Dockerfile` becomes a bit more complex, but much more efficient:
FROM maven:3.8.5-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
# Build the jar and then extract the layers
RUN mvn clean package -DskipTests
RUN java -Djarmode=layertools -jar target/*.jar extract
# --- Second Stage ---
FROM eclipse-temurin:17-jre-focal
WORKDIR /app
# Copy the layers from the builder stage in order of volatility
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
This approach gives you the best layer caching possible while still maintaining full control over the base image and build process within your `Dockerfile`.
Final Verdict: A Quick Comparison
| Method | Control | Simplicity | Prod-Ready? |
|---|---|---|---|
| 1. Maven Plugin | Low (Black Box) | Very High | No (In my opinion) |
| 2. Dockerfile (Multi-stage) | High (Explicit) | Medium | Yes (Recommended) |
| 3. Hybrid (Layered JAR) | Very High | Low | Yes (For pros) |
My advice is simple: learn to write a good, multi-stage `Dockerfile`. It’s a foundational skill for anyone working with containers. It forces you to understand what’s actually going into your application’s environment. While the plugins are tempting shortcuts, the explicit, transparent, and controllable nature of a `Dockerfile` will save you from that 2 AM production fire drill. Your future self, and your friendly DevOps team, will thank you.
🤖 Frequently Asked Questions
âť“ Why should I use a Dockerfile instead of the Spring Boot Maven plugin for production Spring Boot API images?
For production, a multi-stage Dockerfile offers explicit control over base images, security, and reproducibility, which the Maven plugin’s ‘black box’ approach lacks. This control is crucial for patching vulnerabilities and ensuring consistent deployments.
âť“ How does the multi-stage Dockerfile approach compare to the hybrid layered JAR method for Spring Boot images?
The multi-stage Dockerfile is the recommended standard for explicit control and lean images. The hybrid layered JAR method builds upon this by enabling superior Docker build caching, making subsequent builds significantly faster by only invalidating the application layer, and is considered an advanced optimization.
âť“ What is a common implementation pitfall when defining base images in a Dockerfile for Spring Boot applications?
A common pitfall is using the `latest` tag for base images, which can lead to irreproducible builds and unexpected breakages. The solution is to always use specific, immutable base image tags like `eclipse-temurin:17-jre-focal` for consistency and reliability.
Leave a Reply