Kubernetes Pod Manifest

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

Read and write Pod YAML. Required fields, containers, ports, environment, volumes, securityContext, and the fields that are immutable once created.

Every Kubernetes object shares the same four top-level fields, and a Pod is the clearest place to learn them because everything else embeds a Pod spec inside it.

A Deployment, StatefulSet, DaemonSet, Job and CronJob all contain spec.template.spec, which is a Pod spec. Learn it once.

The four top-level fields

apiVersion: v1          # which API group and version
kind: Pod               # what kind of object
metadata:               # name, namespace, labels, annotations
  name: payments-api
spec:                   # the desired state. This is the interesting part
  containers:
    - name: api
      image: ghcr.io/acme/payments-api:1.4.2

apiVersion differs by object: v1 for core types like Pod, Service and ConfigMap, apps/v1 for Deployment and StatefulSet, batch/v1 for Job. Getting it wrong gives no matches for kind.

There is a fifth field, status, which the control plane writes and you never set.

Metadata

metadata:
  name: payments-api
  namespace: payments
  labels:
    app.kubernetes.io/name: payments-api
    app.kubernetes.io/version: "1.4.2"
    app.kubernetes.io/component: api
  annotations:
    kubernetes.io/change-cause: "Deploy 1.4.2 (PR #455)"

Labels are for selection; annotations are for information. Services, Deployments and NetworkPolicies all find Pods by label, so labels must be structured and consistent. Annotations hold anything else, build metadata, controller configuration, tool-specific settings, and cannot be selected on.

Name rules catch people: lowercase alphanumerics and hyphens only, at most 253 characters, and no underscores. A name that works fine as a Docker container name may be rejected here.

The container spec

spec:
  containers:
    - name: api
      image: ghcr.io/acme/payments-api:1.4.2
      imagePullPolicy: IfNotPresent

      ports:
        - name: http
          containerPort: 8080
          protocol: TCP

      env:
        - name: LOG_LEVEL
          value: info
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: payments-db
              key: password
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP

      resources:
        requests:
          cpu: 100m
          memory: 128Mi
        limits:
          memory: 512Mi

      volumeMounts:
        - name: config
          mountPath: /etc/app
          readOnly: true

  volumes:
    - name: config
      configMap:
        name: payments-config

ports is documentation

containerPort does not publish or open anything. The container listens on whatever port its process binds; this field records that for humans and lets a Service reference the port by name.

Naming ports is worth doing. A Service or probe can then use targetPort: http, which survives a port number change.

imagePullPolicy

ValueBehaviour
IfNotPresentPull only if not cached (the default for pinned tags)
AlwaysPull every time: the default when the tag is latest or absent
NeverNever pull; the image must already be on the node

The defaulting behaviour around latest catches people. Pin your tags and this stops mattering.

env and the Downward API

fieldRef exposes Pod metadata to the container without hardcoding it. Useful for logging and telemetry:

        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName

securityContext

Worth setting deliberately, because the defaults are permissive:

spec:
  securityContext:              # Pod level. Applies to all containers
    runAsNonRoot: true
    runAsUser: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: api
      securityContext:          # container level. Overrides the Pod level
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]

runAsNonRoot: true makes Kubernetes refuse to start a container whose image runs as root, which turns a silent security weakness into a visible startup failure. readOnlyRootFilesystem: true means anything needing to write requires an explicit emptyDir mount. More work initially, and it removes a large category of runtime tampering.

fsGroup sets group ownership on mounted volumes, which is how a non-root process gets write access to a PersistentVolume.

Full example

apiVersion: v1
kind: Pod
metadata:
  name: payments-api
  labels:
    app: payments-api
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    fsGroup: 10001
  containers:
    - name: api
      image: ghcr.io/acme/payments-api:1.4.2
      ports:
        - name: http
          containerPort: 8080
      env:
        - name: LOG_LEVEL
          value: info
      resources:
        requests:
          cpu: 100m
          memory: 128Mi
        limits:
          memory: 512Mi
      readinessProbe:
        httpGet:
          path: /healthz
          port: http
        initialDelaySeconds: 5
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}
  restartPolicy: Always

Note port: http in the probe, referencing the named port, and the /tmp emptyDir that a read-only root filesystem makes necessary.

Pods are largely immutable

Once created, almost nothing in a Pod spec can change. You may update the image, and little else. Changing environment variables, resources or volumes requires deleting and recreating the Pod.

