Container Images
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Quarkus does not hand you a single "build a container" command — it hands you four interchangeable builder
extensions behind one abstraction (quarkus.container-image.*), a set of hand-editable Dockerfiles it generates
into every project, and a family of small runtime base images tuned for either the JVM or a native executable.
This page covers all three: which builder extension to add and what each one needs and produces, what the
generated src/main/docker/ Dockerfiles are for, and how to choose a base image, push to a registry, and size
container resources once the image exists. For the Spring Boot counterpart see
Java and Spring Boot Images, and for the Dockerfile
techniques underneath both, Build Cache and
Multi-Stage Builds.
The four container-image extensions
Quarkus’s container-image support is split into a common configuration namespace
(quarkus.container-image.*) plus one extension per build mechanism. Only one builder extension is normally
active at a time, and quarkus.container-image.builder picks which one runs when more than one is on the
classpath:
| Extension | Add with | Needs | Produces |
|---|---|---|---|
Jib ( |
|
Nothing extra — no Docker daemon, no |
A layered image built entirely in-process, which is also why it is the Quarkus default whenever a
container-image extension is present without an explicit |
Docker ( |
|
A running Docker daemon (or a daemon-compatible CLI, e.g. Podman configured as a Docker CLI drop-in) reachable from the build machine, plus one of the generated Dockerfiles below. |
An image built by literally invoking |
Buildpacks ( |
|
A buildpacks builder image (Cloud Native Buildpacks' equivalent of a Dockerfile base plus build logic, e.g. Paketo’s Java builder) and a container runtime capable of running it (Docker daemon by default). |
An OCI image assembled by the builder image’s buildpacks, without the project ever owning a Dockerfile — useful when a platform team standardizes on buildpacks for every language, not just Quarkus. |
S2I ( |
|
An OpenShift cluster: S2I ("Source-to-Image") is an OpenShift-native build strategy, so this extension only makes sense when the target platform already is OpenShift. |
An |
Select the active builder explicitly with:
quarkus.container-image.builder=jib
# or: docker | buildpack | s2i
The generated Dockerfiles under src/main/docker/
Regardless of which builder is selected, Quarkus scaffolds four Dockerfiles into src/main/docker/ the first
time a project is created (via the Quarkus Maven/Gradle plugin or the code-generator UI), each targeting a
different packaging output. Only the Docker extension actually consumes one of them to build an image — Jib, Buildpacks and S2I use their own build mechanisms and ignore these files — but all four remain useful as
reference or as a starting point for a hand-rolled docker build:
| File | Targets |
|---|---|
|
The default fast-jar layout (see Packaging and JVM Modes) run on a plain JDK
base image — the most common JVM-mode image, laid out to take advantage of the fast-jar directory structure
for Docker layer caching (dependencies, then the application itself, in separate |
|
The |
|
A native executable (see Native Executables with GraalVM/Mandrel) built on a general-purpose UBI base
image — glibc-linked, so it tolerates a native executable built without the |
|
The same native executable copied onto |
When the Docker extension is active, it defaults to Dockerfile.jvm for a JVM build and Dockerfile.native for
a native build, but either path is overridable:
quarkus.docker.dockerfile-jvm-path=src/main/docker/Dockerfile.jvm
quarkus.docker.dockerfile-native-path=src/main/docker/Dockerfile.native
Runtime base images and layer caching
UBI and distroless-style minimal images
Quarkus’s own default base images are Red Hat’s Universal Base Image (UBI) family, chosen because UBI images are freely redistributable while still being built and patched the way Red Hat’s own supported images are:
-
ubi9-minimal(and its JVM equivalents) — a small general-purpose UBI variant with a package manager (microdnf) still present, used as the default JVM-mode base and as theDockerfile.nativebase for a glibc-linked native executable. -
ubi-quarkus-micro— a distroless-style minimal image: stripped down to the C library and few packages a native executable actually needs at runtime, no shell or package manager retained. It is whatDockerfile.native-microbuilds on, and it is the base Quarkus recommends whenever the goal is the smallest possible attack surface for a native image (see Native Executables with GraalVM/Mandrel for how a native executable is produced in the first place).
Either base is overridable independently for JVM and native builds through the same container-image.*
namespace used to select the builder:
quarkus.container-image.base-jvm-image=registry.access.redhat.com/ubi9/openjdk-21-runtime
quarkus.container-image.base-native-image=quay.io/quarkus/ubi-quarkus-micro-image:2.0
Layer caching
Jib in particular is built around fine-grained layering: rather than one COPY instruction that invalidates
the whole image cache whenever a single class file changes, Jib splits the application into separate layers by
how often each part actually changes — typically dependencies (rarely change), resources (change
occasionally), and application classes (change on every build). Because each layer is cached and pushed
independently, a rebuild after touching only application code re-pushes just the classes layer, while the
dependency and resource layers are reused unchanged from the registry. Dockerfile.jvm’s own multi-`COPY
structure over the fast-jar layout (see Packaging and JVM Modes) achieves the same
effect for the Docker builder, for the same reason: fast-jar was designed to put rarely-changing dependency
JARs in a separate directory from the frequently-changing application classes precisely so a layered
COPY (or Jib’s layer split) can cache the former.
Pushing to a registry
Any of the four builders can push the image it produces straight to a registry as part of the build, rather than only building it locally:
quarkus.container-image.push=true
quarkus.container-image.registry=quay.io
quarkus.container-image.group=my-org
quarkus.container-image.name=my-app
quarkus.container-image.tag=1.0.0
-
quarkus.container-image.registry— the registry host (omit it, or set it todocker.io, for Docker Hub). -
quarkus.container-image.group— the namespace/organization/repository path segment under the registry (commonly a Quay or Docker Hub organization, or a project namespace). -
quarkus.container-image.name— the image name; defaults to the Maven/Gradle artifact id when unset. -
quarkus.container-image.tag— the image tag; defaults to the application version when unset. Multiple tags can be produced from one build viaquarkus.container-image.additional-tags.
The fully-qualified image reference assembled from these is
<registry>/<group>/<name>:<tag>, and it is this reference that a Kubernetes/OpenShift deployment manifest
later pulls — the manifest-generation side of that (via quarkus-kubernetes/quarkus-openshift) belongs on
Kubernetes and OpenShift, not here.
Registry authentication is supplied one of two ways:
quarkus.container-image.username=my-registry-user
quarkus.container-image.password=my-registry-password
or, when neither is set, each builder falls back to whatever credentials are already configured for it outside
Quarkus — Jib and Buildpacks read the local Docker config file (~/.docker/config.json, the same credential
store docker login populates), the Docker builder relies on the Docker daemon already being logged in, and
S2I relies on the OpenShift cluster’s own configured pull/push secrets. Credentials should be supplied through
environment variables, a secrets manager, or CI-native secret injection rather than committed as plain
application.properties values, exactly as with any other credential Quarkus config accepts (see
Secrets, TLS and Hardening).
Sizing container resources for JVM vs. native
A JVM-mode image and a native-executable image warrant very different resource requests, because they carry different memory profiles:
-
JVM mode needs headroom beyond the application’s own working set for heap, metaspace, and the JIT compiler’s own bookkeeping — a JVM given only the memory its heap needs will still be killed under memory pressure once metaspace and JIT overhead are counted. A JVM-mode container typically wants a noticeably higher memory ceiling than the equivalent native executable and benefits from an explicit heap cap (e.g.
-XX:MaxRAMPercentage) sized comfortably under the container’s memory limit rather than left to the JVM’s own container-awareness heuristics alone. -
Native mode starts with a much smaller footprint — no JIT, no bytecode interpreter, no class-loading machinery resident at runtime (see Native Executables with GraalVM/Mandrel and Build-Time Augmentation and the Closed World for why) — so it tolerates a substantially lower memory ceiling. It should still declare explicit
requests/limitsrather than none at all: an unbounded native process under a real workload can still grow beyond a careless guess, and Kubernetes needs a declared request to schedule the pod sensibly in the first place.
As a concrete starting point (to be tuned against the application’s actual profiling data, not used as a fixed rule):
# JVM-mode container
resources:
requests:
cpu: "0.5"
memory: "512Mi"
limits:
cpu: "1"
memory: "768Mi"
# Native-executable container
resources:
requests:
cpu: "0.2"
memory: "64Mi"
limits:
cpu: "0.5"
memory: "128Mi"
These resources values live in a Kubernetes/OpenShift manifest, not in application.properties — how
Quarkus generates that manifest, and the rest of the Kubernetes-specific configuration around it, is covered on
Kubernetes and OpenShift.