Back to blog
Java 13 min read

Quarkus Native for Kubernetes: Fast Startup and Tiny Images

Build a GraalVM native Quarkus binary, shrink the Docker layer to tens of MB, and deploy to K8s with short cold starts and modest requests/limits.

QuarkusGraalVMNativeKubernetesDocker
Quarkus Native for Kubernetes: Fast Startup and Tiny Images

Why Quarkus Native in Kubernetes

A classic JVM service on Spring Boot / Quarkus JVM often starts in seconds and wants hundreds of megabytes of heap. In Kubernetes that hurts scale-out, rolling updates and scale-to-zero: until the Pod is Ready, traffic stays away.

Quarkus + GraalVM Native Image flips the profile: cold start is often ~50–300 ms for a small REST service, RSS around 30–80 MB, and the container is tens of MB instead of hundreds. In K8s that means faster probes, lower requests/limits and denser packing per node.

A real service skeleton

mvn io.quarkus.platform:quarkus-maven-plugin:3.17.5:create \
  -DprojectGroupId=by.example \
  -DprojectArtifactId=hello-native \
  -Dextensions="rest,rest-jackson,hibernate-validator,smallrye-health,micrometer-registry-prometheus"
cd hello-native
@Path("/api/hello")
public class GreetingResource {
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public Map<String, String> hello(@QueryParam("name") @DefaultValue("world") String name) {
    return Map.of("message", "Hello, " + name);
  }
}
# application.properties
quarkus.application.name=hello-native
quarkus.http.port=8080
quarkus.native.container-build=true
quarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21
quarkus.package.jar.enabled=false

Building native (two practical options)

1. In a container (CI-friendly, no local GraalVM):

./mvnw package -Dnative -DskipTests
# artifact: target/hello-native-1.0.0-SNAPSHOT-runner
ls -lh target/*-runner

2. Local Mandrel/GraalVM 21:

export GRAALVM_HOME=/opt/mandrel-java21
./mvnw package -Dnative -Dquarkus.native.container-build=false

Ballpark numbers for a simple REST service: build 3–8 minutes, binary ~40–70 MB, startup <0.2 s on warm CPU.

Minimal Docker image

The official Quarkus path is multi-stage thinking: build native, run on micro/UBI without a JDK.

# src/main/docker/Dockerfile.native-micro
FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0
WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work /work/application \
  && chown 1001 /work \
  && chown 1001 /work/application
EXPOSE 8080
USER 1001
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
docker build -f src/main/docker/Dockerfile.native-micro -t registry.example/hello-native:1.0.0 .
docker images registry.example/hello-native:1.0.0
# often ~60–90 MB vs 250–400+ MB for a JVM image

Even tighter options exist (distroless/static) if the binary matches the libc story; start with ubi9-quarkus-micro-image for fewer DNS/certs surprises in K8s.

Deploy to Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-native
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-native
  template:
    metadata:
      labels:
        app: hello-native
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/path: /q/metrics
        prometheus.io/port: "8080"
    spec:
      containers:
        - name: app
          image: registry.example/hello-native:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: QUARKUS_HTTP_PORT
              value: "8080"
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 500m
              memory: 128Mi
          startupProbe:
            httpGet:
              path: /q/health/started
              port: 8080
            failureThreshold: 10
            periodSeconds: 1
          readinessProbe:
            httpGet:
              path: /q/health/ready
              port: 8080
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /q/health/live
              port: 8080
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: hello-native
spec:
  selector:
    app: hello-native
  ports:
    - port: 80
      targetPort: 8080

Fast startup lets you keep startupProbe short — Pods become Ready faster during rolling updates and HPA bursts.

Rough comparison (typical micro-REST)

  • Spring Boot JVM: image ~250–400 MB, start 2–8 s, RSS 200–400 MB.
  • Quarkus JVM: image ~150–250 MB, start 0.5–2 s, RSS 100–200 MB.
  • Quarkus Native: image ~60–100 MB, start <0.3 s, RSS 40–80 MB.

Native wins on burst scale and serverless-like setups (Knative, KEDA, frequent restarts).

Native pitfalls

  • Reflection/JNI/dynamic proxies need @RegisterForReflection or extension support.
  • CI is slower: cache the Mandrel builder layer in your registry.
  • Not every Spring library “just works” — prefer Quarkus extensions (REST, Hibernate ORM Panache, Kafka, Redis).
  • Harder to debug than JVM: use quarkus:dev locally, native for staging/prod images.
./mvnw quarkus:dev
./mvnw package -Dnative -DskipTests

CI snippet (GitLab)

build-native:
  image: quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21
  stage: build
  script:
    - ./mvnw -B package -Dnative -DskipTests
    - docker build -f src/main/docker/Dockerfile.native-micro
        -t $CI_REGISTRY_IMAGE/hello-native:$CI_COMMIT_SHORT_SHA .
    - docker push $CI_REGISTRY_IMAGE/hello-native:$CI_COMMIT_SHORT_SHA
  artifacts:
    paths:
      - target/*-runner

Takeaway

Quarkus Native is a practical way to get fast cold starts and small images for Kubernetes without leaving Java. Build the runner with Mandrel, pack it into micro-UBI, set modest resources and short probes. For API gateways, BFFs and low-traffic workers, the density and rollout speed usually repay the longer native build in CI.

Alexey Prybytkou

Senior Software Developer

© 2026 All rights reserved