Sub-Module 1.2: Multi-Stage Builds

Sub-Module Overview

Duration: 10-12 minutes
Learning Objectives:

  • Examine a sample Quarkus REST application

  • Create multi-stage Containerfiles with builder + runtime

  • Build production-ready minimal container images

  • Test and verify built applications

Multi-stage builds with hardened images

In Sub-Module 1.1, you explored basic hardened images and saw how the builder variant includes tools we don’t want in production. Now you’ll learn the most important pattern for production deployments: multi-stage builds.

Multi-stage builds allow you to separate the final application artifacts from build time components and to choose the smallest runtimes for production. This keeps with the overall goal of minimal images, reduced attack and risk surfaces. Multi-stage builds provide a good blend of flexibility and control.

This will combine the use of different variants for different purposes.

Sample Java Application

A complete Quarkus REST application has been created to demonstrate multi-stage build patterns. Let’s examine the generated structure:

Step 1: Examine Pre-Built Application

ls -la ~/sample-app/

Feel free to examine the code if you’re interested. For our purposes we need to know the application provides two REST endpoints:

  • / - Returns JSON greeting with runtime information

  • /health - Returns application health status for orchestrator probes

This application listens on all interfaces (0.0.0.0) port 8080 for container deployment.

Multi-Stage Containerfile

Multi-stage builds create new images for each set of steps, delimited by a new FROM directive, effectively creating a scripted build in a single Containerfile. The first stage will use the builder variant of the openjdk-21 image. This provides additional tools like dnf to install RPMs into the environment. Once we’ve created the application with maven, we can copy just the necessary application files to the runtime variant for deployment. Not only do all of the maven build artifacts stay in the first stage, but so do any additional packages or tools we may have needed to get a working build environment.

Step 2: Examine Multi-Stage Containerfile

A multi-stage Containerfile demonstrates a more advanced container pattern. Let’s examine its structure:

cat ~/sample-app/Containerfile
Expected output:
# Multi-stage build: builder -> runtime

# ============================================
# Stage 1: Build stage using builder variant (1)
# ============================================
FROM registry.access.redhat.com/hi/openjdk:21-builder AS builder

# Install unzip needed by the Maven wrapper to extract the Maven distribution
USER root
RUN dnf install -y unzip && dnf clean all

WORKDIR /build

# Copy Maven wrapper and dependency manifest first (layer cache) (2)
COPY mvnw pom.xml ./
COPY .mvn ./.mvn

# Download dependencies (cached unless pom.xml changes) (2)
RUN ./mvnw dependency:go-offline -B

# Copy source and build
COPY src ./src
RUN ./mvnw package -DskipTests -B

# ============================================
# Stage 2: Runtime stage (1)
# ============================================
FROM registry.access.redhat.com/hi/openjdk:21-runtime

WORKDIR /app

