Kubernetes Pod Manifest
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
| Value | Behaviour |
|---|---|
IfNotPresent | Pull only if not cached (the default for pinned tags) |
Always | Pull every time: the default when the tag is latest or absent |
Never | Never 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
| Task | Command |
|---|---|
| Generate a Pod manifest | kubectl run <n> --image=<i> --dry-run=client -o yaml |
| Document a field | kubectl explain pod.spec.<field> |
| Validate | kubectl apply -f pod.yaml --dry-run=server |
| The live object | kubectl get pod <n> -o yaml |
| Apply | kubectl apply -f pod.yaml |
| Available kinds | kubectl api-resources |
Key takeaways
- Every object has
apiVersion,kind,metadataandspec, andstatusis 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.
containerPortpublishes nothing. It is documentation, and naming ports makes probes and Services more robust.runAsNonRoot: trueturns a silent risk into a visible startup failure.- Pods are effectively immutable: only the image can be changed. Use a Deployment.
kubectl explainmatches your cluster's version, unlike search results.
Check your understanding
What containerPort does
A Pod declares containerPort: 8080 but the application actually listens on 3000. What happens?
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.