Run Kubernetes YAML Locally with `podman kube play`

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

text
Kubernetes-style YAML
podman kube play
local Podman objects
       ├── pods
       ├── containers
       ├── volumes
       ├── secrets
       └── supporting config

Podman 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:

bash
podman kube play workload.yaml

The older grouping still works:

bash
podman play kube workload.yaml

podman 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:

bash
mkdir -p ~/podman-kube-play-lab && cd ~/podman-kube-play-lab

Confirm the subcommand and supported kinds on your build:

bash
podman kube play --help

Sample output (trimmed):

output
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 use

On 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:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
    - name: web
      image: docker.io/library/nginx:latest
      ports:
        - containerPort: 80
          hostPort: 8080

Create the pod and container from that file:

bash
podman kube play web.yaml

Sample output:

output
Pod:
5004356ec003e166bb20dc4ae85f26b0c4ba30911a114f720b5afc6d8f4b70e5
Container:
def99db10fd9476b49180d5dd1dfa24e83c9dc2d8d5a682e1712a6f39763a4aa

The command prints Pod and container IDs. List pods to see the infra container Podman adds for networking:

bash
podman pod ps

Sample output:

output
POD ID        NAME        STATUS      CREATED        INFRA ID      # OF CONTAINERS
5004356ec003  web         Running     47 seconds ago  50bdd8e53d47  2

List containers with their parent pod:

bash
podman ps --pod

Sample output:

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  web

Confirm nginx responds inside the workload container:

bash
podman exec web-web curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:80

Sample output:

output
200

Host 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:

text
Pod
Deployment
PersistentVolumeClaim
ConfigMap
Secret
DaemonSet
Job

Support 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:

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:latest

Play the manifest:

bash
podman kube play deploy.yaml

Sample output:

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:
66dc1ba3c1923b449f4854222db6d78e7129aa1a7e7a419a2e12915580c2ab42

Count pods — you get one deployment pod, not five:

bash
podman pod ps --filter name=nginx-deploy

Podman 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:

text
nodeSelector
nodeName
affinity
tolerations
schedulerName
topologySpreadConstraints

Pod fields

Unsupported examples include:

text
imagePullSecrets
serviceAccountName
automountServiceAccountToken
dnsPolicy
runtimeClassName
priorityClassName
fsGroup

Container fields

Unsupported examples include:

text
readinessProbe
startupProbe
lifecycle.postStart
lifecycle.preStop
volumeMounts.mountPropagation
subPathExpr

Podman 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:

yaml
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: 5

Play it:

bash
podman kube play probe-readiness.yaml

Inspect the running container health configuration:

bash
podman inspect probe-readiness-app --format 'Healthcheck={{json .Config.Healthcheck}}'

Sample output:

output
Healthcheck=null

The 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:

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: 10

After podman kube play probe-liveness.yaml, inspect again:

bash
podman inspect probe-liveness-app --format 'Healthcheck={{json .Config.Healthcheck}}'

Sample output:

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:

bash
podman kube play --validate=warn workload.yaml

Documented modes:

text
ignore   → silently skip unsupported content
warn     → continue but report unsupported fields
strict   → fail when validation encounters unsupported content

On Podman 5.8.2, the flag is absent:

bash
podman kube play --validate=warn web.yaml

Sample output:

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:

yaml
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-config

Play the manifest:

bash
podman kube play config-inline.yaml

Confirm the ConfigMap keys reached the container environment:

bash
podman exec config-inline-app printenv APP_MODE LOG_LEVEL

Sample output:

output
production
info

Podman 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:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  EXT_KEY: from-external-file

ext-pod.yaml references app-config the same way as the inline example. Pass the external file explicitly:

bash
podman kube play --configmap ext-configmap.yaml ext-pod.yaml

Multiple external maps chain with repeated flags:

bash
podman kube play --configmap one.yaml --configmap two.yaml workload.yaml

Verify the injected value:

bash
podman exec ext-config-pod-app printenv EXT_KEY

Sample output:

output
from-external-file

Kubernetes Secrets

Use non-sensitive test data only in tutorials. dGVzdA== is base64 for test.

yaml
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: password

Play the manifest:

bash
podman kube play secret-pod.yaml

Sample output:

output
Secrets:
3ec7d4b69981ac2d1dad91fef
Pod:
1c6002234e03d397110f9f4cbd2f0cd100f622cd4d1a400e6c4fad66ff7bbffe
Container:
6499a4ada707ec10624fc1e2bf63043123be28e09cf9f75bf7f7d6eeae8948f1

Confirm the environment variable:

bash
podman exec secret-pod-app printenv SECRET_PASSWORD

Sample output:

output
test

Kubernetes 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:

text
hostPath
emptyDir
configMap
persistentVolumeClaim
image

Mappings to remember:

text
PersistentVolumeClaim  → Podman named volume
emptyDir                 → anonymous Podman volume