# Copy the Quarkus fast-jar layout (3)
COPY --from=builder --chown=65532:65532 /build/target/quarkus-app/lib/ ./lib/
COPY --from=builder --chown=65532:65532 /build/target/quarkus-app/*.jar ./
COPY --from=builder --chown=65532:65532 /build/target/quarkus-app/app/ ./app/
COPY --from=builder --chown=65532:65532 /build/target/quarkus-app/quarkus/ ./quarkus/

# Run as non-root user (4)
USER 65532

# Expose port
EXPOSE 8080

# JVM configuration
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0"

# Start application
ENTRYPOINT ["java", "-jar", "quarkus-run.jar"]

Multi-stage build architecture concepts

1 Builder/Runtime Stages: Uses openjdk:21-builder with full JDK + Maven for compilation, then openjdk:21-runtime with minimal JRE for deployment
2 Layer Optimization: Dependencies copied separately from source for efficient rebuilds
3 Quarkus Fast-JAR: Optimized layer creation with separated lib/, app/, quarkus/ directories
4 Security Patterns: Non-root user (65532), proper file ownership with --chown

Building the Image

Step 3: Build with Podman

Building a multi-stage image with podman is no different than any other image. The stages are automatically handled as part of the standard OCI specification.

podman build -t hummingbird-demo:v1 -f ~/sample-app/Containerfile ~/sample-app

This build may take 2-3 minutes to complete

Expected output (excerpt):
[1/2] STEP 1/9: FROM {hummingbird-registry}/openjdk:21-builder AS builder
[1/2] STEP 2/9: USER root
[1/2] STEP 3/9: RUN microdnf install -y unzip && microdnf clean all
[1/2] STEP 4/9: WORKDIR /build
[1/2] STEP 5/9: COPY mvnw pom.xml ./
...
[INFO] BUILD SUCCESS
...
[2/2] STEP 1/10: FROM {hummingbird-registry}/openjdk:21-runtime
...
[2/2] STEP 10/10: ENTRYPOINT ["java", "-jar", "quarkus-run.jar"]
COMMIT hummingbird-demo:v1
Successfully tagged localhost/hummingbird-demo:v1

Testing the Application

Step 4: Run and Test the Container

Run the container and test the API endpoints, use -s to quiet any non-essential output from curl.

podman run -d --rm --name demo -p 8080:8080 hummingbird-demo:v1
curl -s http://localhost:8080/ | jq
Expected output:
{
  "message": "Hello from Hummingbird!",
  "runtime": "Java 21.0.10",
  "platform": "linux",
  "timestamp": "2026-03-06T12:34:56.789Z"
}

Test the health endpoint:

curl -s http://localhost:8080/health | jq
Expected output:
{
  "status": "healthy"
}

Step 5: View Container Logs

podman logs demo
Expected output:
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
...
Listening on: http://0.0.0.0:8080

Step 6: Stop and Remove Container

podman stop demo

Summary

Congratulations! You’ve completed Sub-Module 1.2 and mastered multi-stage builds with Red Hat Hardened Images.

What You’ve Accomplished

✅ Examined a sample Quarkus REST application
✅ Understood multi-stage Containerfile patterns
✅ Built a production image with separate builder + runtime stages
✅ Compared builder vs runtime image sizes
✅ Tested the running application and verified health endpoint

Key Takeaways

Multi-Stage Build Pattern:

  • Stage 1 (Builder): Hardened image (~535MB) with all build tools (JDK, Maven)

  • Stage 2 (Runtime): Hardened image (~258MB) with only the JRE

  • Result: ~272MB final image with application

Best Practices Applied:

  • Non-root user (UID 65532)

  • Minimal runtime image (no build tools, no package managers, no compiler)

  • Proper file ownership with --chown

  • Dependency layer caching for faster rebuilds

Why Multi-Stage Matters:

  • Separates build-time from run-time dependencies

  • Reduces final image size dramatically

  • Eliminates attack surface from build tools

  • Maintains development flexibility

Next Steps

In Sub-Module 1.3: Vulnerability Scanning & SBOMs, you’ll learn how to analyze the security posture of your images, scan for CVEs, and generate Software Bill of Materials for compliance.

Troubleshooting

Issue: Build fails with Maven dependency download errors

# Clear build cache and rebuild
podman build --no-cache -t hummingbird-demo:v1 .

# Check network connectivity
curl https://repo.maven.apache.org/maven2/

Issue: Image size larger than expected

# Verify multi-stage build is working
podman history hummingbird-demo:v1

# Check if builder stage artifacts leaked into runtime
podman run --rm hummingbird-demo:v1 ls /build 2>/dev/null && echo "LEAK" || echo "OK"

Issue: Container fails to start

# Check logs for errors
podman logs demo

# Verify the fast-jar layout is intact
podman run --rm hummingbird-demo:v1 ls -la /app/

# Test Java availability in the runtime image
podman run --rm hummingbird-demo:v1 java -version

Issue: Maven build fails inside the container

# Run builder stage interactively to debug
podman run --rm -it {ubi-registry}/openjdk-21:latest bash

# Verify Maven wrapper is executable
ls -la mvnw
chmod +x mvnw
./mvnw --version