The Pod "payments-api" is invalid: spec: Forbidden:
pod updates may not change fields other than `spec.containers[*].image` ...

This is why you use a Deployment. Change its template and the Deployment creates new Pods for you, gradually, with a rollback available.

Finding fields without searching

kubectl explain pod.spec
kubectl explain pod.spec.containers.resources
kubectl explain pod.spec.securityContext --recursive

kubectl explain documents fields against the API server you are actually connected to, so it matches your Kubernetes version rather than whatever version a search result described.

# Generate a skeleton rather than typing from memory
kubectl run payments-api --image=ghcr.io/acme/payments-api:1.4.2 \
  --dry-run=client -o yaml > pod.yaml

# Validate without applying
kubectl apply -f pod.yaml --dry-run=server

Quick reference

TaskCommand
Generate a Pod manifestkubectl run <n> --image=<i> --dry-run=client -o yaml
Document a fieldkubectl explain pod.spec.<field>
Validatekubectl apply -f pod.yaml --dry-run=server
The live objectkubectl get pod <n> -o yaml
Applykubectl apply -f pod.yaml
Available kindskubectl api-resources

Key takeaways

  • Every object has apiVersion, kind, metadata and spec, and status is written by the cluster.
  • A Pod spec is embedded in every workload controller: learning it once covers all of them.
  • Labels are selectable; annotations are not. Services and controllers find Pods by label.
  • containerPort publishes nothing. It is documentation, and naming ports makes probes and Services more robust.
  • runAsNonRoot: true turns a silent risk into a visible startup failure.
  • Pods are effectively immutable: only the image can be changed. Use a Deployment.
  • kubectl explain matches your cluster's version, unlike search results.

Check your understanding

Exercise

What containerPort does

A Pod declares containerPort: 8080 but the application actually listens on 3000. What happens?

Exercise

Editing a running Pod

You edit a standalone Pod to change an environment variable and apply it. The API server rejects the change. Why?


Frequently Asked Questions

What are the required fields in a Pod manifest?

apiVersion: v1, kind: Pod, a metadata.name, and a spec.containers list where each container has a name and an image. Everything else is optional, though a manifest with only those is unfit for production. Resource requests, a readiness probe and a securityContext should all be considered part of the minimum in practice. kubectl explain pod.spec documents every available field against your cluster's own API version.

What is the difference between labels and annotations?

Labels are indexed key-value pairs used for selection: Services find Pods by label, Deployments identify the Pods they own by label, and NetworkPolicies choose targets by label. Annotations hold arbitrary metadata that nothing selects on, build information, tool configuration, controller hints, and can be far larger. If something needs to find the object, it must be a label; if it only needs to know something, it belongs in an annotation.

Does containerPort actually expose the port?

No. It is informational. The container listens on whatever port its process binds, and Kubernetes neither enforces nor alters that. Declaring containerPort documents intent and, when named, lets a Service's targetPort or a probe's port reference it by name rather than number, which is more readable and survives a port change. A mismatch between the declaration and reality produces no error, only a Service or probe that cannot connect.

Why can't I edit a running Pod?

Pods are almost entirely immutable once created, because the scheduler has already placed them and reserved resources on a specific node based on their spec. Only spec.containers[*].image may be changed. To alter anything else you delete and recreate the Pod, which is precisely why real workloads use a Deployment, where changing the template creates replacement Pods gradually and keeps the previous version available for rollback.

What should I put in securityContext?

At Pod level, runAsNonRoot: true with an explicit runAsUser, an fsGroup so mounted volumes are writable by that user, and seccompProfile: RuntimeDefault. At container level, allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, and capabilities: drop: ["ALL"]. runAsNonRoot is the highest-value single setting because it converts an image that silently runs as root into a visible startup failure. A read-only root filesystem usually requires adding an emptyDir at /tmp.

How do I find out what fields are available?

kubectl explain pod.spec and kubectl explain pod.spec.containers.resources document fields directly from the API server you are connected to, so the output always matches your cluster's version rather than whatever a search result describes. Add --recursive to see a whole subtree at once. To get a working starting point, kubectl run name --image=x --dry-run=client -o yaml generates a valid manifest without contacting the cluster.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Kubernetes Playground

Practise kubectl against a simulated cluster in your browser.

When it breaks

Related Resources