PVC behavior is not dynamic provisioning. There is no StorageClass controller creating backend disks.

volumes-pod.yaml exercises emptyDir and hostPath:

yaml
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: DirectoryOrCreate

Prepare the host path and play:

bash
mkdir -p /tmp/kube-hostdata && echo host-marker > /tmp/kube-hostdata/marker.txt

With the host directory in place, create the pod:

bash
podman kube play volumes-pod.yaml

Write into the emptyDir mount and list both paths:

bash
podman exec vol-test-app sh -c 'touch /empty/empty-marker && ls -la /empty /hostdata'

Sample output:

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.txt

hostPath 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

yaml
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-pvc

Create the pod and named volume:

bash
podman kube play pvc-pod.yaml

Write through the mounted PVC path and read it back:

bash
podman exec pvc-pod-app sh -c 'echo pvc-data > /data/pvc.txt && cat /data/pvc.txt'

Sample output:

output
pvc-data

List the named volume Podman created:

bash
podman volume ls | grep test-pvc

Sample output:

output
local       test-pvc

Build missing images during kube play

Layout Podman expects when the image name matches a directory with a Containerfile:

text
project/
├── workload.yaml
└── local-web/
    └── Containerfile

workload.yaml:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: local-build
spec:
  containers:
    - name: web
      image: local-web

local-web/Containerfile:

dockerfile
FROM docker.io/library/nginx:latest
USER root
RUN echo "local-build-marker" > /usr/share/nginx/html/build.txt

USER 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:

bash
podman kube play workload.yaml

Sample output (trimmed):

output
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:
add175c09075060d1b794b19b6819549045aa4f190343cd766aa5d080f73cfd8

Force a rebuild even when the tag already exists:

bash
podman kube play --build workload.yaml

Skip builds entirely when you know the image is present:

bash
podman kube play --build=false workload.yaml

Image 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:

bash
podman exec local-build-web cat /usr/share/nginx/html/build.txt

Sample output:

output
local-build-marker

Replace 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:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: replace-demo
spec:
  containers:
    - name: app
      image: docker.io/library/nginx:latest
      env:
        - name: TEST_VAR
          value: original

Play once, change the value to replaced in the file, then apply the update:

bash
podman kube play --replace replace-demo.yaml

Read the environment inside the new container:

bash
podman exec replace-demo-app printenv TEST_VAR

Sample output:

output
replaced

Expect brief downtime — no gradual replica shift, no maxUnavailable budgeting.


Tear down with kube down

Stop and remove resources created from a manifest:

bash
podman kube down web.yaml

Sample output:

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):

bash
podman kube play --down web.yaml

Normal 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:

bash
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:

bash
podman kube play https://example.com/manifest.yaml

Treat 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:

yaml
metadata:
  annotations:
    io.podman.annotations.userns: keep-id

Or set the namespace on the command line (CLI overrides the annotation when both are present):

bash
podman kube play --userns=keep-id workload.yaml

With --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:

yaml
resources:
  limits:
    nvidia.com/gpu: 1

This 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:

ini
[Kube]
Yaml=/srv/web/workload.yaml

[Install]
WantedBy=multi-user.target

After placing the unit under /etc/containers/systemd/ or ~/.config/containers/systemd/, reload systemd:

bash
sudo systemctl daemon-reload

Start the generated service unit:

bash
sudo systemctl start web.service

Quadlet 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:

bash
systemctl list-unit-files 'podman-kube@*'

Sample output:

output
UNIT FILE            STATE    PRESET
podman-kube@.service disabled disabled

1 unit files listed.

Escape the manifest path for the template instance name:

bash
escaped=$(systemd-escape /path/to/workload.yaml)

Start the user-scoped template unit:

bash
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


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.


Frequently Asked Questions

1. Does podman kube play start a Kubernetes cluster?

No. Podman interprets supported Kubernetes YAML on a single local host and creates Podman pods, containers, volumes, and secrets. There is no scheduler, kubelet, or controller reconciliation loop.

2. What is the difference between podman kube play and podman play kube?

They are the same command. podman play kube is an officially supported alias. This guide uses podman kube play for consistency with current Podman command grouping.

3. Why does my Deployment with replicas 5 only create one pod?

Podman kube play does not run a Kubernetes Deployment controller. On Podman 5.8.2, replica counts above one are limited to a single local pod instance. It is not equivalent to cluster ReplicaSet behavior.

4. Will unsupported Kubernetes fields cause podman kube play to fail on Podman 5.8.2?

Not necessarily. Without the newer --validate flag, unsupported fields such as readinessProbe may be ignored silently. Always inspect the resulting containers instead of assuming YAML semantics carried through.

5. How do I tear down resources created by podman kube play?

Use podman kube down workload.yaml or podman kube play --down workload.yaml. Named PVC data may survive a normal down unless you pass --force to remove linked volumes intentionally.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)