Chapter 68: Podman in Depth: Install, Rootless Containers, Pods, Compose, Kube YAML and Alternatives
68.5 podman generate kube, podman kube play and Quadlet: from a laptop pod to Kubernetes and systemd
Why: the problem
This lesson is where Podman connects directly to the rest of the course. You have a working pod on your laptop. With one command, podman kube generate, Podman writes a Kubernetes manifest that describes that pod: containers, images, environment, ports, volumes and, since Podman 6.1, healthchecks turned into livenessProbes. With another command, podman kube play, Podman reads a Kubernetes manifest and runs it locally as pods, without any cluster. And with Quadlet, a small file in ~/.config/containers/systemd, systemd starts that same manifest at boot, restarts it on failure and logs to journald. Why does that matter? Writing Kubernetes YAML by hand from zero is slow and error prone, especially for beginners. Generating it from something that already works gives a correct starting point that you then clean up: add resource requests, replace plain env values with Secrets, and change the Pod into a Deployment. In the other direction, kube play lets a developer test the exact manifest that will go to the cluster, on a laptop with no kind or minikube, which is handy on low-memory machines. Note the older names: podman generate kube and podman play kube still work as aliases, but the current commands are podman kube generate and podman kube play. Where does Quadlet fit? Many servers do not need Kubernetes at all. A single RHEL VM running an internal tool at a bank or a factory just needs the containers to start at boot and restart if they die. The old answer was podman generate systemd, which wrote long unit files and is now deprecated. Quadlet replaces it: you write a short .container, .pod or .kube file, run systemctl daemon-reload, and a generator turns it into a proper systemd service. Red Hat recommends Quadlet for all new Podman services, and it works rootless with lingering from 68.2. We will generate YAML from the orders pod, clean it up, replay it with kube play, take it down, apply it to the kind cluster from 67.3 to prove it is real Kubernetes, and finally run it as a Quadlet .kube unit that survives a reboot.
What: every term in plain English
- podman kube generate — Writes Kubernetes YAML from pods, containers or volumes.
- podman kube play — Runs Kubernetes YAML locally as Podman pods.
- podman kube down — Stops and removes what kube play created.
- -s / --service — Also generates a Service for the pod's ports.
- --type deployment — Generates a Deployment instead of a bare Pod.
- Quadlet — systemd generator that turns small unit files into Podman services.
- .container file — Quadlet unit for one container.
- .kube file — Quadlet unit that runs a Kubernetes YAML with kube play.
- systemd generator — Program that creates units at daemon-reload time.
- AutoUpdate — Quadlet key that lets podman auto-update pull new images.
How: step by step
- Start the orders pod from 68.3. Pod running.
- Generate a Deployment and Service. orders.yaml written.
- Clean up the YAML. Secret reference, resources.
- Remove the pod and replay with kube play. Pod back from YAML.
- Take it down with kube down. Nothing left.
- Apply the same YAML to kind. Pods Running in Kubernetes.
- Write a Quadlet .kube unit. Service generated.
- Start it and check it survives restart. active (running).
Picture three doors in the same corridor. The first door, kube generate, lets you walk out of Podman into Kubernetes carrying a manifest. The second door, kube play, lets you walk back in with a manifest from the cluster and run it locally. The third door, Quadlet, leads to systemd, the part of Linux that starts things at boot. Because the same YAML file passes through all three doors, a small team can begin on one server with Quadlet, test changes on laptops with kube play, and move to a real cluster later without rewriting anything. This is a common path for startups in Pune and Bengaluru: one VM first, Kubernetes only when traffic and team size make it worth the extra work.
Example 1: Generate Kubernetes YAML from the running pod
Goal. Generate Kubernetes YAML from the running pod.
gen.sh
set -euo pipefail
podman pod create --name orders -p 8080:8080
podman run -d --pod orders --name db -e POSTGRES_USER=orders -e POSTGRES_DB=orders \
-e POSTGRES_PASSWORD=REPLACE_WITH_DB_PASSWORD \
--health-cmd 'pg_isready -U orders' --health-interval 10s \
docker.io/library/postgres:16-alpine
podman run -d --pod orders --name api -e DB_HOST=localhost -e DB_USER=orders \
registry.example.com/shop/orders-api:1.4.0
podman kube generate orders --type deployment --replicas 1 -s -f orders.yaml
grep -nE 'kind:|image:|containerPort|livenessProbe|POSTGRES_PASSWORD' orders.yaml
What each line does
--health-cmd— Podman healthcheck; Podman 6.1 exports it as a livenessProbe.--type deployment— Writes a Deployment wrapping the pod template, ready for a cluster.-s— Adds a Service with the pod's published ports.-f orders.yaml— Writes to a file instead of the screen.grep— Quick look at the important lines before editing.
Green output
$ bash gen.sh
3:kind: Service
19:kind: Deployment
38: image: docker.io/library/postgres:16-alpine
44: - name: POSTGRES_PASSWORD
47: livenessProbe:
55: image: registry.example.com/shop/orders-api:1.4.0
58: - containerPort: 8080
Expected result. A Deployment and a Service in one command. The password is in plain text in the YAML, which is fine for a lab placeholder but must become a Secret before anything leaves your laptop. That cleanup is the next example.
Example 2: Clean up the YAML, replay it with kube play, then apply it to kind
Goal. Clean up the YAML, replay it with kube play, then apply it to kind.
play.sh
set -euo pipefail
podman pod rm -f orders
sed -i 's/^\(\s*\)value: REPLACE_WITH_DB_PASSWORD/\1valueFrom: {secretKeyRef: {name: orders-db, key: password}}/' orders.yaml
printf '%s\n' 'apiVersion: v1' 'kind: Secret' 'metadata: {name: orders-db}' \
'stringData: {password: REPLACE_WITH_DB_PASSWORD}' > secret.yaml
podman kube play secret.yaml
podman kube play orders.yaml
podman pod ps --format '{{.Name}} {{.Status}} {{.NumberOfContainers}}'
curl -s localhost:8080/health; echo
podman kube down orders.yaml && podman kube down secret.yaml
kubectl config use-context kind-shop
kubectl apply -f secret.yaml -f orders.yaml
kubectl rollout status deploy/orders --timeout=120s
kubectl get pods -l app=orders
What each line does
sed valueFrom— Swaps the plain value for a secretKeyRef, the standard Kubernetes way.secret.yaml— Placeholder Secret; Podman's kube play turns Kubernetes Secrets into Podman secrets.kube play— Creates the pod named orders-pod from the Deployment template and publishes the Service port.kube down— Removes exactly what the file created.kubectl apply— The same two files run on the kind cluster from 67.3 without changes.
Green output
$ bash play.sh
Secrets:
orders-db
Pod:
5c4d3e2f1a0b...
orders-pod Running 3
{"status":"ok","db":"up"}
Pods stopped: 5c4d3e2f1a0b
Pods removed: 5c4d3e2f1a0b
secret/orders-db created
deployment.apps/orders created
service/orders created
deployment "orders" successfully rolled out
NAME READY STATUS RESTARTS AGE
orders-6f8d9c7b5-x2k4p 2/2 Running 0 21s
Expected result. One YAML, two runtimes. The manifest you tested with podman kube play is the same one Kubernetes accepted, which removes the classic 'works on my machine but not in the cluster' gap.
Example 3: Run the YAML at boot with a Quadlet
Goal. Run the YAML at boot with a Quadlet .kube unit.
quadlet.sh
set -euo pipefail
mkdir -p ~/.config/containers/systemd
cp orders.yaml secret.yaml ~/.config/containers/systemd/
printf '%s\n' '[Unit]' 'Description=Orders app from Kubernetes YAML' '' \
'[Kube]' 'Yaml=orders.yaml' 'PublishPort=8080:8080' 'AutoUpdate=registry' '' \
'[Service]' 'Restart=always' 'ExecStartPre=/usr/bin/podman kube play --replace %h/.config/containers/systemd/secret.yaml' '' \
'[Install]' 'WantedBy=default.target' > ~/.config/containers/systemd/orders.kube
systemctl --user daemon-reload
systemctl --user start orders.service
systemctl --user status orders.service --no-pager | sed -n '1,3p'
loginctl enable-linger $USER
podman auto-update --dry-run --format '{{.Unit}} {{.Image}} {{.Updated}}'
journalctl --user -u orders.service -n 3 --no-pager
What each line does
~/.config/containers/systemd— Rootless Quadlet folder; /etc/containers/systemd is the rootful one.[Kube] Yaml=— Tells Quadlet to run this manifest with kube play.AutoUpdate=registry— podman auto-update pulls newer images with the same tag and restarts the unit.daemon-reload— Runs the Quadlet generator, which writes orders.service.enable-linger— Keeps the service running after logout and starts it at boot.
Green output
$ bash quadlet.sh
● orders.service - Orders app from Kubernetes YAML
Loaded: loaded (/home/ravi/.config/containers/systemd/orders.kube; generated)
Active: active (running) since Sun 2026-10-25 11:42:10 IST; 3s ago
orders.service registry.example.com/shop/orders-api:1.4.0 false
podman[4121]: Pod: 7e6d5c4b3a2f
podman[4121]: Container: api
systemd[1]: Started orders.service - Orders app from Kubernetes YAML.
Expected result. A six-line unit turned the Kubernetes manifest into a real systemd service with restart, boot start, journald logs and image auto-update. For a single server this is often all you need, and the YAML is ready for a cluster whenever you grow.
Every command, step by step (Podman lab)
-
Generate
podman kube generate orders -s -f orders.yamlWhat it does: YAML.
orders.yaml -
Deployment
podman kube generate orders --type deploymentWhat it does: Kind.
kind: Deployment -
Play
podman kube play orders.yamlWhat it does: Run.
Pod: 5c4d... -
Pods
podman pod psWhat it does: Check.
orders-pod Running -
Down
podman kube down orders.yamlWhat it does: Remove.
Pods removed -
Cluster
kubectl apply -f orders.yamlWhat it does: kind.
deployment.apps/orders created -
Quadlet
ls ~/.config/containers/systemdWhat it does: Units.
orders.kube orders.yaml -
Reload
systemctl --user daemon-reloadWhat it does: Generate.
(no output) -
Status
systemctl --user status ordersWhat it does: Service.
active (running) -
Dry run
podman auto-update --dry-runWhat it does: Updates.
orders.service false
When it is red 1
Error: orders.yaml: unsupported kind StatefulSet
Cause. kube play supports Pod, Deployment, DaemonSet, Job, ConfigMap, Secret, PVC, not every kind.
Fix, step by step
- Use a Deployment for local play, or test StatefulSets on kind.
When it is red 2
Failed to start orders.service: Unit orders.service not found
Cause. Generator did not run or the file has a typo.
Fix, step by step
- Run /usr/libexec/podman/quadlet -dryrun -user to see parse errors, then daemon-reload.
When it is red 3
Error: no such secret orders-db
Cause. Secret not created before the pod.
Fix, step by step
- kube play the Secret first or use ExecStartPre.
When it is red 4
WARNING: podman generate systemd is deprecated
Cause. Old method.
Fix, step by step
- Write a Quadlet .container or .kube file instead.
When it is red 5
auto-update: no image with label io.containers.autoupdate
Cause. AutoUpdate not set.
Fix, step by step
- Add AutoUpdate=registry to the unit and use a full image name.
Practice tasks
Easy Task 1: Generate
Why: YAML.
What to do: Generate YAML from a pod.
- kind: Pod.
Solution
podman kube generate ordersEasy Task 2: Service
Why: Ports.
What to do: Add a Service with -s.
- kind: Service.
Solution
grep kind orders.yamlEasy Task 3: Play
Why: Run.
What to do: Replay with kube play.
- Running.
Solution
podman pod psMedium Task 4: Secret
Why: Clean.
What to do: Move the password into a Secret.
- secretKeyRef.
Solution
grep -n secretKeyRef orders.yamlMedium Task 5: Cluster
Why: kind.
What to do: Apply to kind.
- 2/2 Running.
Solution
kubectl get podsMedium Task 6: Container unit
Why: Quadlet.
What to do: Write a .container unit for Redis.
- active.
Solution
systemctl --user status redisHard Task 7: Kube unit
Why: Quadlet.
What to do: Run orders.yaml with a .kube unit.
- active.
Solution
systemctl --user status ordersHard Task 8: Infosys app
Why: Main task.
What to do: Full flow with auto-update.
- Restarts.
Solution
journalctl --user -u timesheetHomework: try at home
For an Infosys-style internal timesheet app, run Postgres and the app in a pod, generate a Deployment and Service, move the password into a Secret, replay with kube play, apply the same files to kind, then install a Quadlet .kube unit with AutoUpdate and lingering, and prove it restarts after podman pod kill.
Recap
- podman kube generate turns working pods into Kubernetes YAML.
- podman kube play runs Kubernetes YAML locally; kube down removes it.
- Always replace plain env secrets with Secret references before sharing YAML.
- Quadlet .container, .pod and .kube files replace generate systemd.
- AutoUpdate and lingering make rootless services production-ready on one server.
FAQ
Q. Is generated YAML production-ready?
It is a correct starting point, not the final file. Add resource requests and limits, readiness probes, Secrets, labels for your team and a proper image tag policy. Many teams generate once, then keep the YAML in Git and edit it by hand.
Q. Can kube play use a ConfigMap?
Yes. Pass it with --configmap or include it in the same file, and Podman mounts or injects it like Kubernetes.
Q. Should I write a .container or a .kube Quadlet?
Use .container for a single container on a server. Use .kube when you already have Kubernetes YAML and want one source of truth for laptop, server and cluster.
Q. Does Quadlet need root?
No. Rootless units live in ~/.config/containers/systemd and run as your user with systemctl --user; rootful units go in /etc/containers/systemd.
Q. How do I see the unit Quadlet generated?
Run systemctl --user cat orders.service, or /usr/libexec/podman/quadlet -dryrun -user, which prints every generated unit and any parse errors without starting anything.
Interview questions
-
How do you move a Podman workload to Kubernetes?
Model answer: podman kube generate with --type deployment and -s, clean up secrets and resources, then kubectl apply.
-
How do you test Kubernetes YAML without a cluster?
Model answer: podman kube play the file, test, then podman kube down.
-
What is Quadlet and why use it?
Model answer: A systemd generator that turns short unit files into Podman services; it replaced generate systemd.
-
How does podman auto-update work?
Model answer: It checks images of units with AutoUpdate, pulls newer digests, restarts the unit and rolls back on failure.
Quick quiz
Q1. Which command writes YAML from a pod?
podman kube generate.
Q2. Which runs YAML locally?
podman kube play.
Q3. Which replaces generate systemd?
Quadlet.
Q4. Where do rootless Quadlet files go?
~/.config/containers/systemd.
Q5. Which key enables image updates?
AutoUpdate=registry.
Samajla ka?