| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64 |
| Applies to | Linux hosts with Podman where you want to rehearse Kubernetes-style YAML locally without a cluster API server |
| Privilege | Normal user for rootless kube play; root for image-volume workloads and some systemd unit paths |
| Scope | podman kube play and podman play kube alias, supported Kubernetes kinds on Podman 5.8.2, field-level support limits, Deployment replica behavior, silently ignored unsupported fields, newer --validate version gate, ConfigMaps and Secrets, volume mappings, --build, --replace, kube down, stdin and URL input, --userns, brief .kube Quadlet and podman-kube@.service notes, and comparison to a real cluster. Does not cover generating YAML from containers, applying manifests to a Kubernetes API server, or general Kubernetes manifest teaching. |
| Related guides | Podman volumes Podman vs Docker Migrate from Docker to Podman |
You have Kubernetes-style YAML and want to see what it does on a single Linux box — without installing a control plane, CNI controller farm, or worker nodes. podman kube play reads that YAML and creates local Podman objects.
This is not a Kubernetes cluster. There is no scheduler, no kubelet, and no Deployment controller reconciling replicas. Podman translates supported fields into pods, containers, volumes, and secrets on one host. This guide shows what actually works on Podman 5.8.2, what gets ignored, and where rehearsal stops matching cluster behavior.
What podman kube play does
Kubernetes-style YAML
│
▼
podman kube play
│
▼
local Podman objects
├── pods
├── containers
├── volumes
├── secrets
└── supporting configPodman forks processes per operation the same way as normal CLI use. The YAML path is a convenience layer: supported kind values become Podman resources, not API objects inside etcd.
That distinction matters when you rehearse a Deployment. On a cluster, controllers keep desired state. With kube play, you get one local interpretation of the manifest — and many Kubernetes fields never become Podman configuration at all.
podman kube play vs podman play kube
Current preferred syntax groups Kubernetes helpers under podman kube:
podman kube play workload.yamlThe older grouping still works:
podman play kube workload.yamlpodman play kube is an officially supported alias — not deprecated on Podman 5.8.2. Both commands share the same flags and behavior. This article uses podman kube play for consistency with current documentation.
Lab setup
Create an isolated directory for the examples:
mkdir -p ~/podman-kube-play-lab && cd ~/podman-kube-play-labConfirm the subcommand and supported kinds on your build:
podman kube play --helpSample output (trimmed):
Play a pod or volume based on Kubernetes YAML
...
podman kube play [options] [KUBEFILE [KUBEFILE...]]|-
...
--build Build all images in a YAML (given Containerfiles exist)
--configmap Pathname Pathname of a YAML file containing a kubernetes configmap
--replace Delete and recreate pods defined in the YAML file
--userns string User namespace to useOn Podman 5.8.2, --validate does not appear in this help output. That flag arrives in newer Podman releases and is covered later as a version gate — not part of the baseline lab commands here.
Run a simple Pod manifest
Save a minimal Pod that publishes nginx on host port 8080:
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: docker.io/library/nginx:latest
ports:
- containerPort: 80
hostPort: 8080Create the pod and container from that file:
podman kube play web.yamlSample output:
Pod:
5004356ec003e166bb20dc4ae85f26b0c4ba30911a114f720b5afc6d8f4b70e5
Container:
def99db10fd9476b49180d5dd1dfa24e83c9dc2d8d5a682e1712a6f39763a4aaThe command prints Pod and container IDs. List pods to see the infra container Podman adds for networking:
podman pod psSample output:
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
5004356ec003 web Running 47 seconds ago 50bdd8e53d47 2List containers with their parent pod:
podman ps --podSample output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES POD ID PODNAME
50bdd8e53d47 47 seconds ago Up 9 seconds 0.0.0.0:8080->80/tcp 5004356ec003-infra 5004356ec003 web
def99db10fd9 docker.io/library/nginx:latest nginx -g daemon o... 8 seconds ago Up 8 seconds 0.0.0.0:8080->80/tcp, 8080/tcp web-web 5004356ec003 webConfirm nginx responds inside the workload container:
podman exec web-web curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:80Sample output:
200Host port publishing can behave differently by network stack and image listen ports. When curl http://127.0.0.1:8080 fails on your host, check podman port web-web and test from the pod network namespace or pod IP before assuming the YAML failed.
Supported Kubernetes object kinds
On Podman 5.8.2, documented supported kinds include:
Pod
Deployment
PersistentVolumeClaim
ConfigMap
Secret
DaemonSet
JobSupport for a kind does not mean every field of that Kubernetes object is implemented. A valid Deployment YAML can still ignore scheduling fields, many pod security settings, and most probe types. The sections below separate kind support from field support.
Deployment does not behave like a Kubernetes controller
Podman accepts kind: Deployment, but there is no ReplicaSet controller behind it. On Podman 5.8.2, spec.replicas above one is capped locally.
Create deploy.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
spec:
replicas: 5
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: docker.io/library/nginx:latestPlay the manifest:
podman kube play deploy.yamlSample output:
time="2026-08-23T07:06:14+05:30" level=warning msg="Limiting replica count to 1, more than one replica is not supported by Podman"
Pod:
8b6d07eec22fd762028c46a74bd92bb2fad1abb081291fbac0a2524ac87b5236
Container:
66dc1ba3c1923b449f4854222db6d78e7129aa1a7e7a419a2e12915580c2ab42Count pods — you get one deployment pod, not five:
podman pod ps --filter name=nginx-deployPodman warns and continues. That is the core limitation for using kube play as cluster rehearsal: you are not exercising horizontal scaling or controller recovery.
Kubernetes fields Podman does not implement
Do not assume “it parsed, so it works.” Podman 5.8.2 documents a support matrix where many fields are explicitly unsupported.
Scheduling fields
These are not meaningful on a single-node Podman host:
nodeSelector
nodeName
affinity
tolerations
schedulerName
topologySpreadConstraintsPod fields
Unsupported examples include:
imagePullSecrets
serviceAccountName
automountServiceAccountToken
dnsPolicy
runtimeClassName
priorityClassName
fsGroupContainer fields
Unsupported examples include:
readinessProbe
startupProbe
lifecycle.postStart
lifecycle.preStop
volumeMounts.mountPropagation
subPathExprPodman 5.8.2 does support livenessProbe — do not write that all Kubernetes probes are ignored.
| Probe type | Podman 5.8.2 behavior |
|---|---|
livenessProbe |
Mapped to Podman healthcheck configuration |
readinessProbe |
Unsupported — silently ignored |
startupProbe |
Unsupported |
Silent unsupported-field behavior on Podman 5.8.2
Without --validate, unsupported YAML may disappear during translation instead of failing the play.
Create probe-readiness.yaml with a readiness probe:
apiVersion: v1
kind: Pod
metadata:
name: probe-readiness
spec:
containers:
- name: app
image: docker.io/library/nginx:latest
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5Play it:
podman kube play probe-readiness.yamlInspect the running container health configuration:
podman inspect probe-readiness-app --format 'Healthcheck={{json .Config.Healthcheck}}'Sample output:
Healthcheck=nullThe readiness probe never became Podman configuration. The pod still starts — which is exactly the trap when rehearsing manifests locally.
Compare with livenessProbe in probe-liveness.yaml:
apiVersion: v1
kind: Pod
metadata:
name: probe-liveness
spec:
containers:
- name: app
image: docker.io/library/nginx:latest
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 10After podman kube play probe-liveness.yaml, inspect again:
podman inspect probe-liveness-app --format 'Healthcheck={{json .Config.Healthcheck}}'Sample output:
Healthcheck={"Test":["CMD-SHELL","curl","-f","http://localhost:80/","||","exit","1"],"Interval":10000000000,"Timeout":1000000000,"Retries":3}Liveness translated into a Podman healthcheck. Always verify the runtime object — not just successful kube play output.
Newer Podman --validate behavior
Podman releases after 5.8.2 add validation modes:
podman kube play --validate=warn workload.yamlDocumented modes:
ignore → silently skip unsupported content
warn → continue but report unsupported fields
strict → fail when validation encounters unsupported contentOn Podman 5.8.2, the flag is absent:
podman kube play --validate=warn web.yamlSample output:
Error: unknown flag: --validate
See 'podman kube play --help'Plan staging tests on the Podman version you run in production. Newer Podman releases provide --validate; the default remains ignore, while --validate=warn reports unsupported content and --validate=strict rejects it.
ConfigMaps in YAML
Inline ConfigMap plus envFrom reference:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_MODE: production
LOG_LEVEL: info
---
apiVersion: v1
kind: Pod
metadata:
name: config-inline
spec:
containers:
- name: app
image: docker.io/library/nginx:latest
envFrom:
- configMapRef:
name: app-configPlay the manifest:
podman kube play config-inline.yamlConfirm the ConfigMap keys reached the container environment:
podman exec config-inline-app printenv APP_MODE LOG_LEVELSample output:
production
infoPodman does not leave a standalone Kubernetes control-plane ConfigMap object. It translates supported usage into container environment or volume configuration for the local pod.
External ConfigMap files with --configmap
Split the ConfigMap into its own file when you want reuse across manifests.
ext-configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
EXT_KEY: from-external-fileext-pod.yaml references app-config the same way as the inline example. Pass the external file explicitly:
podman kube play --configmap ext-configmap.yaml ext-pod.yamlMultiple external maps chain with repeated flags:
podman kube play --configmap one.yaml --configmap two.yaml workload.yamlVerify the injected value:
podman exec ext-config-pod-app printenv EXT_KEYSample output:
from-external-fileKubernetes Secrets
Use non-sensitive test data only in tutorials. dGVzdA== is base64 for test.
apiVersion: v1
kind: Secret
metadata:
name: db-secret
data:
password: dGVzdA==
---
apiVersion: v1
kind: Pod
metadata:
name: secret-pod
spec:
containers:
- name: app
image: docker.io/library/nginx:latest
env:
- name: SECRET_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: passwordPlay the manifest:
podman kube play secret-pod.yamlSample output:
Secrets:
3ec7d4b69981ac2d1dad91fef
Pod:
1c6002234e03d397110f9f4cbd2f0cd100f622cd4d1a400e6c4fad66ff7bbffe
Container:
6499a4ada707ec10624fc1e2bf63043123be28e09cf9f75bf7f7d6eeae8948f1Confirm the environment variable:
podman exec secret-pod-app printenv SECRET_PASSWORDSample output:
testKubernetes Secret support integrates with Podman secrets locally. Production secret handling belongs in Manage Podman secrets — this page only shows the kube play translation path.
Volumes: emptyDir, hostPath, and PVC
Supported volume types for local testing include:
hostPath
emptyDir
configMap
persistentVolumeClaim
imageMappings to remember:
PersistentVolumeClaim → Podman named volume
emptyDir → anonymous Podman volumePVC behavior is not dynamic provisioning. There is no StorageClass controller creating backend disks.
volumes-pod.yaml exercises emptyDir and hostPath:
apiVersion: v1
kind: Pod
metadata:
name: vol-test
spec:
containers:
- name: app
image: docker.io/library/nginx:latest
volumeMounts:
- name: scratch
mountPath: /empty
- name: hostdata
mountPath: /hostdata
volumes:
- name: scratch
emptyDir: {}
- name: hostdata
hostPath:
path: /tmp/kube-hostdata
type: DirectoryOrCreatePrepare the host path and play:
mkdir -p /tmp/kube-hostdata && echo host-marker > /tmp/kube-hostdata/marker.txtWith the host directory in place, create the pod:
podman kube play volumes-pod.yamlWrite into the emptyDir mount and list both paths:
podman exec vol-test-app sh -c 'touch /empty/empty-marker && ls -la /empty /hostdata'Sample output:
/empty:
total 0
drwxr-xr-x. 2 nginx nginx 26 Aug 23 01:39 .
dr-xr-xr-x. 1 root root 46 Aug 23 01:39 ..
-rw-r--r--. 1 nginx nginx 0 Aug 23 01:39 empty-marker
/hostdata:
total 4
drwxr-xr-x. 2 root root 24 Aug 23 01:38 .
dr-xr-xr-x. 1 root root 46 Aug 23 01:39 ..
-rw-r--r--. 1 root root 12 Aug 23 01:38 marker.txthostPath path rules
When hostPath.path contains /, Podman treats it as a host filesystem path — as with /tmp/kube-hostdata above. Paths without / may be interpreted as named volumes instead. Always use absolute host paths when you mean “bind this directory from the host.”
Image volumes are documented as rootful-only on Podman 5.8.2. SELinux labeling on RHEL can block host binds; see Fix Podman volume permissions when mounts look empty despite correct YAML.
PersistentVolumeClaim example
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: pvc-pod
spec:
containers:
- name: app
image: docker.io/library/nginx:latest
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: test-pvcCreate the pod and named volume:
podman kube play pvc-pod.yamlWrite through the mounted PVC path and read it back:
podman exec pvc-pod-app sh -c 'echo pvc-data > /data/pvc.txt && cat /data/pvc.txt'Sample output:
pvc-dataList the named volume Podman created:
podman volume ls | grep test-pvcSample output:
local test-pvcBuild missing images during kube play
Layout Podman expects when the image name matches a directory with a Containerfile:
project/
├── workload.yaml
└── local-web/
└── Containerfileworkload.yaml:
apiVersion: v1
kind: Pod
metadata:
name: local-build
spec:
containers:
- name: web
image: local-weblocal-web/Containerfile:
FROM docker.io/library/nginx:latest
USER root
RUN echo "local-build-marker" > /usr/share/nginx/html/build.txtUSER root is required here because the stock nginx image cannot write under /usr/share/nginx/html/ as the packaged user.
First play when the image does not exist yet — Podman builds from local-web/Containerfile even without --build:
podman kube play workload.yamlSample output (trimmed):
STEP 1/3: FROM docker.io/library/nginx:latest
STEP 2/3: USER root
STEP 3/3: RUN echo "local-build-marker" > /usr/share/nginx/html/build.txt
COMMIT local-web
Successfully tagged localhost/local-web:latest
Pod:
add175c09075060d1b794b19b6819549045aa4f190343cd766aa5d080f73cfd8Force a rebuild even when the tag already exists:
podman kube play --build workload.yamlSkip builds entirely when you know the image is present:
podman kube play --build=false workload.yamlImage build during kube play is not available through the remote client on the documented 5.8.x line — run plays on the host that owns the container store.
Verify the built content:
podman exec local-build-web cat /usr/share/nginx/html/build.txtSample output:
local-build-markerReplace a running YAML workload
--replace tears down resources from the previous play of the same manifest and recreates them. It is not a Kubernetes rolling update.
replace-demo.yaml starts with TEST_VAR=original:
apiVersion: v1
kind: Pod
metadata:
name: replace-demo
spec:
containers:
- name: app
image: docker.io/library/nginx:latest
env:
- name: TEST_VAR
value: originalPlay once, change the value to replaced in the file, then apply the update:
podman kube play --replace replace-demo.yamlRead the environment inside the new container:
podman exec replace-demo-app printenv TEST_VARSample output:
replacedExpect brief downtime — no gradual replica shift, no maxUnavailable budgeting.
Tear down with kube down
Stop and remove resources created from a manifest:
podman kube down web.yamlSample output:
Pods stopped:
bd078a431a90...
Pods removed:
bd078a431a90...
Secrets removed:
Volumes removed:The same teardown path exists as a flag on play (documented in the man page even when omitted from --help):
podman kube play --down web.yamlNormal down behavior may preserve named PVC data. Pass --force only when you intentionally want linked volume resources removed — that is a data-deletion decision, not a routine cleanup step.
Play YAML from stdin
Pipe generated or transformed manifests directly:
cat web.yaml | podman kube play -Useful when CI renders YAML and you do not want an intermediate file on disk.
Play YAML from a URL
Podman can fetch remote manifests:
podman kube play https://example.com/manifest.yamlTreat URL plays as one-shot experiments. If the remote file changes after creation, podman kube play --down against the URL may not map cleanly to objects created from an earlier revision. Version-controlled local YAML gives repeatable lifecycle management.
Configure user namespace mapping
Podman-specific annotation in metadata:
metadata:
annotations:
io.podman.annotations.userns: keep-idOr set the namespace on the command line (CLI overrides the annotation when both are present):
podman kube play --userns=keep-id workload.yamlWith --userns=keep-id, Podman maps the calling user's UID and GID to the same numeric IDs inside the container and runs the init process under that identity unless the manifest sets an explicit securityContext.runAsUser. On a rootless host, a user with UID 1000 typically sees uid=1000 gid=1000 inside the workload — not the image's default account such as nginx at UID 101.
Full user-namespace theory stays in dedicated guides — here the point is that kube play accepts Podman-specific annotations ordinary Kubernetes clusters would ignore.
CDI devices and GPU selectors
Podman 5.8.2 documents CDI device selectors in Kubernetes YAML for hosts with CDI configured (for example NVIDIA GPU CDI layouts). A conceptual fragment:
resources:
limits:
nvidia.com/gpu: 1This lab host did not exercise GPU hardware. Treat GPU sections as documented capability, not proof of Kubernetes GPU scheduling — there is no cluster device plugin reconciling capacity.
Run YAML as a systemd service with .kube Quadlet
Modern declarative systemd integration uses a Quadlet .kube unit. Example web.kube:
[Kube]
Yaml=/srv/web/workload.yaml
[Install]
WantedBy=multi-user.targetAfter placing the unit under /etc/containers/systemd/ or ~/.config/containers/systemd/, reload systemd:
sudo systemctl daemon-reloadStart the generated service unit:
sudo systemctl start web.serviceQuadlet generates a unit that invokes podman kube play and wires teardown through systemd. Unit layout, dependencies, and troubleshooting live in Podman Quadlet — this page only shows the Kubernetes YAML entry point.
podman-kube@.service template
RHEL 10.2 ships a template unit for path-escaped manifests. Check whether your package includes it:
systemctl list-unit-files 'podman-kube@*'Sample output:
UNIT FILE STATE PRESET
podman-kube@.service disabled disabled
1 unit files listed.Escape the manifest path for the template instance name:
escaped=$(systemd-escape /path/to/workload.yaml)Start the user-scoped template unit:
systemctl --user start "podman-kube@${escaped}.service"For new declarative content, Quadlet .kube units are the more natural modern approach. Verify the template exists on your distribution before documenting it in runbooks — not every Podman package ships the same systemd glue.
podman kube play vs a real Kubernetes cluster
| Topic | podman kube play |
Kubernetes cluster |
|---|---|---|
| Control plane | None — local Podman only | API server, scheduler, controllers |
| Orchestration scope | Single host | Multi-node scheduling |
| Deployment replicas | Capped locally (one pod on 5.8.2) | ReplicaSet maintains desired count |
| Unsupported YAML fields | Often ignored on 5.8.2 | Handled by respective controllers |
| Storage | Local named/anonymous volumes | Dynamic provisioning, StorageClass |
| Best use | Local compatibility rehearsal | Production orchestration |
kube play helps you test how much of a manifest translates on one machine. It does not replace a cluster for scaling, scheduling, or controller semantics.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Pod runs but probe logic never triggers | Unsupported probe type (for example readinessProbe) |
Inspect podman inspect --format '{{json .Config.Healthcheck}}'; test on a real cluster for probe behavior |
Limiting replica count to 1 warning |
Deployment replicas above one | Expected on Podman 5.8.2 — not a bug |
| Host port curl fails while container serves traffic | Port publish vs image listen port mismatch | podman port; test via podman exec or pod IP |
| Build did not run | Remote client or --build=false |
Play on local host; add --build to force |
PVC data survives kube down |
Default down preserves named volumes | Use --force only when intentional data removal is acceptable |
unknown flag: --validate |
Older Podman | Upgrade or rely on manual inspection on 5.8.2 |
References
- podman-kube-play(1) — supported kinds, flags, and teardown
- Kubernetes object specifications — upstream field definitions Podman partially implements
- Red Hat — Building, running, and managing containers —
kube playon RHEL-family systems - Podman Quadlet documentation —
.kubeunit files
Summary
podman kube play turns supported Kubernetes YAML into local Podman pods, containers, volumes, and secrets. It does not start a cluster — no scheduler, no kubelet, and no Deployment controller maintaining replicas. On Podman 5.8.2, a replicas: 5 Deployment warns and creates one pod.
Field support is the real compatibility boundary. livenessProbe maps to Podman healthchecks; readinessProbe can vanish silently because --validate is not available on 5.8.2. ConfigMaps, Secrets, emptyDir, hostPath, and PVCs translated correctly in the lab exercises, but StorageClass semantics and scheduling fields do not.
Use --build for local image directories, --replace for local recreation (not rolling updates), and podman kube down for teardown — mind PVC data before adding --force. For boot-time service management, prefer Quadlet .kube units over ad hoc scripts. When rehearsal ends, apply the same YAML to a real API server and expect controllers to enforce semantics kube play never simulated.
Generating YAML from running containers is the reverse path — see Generate Kubernetes YAML from Podman containers for export, round-trip fidelity, and when to use SpecGen JSON instead.

