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
@RegisterForReflectionor 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:devlocally, 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.
