How Quarkus Deploys Java Apps to Kubernetes Without YAML
Build a CRUD API, generate Kubernetes manifests automatically, and run it on Minikube using Quarkus and Podman.
Have you ever wanted to ship a Java app to Kubernetes but balked at maintaining a pile of YAML? Quarkus can generate your Deployment, Service, ConfigMaps, and even liveness and readiness probes from a handful of properties and one optional fragment. You keep the logic in code and the plumbing in config. In this post we’ll deploy a small Person CRUD API to a local cluster using Minikube and Podman, push images into Minikube’s in-cluster registry, and wire the app to PostgreSQL, with almost no hand-written Kubernetes manifests.
What you’ll get: A running Quarkus app on Minikube, talking to PostgreSQL in the cluster, with health probes and config coming from a ConfigMap. We’ll focus on how the Quarkus Kubernetes extensions work together so you can reuse the same ideas on a real cluster or in CI/CD.
Prerequisites: JDK 17 or later, Maven, Podman (or Docker), and kubectl and Minikube. You can start from scratch (we’ll create the Quarkus project in Step 0) or skip to Step 1 if you already have a Quarkus app with the right extensions.
How the Quarkus Kubernetes Extensions Fit Together
Before we touch the keyboard, a quick map of what we’re using. Quarkus doesn’t rely on a single “Kubernetes” plugin; it composes several extensions. Knowing their roles makes the rest of the tutorial easier to follow.
quarkus-kubernetes— The base. It generates generic Kubernetes manifests (Deployment, Service, and so on) from application.properties and from any YAML you add under src/main/kubernetes/. It merges your fragments into one output. It also wires in ConfigMaps and Secrets you reference viaquarkus.kubernetes.env.configmapsand similar properties. No YAML for the app itself unless you want to override something.quarkus-container-image-jib— Builds an OCI container image with Jib at build time. You get a pushable image without a Dockerfile or a running Docker daemon. The image name and registry come from application.properties; the same values feed into the Deployment’simagefield. So one set of properties drives both “where the image is built” and “what image the cluster pulls.”quarkus-minikube— A thin layer on top ofquarkus-kubernetes. It adjusts the generated manifests for a local Minikube cluster: for example, image pull policy and service types that work when you’re pushing to Minikube’s registry addon instead of a remote registry. You still get one manifest file (e.g. minikube.yml) that contains Deployment, Service, and any ConfigMaps the base extension produced.
In other words: The Kubernetes extension produces the structure; Jib produces the image; the Minikube extension tunes that structure for Minikube. ConfigMap injection, health probes (once we add SmallRye Health), and the registry URL all flow from application.properties and optional YAML fragments. We’ll see that in practice next.
What We’ll Build
We’ll create a Person CRUD API (Create, Read, Update, Delete) with:
A JPA entity and a REST resource.
PostgreSQL in the cluster and configuration via a ConfigMap.
Container image built with Jib and pushed to Minikube’s registry.
NodePort exposure so we can call the API with
minikube service ... --url.Liveness and readiness probes added by SmallRye Health and the Kubernetes extension.
The only hand-written Kubernetes YAML will be the PostgreSQL Deployment and Service. Everything for the Quarkus app comes from extensions and properties.
Step 0: Create the Quarkus Project and Person API (Optional)
If you already have a Quarkus project with the Kubernetes and Jib extensions, jump to Step 1. Otherwise we’ll generate a project and replace the default endpoint with a Person CRUD API.
Generate the project
We need REST with JSON, Hibernate ORM with Panache, PostgreSQL JDBC, Jib for the container image, and the base Kubernetes extension:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=person-api \
-DclassName="com.example.GreetingResource" \
-Dpath="/hello" \
-Dextensions="rest-jackson,quarkus-hibernate-orm-panache,quarkus-jdbc-postgresql,quarkus-container-image-jib,kubernetes"
cd person-apiThis gives you a default greeting endpoint and a scaffolded entity. We’ll replace those with a Person entity and a CRUD resource.
Add the Person entity
Create the directory src/main/java/com/example/entity/ and add Person.java:
package com.example.entity;
import java.time.LocalDate;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birthDate;
public Person(String name, LocalDate birthDate) {
this.name = name;
this.birthDate = birthDate;
}
public Person() {
}
}PanacheEntity gives you an id field and static helpers like findById(), listAll(), and deleteById(), plus instance methods like persist() and persistAndFlush().
Add the Person REST resource
Create src/main/java/com/example/resource/PersonResource.java and delete the generated GreetingResource.java:
package com.example.resource;
import java.util.List;
import com.example.entity.Person;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("/persons")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PersonResource {
@GET
public List<Person> listAll() {
return Person.listAll();
}
@GET
@Path("/{id}")
public Response findById(@PathParam("id") Long id) {
Person person = Person.findById(id);
if (person == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.ok(person).build();
}
@POST
@Transactional
public Response create(Person person) {
if (person == null || person.id != null) {
throw new jakarta.ws.rs.WebApplicationException("Person ID must not be set", 422);
}
person.persistAndFlush();
return Response.status(Response.Status.CREATED).entity(person).build();
}
@PUT
@Path("/{id}")
@Transactional
public Response update(@PathParam("id") Long id, Person person) {
Person existing = Person.findById(id);
if (existing == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
existing.name = person.name;
existing.birthDate = person.birthDate;
return Response.ok(existing).build();
}
@DELETE
@Path("/{id}")
@Transactional
public Response delete(@PathParam("id") Long id) {
boolean deleted = Person.deleteById(id);
return deleted ? Response.noContent().build() : Response.status(Response.Status.NOT_FOUND).build();
}
}
Configure the datasource and run locally (optional)
Add to src/main/resources/application.properties:
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.schema-management.strategy=drop-and-create
quarkus.hibernate-orm.log.sql=trueWith Podman (or Docker) running, start the app in dev mode:
./mvnw quarkus:devQuarkus Dev Services will start PostgreSQL in a container. Try the API:
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"Ada Lovelace", "birthDate":"1815-12-10"}' \
http://localhost:8080/persons
curl http://localhost:8080/personsStop with Ctrl+C when you’re done. Next we’ll point this app at a Kubernetes cluster.
Step 1: Start Minikube with Podman and the Registry Addon
We use Podman as the Minikube driver so we don’t need a separate Docker daemon. We use the containerd runtime inside the cluster (instead of CRI-O) so Minikube’s registry addon and addon verification works. There’s some development happening right now and a couple of hiccups you could run into with other configurations. If you’re on MacOS, stick to below order.
Install Minikube if needed (e.g. brew install minikube on macOS). Then start the cluster:
minikube start --driver=podman --container-runtime=containerd \
--insecure-registry="10.0.0.0/24"The --insecure-registry flag lets the in-cluster runtime pull from the registry addon over plain HTTP. Keep this cluster running for the rest of the tutorial.
Enable the registry addon so we can push images from the host into the cluster:
minikube addons enable registryThe registry runs inside the cluster. Later we’ll use a port-forward so the host can reach it at localhost:5000 for Jib.
Verify the cluster:
kubectl cluster-infoYour kubectl context should point at the Minikube cluster.
Step 2: Add the Minikube Extension
From the project root (person-api if you created it in Step 0):
quarkus extension add quarkus-minikubeThis adds the Minikube-specific tweaks to the manifests the base Kubernetes extension generates (e.g. image pull policy and service defaults for a local cluster).
Step 3: Deploy PostgreSQL in the Cluster
The Person API needs PostgreSQL. We deploy it with a small bit of YAML—the only manual Kubernetes we’ll write. Create postgresql.yaml in the project root (or a k8s folder):
apiVersion: v1
kind: Service
metadata:
name: postgresql
spec:
selector:
app: postgresql
ports:
- port: 5432
targetPort: 5432
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgresql
spec:
replicas: 1
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
spec:
containers:
- name: postgresql
image: docker.io/library/postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
value: user
- name: POSTGRES_PASSWORD
value: pass
- name: POSTGRES_DB
value: example
Apply it and wait for the pod to be ready:
kubectl apply -f postgresql.yaml
kubectl get pods -l app=postgresql -wWhen the pod is Running and 1/1, press Ctrl+C. If the pod stays in ImagePullBackOff, run minikube image pull docker.io/library/postgres:16 and delete the pod so it is recreated.
Step 4: Wire the Datasource via ConfigMap
We keep configuration out of the image by putting datasource settings in a ConfigMap and telling the Quarkus Kubernetes extension to inject them into the generated Deployment.
Create src/main/kubernetes/common.yml:
apiVersion: v1
kind: ConfigMap
metadata:
name: postgresql-datasource-props
data:
POSTGRESQL_URL: "jdbc:postgresql://postgresql:5432/example"
POSTGRESQL_USER: "user"
POSTGRESQL_PASSWORD: "pass"The Kubernetes extension merges this into the generated output. In application.properties add:
%prod.quarkus.datasource.username=${POSTGRESQL_USER}
%prod.quarkus.datasource.password=${POSTGRESQL_PASSWORD}
%prod.quarkus.datasource.jdbc.url=${POSTGRESQL_URL}
quarkus.kubernetes.env.configmaps=postgresql-datasource-propsThe %prod. prefix applies those properties only when the prod profile is active (e.g. in Kubernetes). In dev, Quarkus keeps using Dev Services. In the cluster, the app reads the values from the ConfigMap. No credentials in the image or in hand-written Deployment YAML.
Step 5: Configure the Image and Deployment in application.properties
We tell Jib where to push the image and how the Deployment should look. Add (or adjust) in application.properties:
quarkus.container-image.registry=localhost:5000
quarkus.container-image.group=
quarkus.container-image.name=person-api
quarkus.container-image.tag=1.0
quarkus.container-image.build=true
quarkus.container-image.push=true
quarkus.container-image.insecure=true
quarkus.kubernetes.replicas=1
quarkus.kubernetes.deployment-target=kubernetes
quarkus.kubernetes.ingress.expose=false
quarkus.kubernetes.service-type=NodePortregistry=localhost:5000 — Jib will push to the Minikube registry addon; we’ll expose it on the host with a port-forward.
push=true and insecure=true — Build pushes over HTTP (no TLS on the in-cluster registry).
tag=1.0 — A fixed tag avoids Kubernetes defaulting to
imagePullPolicy: Alwaysthat:latesttriggers.service-type=NodePort — So we can reach the app with
minikube service person-api --url.
The Minikube extension will emit target/kubernetes/minikube.yml with the Deployment, Service, and the ConfigMap reference.
If you created the project in Step 0: The generated test classes still target the old /hello endpoint. Delete or replace GreetingResourceTest and GreetingResourceIT with tests for the Person CRUD API (e.g. Rest Assured against /persons), and add @Transactional to any test method that calls Person.deleteAll(), so the build and Step 6 can succeed.
Step 6: Build and Push the Image
We need the host to reach the in-cluster registry at localhost:5000. In a separate terminal (or in the background) run:
kubectl port-forward -n kube-system service/registry 5000:80 &Check that the registry is up:
curl -s http://localhost:5000/v2/ && echo "Registry OK"You should see {} followed by “Registry OK.”
Build and push in one step:
./mvnw clean packageWith quarkus.container-image.build=true and quarkus.container-image.push=true, Maven builds the image with Jib and pushes it to localhost:5000/person-api:1.0. The manifests land under target/kubernetes/. Optional check: curl -s http://localhost:5000/v2/person-api/tags/list should show the tag 1.0.
Step 7: Deploy the Application
Apply the generated Minikube manifest:
kubectl apply -f target/kubernetes/minikube.ymlOpen target/kubernetes/minikube.yml and you’ll see the Deployment, Service, and ConfigMap (and the Deployment’s envFrom or valueFrom pointing at the ConfigMap). All of that came from the Kubernetes and Minikube extensions plus application.properties and common.yml.
Wait for the app pod to be ready:
kubectl get pods
kubectl get servicesThen get the URL and test the API:
minikube service person-api --url
# Use that URL, e.g. http://192.168.49.2:31234
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"Ada Lovelace", "birthDate":"1815-12-10"}' \
http://<URL>/persons
curl http://<URL>/personsThe Person API is now running in Kubernetes and talking to PostgreSQL in the cluster.
Step 8: Add Liveness and Readiness Probes
Kubernetes uses probes to decide when to restart a pod (liveness) and when to send traffic (readiness). We add the SmallRye Health extension; Quarkus exposes /q/health/live and /q/health/ready, and the Kubernetes extension adds the corresponding probe blocks to the Deployment.
quarkus add extension quarkus-smallrye-healthRebuild, push, and redeploy (keep the port-forward from Step 6 running):
./mvnw clean package
kubectl apply -f target/kubernetes/minikube.ymlIn target/kubernetes/minikube.yml you’ll see livenessProbe and readinessProbe on the Deployment. No manual YAML for probes.
Common Pitfalls
Port-forward must be running when you build. If Jib can’t reach
localhost:5000, the push fails. Startkubectl port-forward -n kube-system service/registry 5000:80before./mvnw clean package.Use containerd with Podman. If you use
--container-runtime=cri-o, the registry addon enable step can hang with “open /run/runc: no such file or directory.” Switching to--container-runtime=containerdavoids that.Project name vs. service name. The service name in Kubernetes comes from
quarkus.container-image.name(e.g.person-api). Use that name inminikube service person-api --url.
What We Automated (and What We Didn’t)
Automated by Quarkus:
Deployment, Service, and ConfigMap reference from application.properties and src/main/kubernetes/common.yml.
Liveness and readiness probes from SmallRye Health and the Kubernetes extension.
ConfigMap injection via
quarkus.kubernetes.env.configmaps.Container image build and push via Jib; no Dockerfile and no Docker daemon.
Manual:
The PostgreSQL Deployment and Service YAML.
Enabling the Minikube registry addon and running the port-forward so the host can push to it. In CI/CD you’d push to a real registry and point the Deployment at that image.
Next Steps
Point the same app at a real cluster: set
quarkus.container-image.registry(and optionallypush=true) to your registry, then deploy the generated manifest (e.g. kubernetes.yml or a variant).Add security: protect the REST API with OIDC (e.g. Keycloak) and let the Kubernetes extension wire the auth config into the Deployment.
Tweak the Kubernetes extension: explore
quarkus.kubernetes.*and Quarkus Deploying to Kubernetes for resource limits, labels, and multiple deployment targets.
Your Person API is running on Minikube with PostgreSQL and health probes, and you got there with almost no hand-written YAML. Thanks to the Quarkus Kubernetes extensions and a local stack built around Minikube and Podman.



Surprized to learn that, in order to avoid manually writing YAML files, one needs to ... manually write postgres.yaml and common.yaml.