Workloads
Pods, Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs - the right workload type for the right job
Workloads
| Resource | What it manages | Use when |
|---|---|---|
| Pod | One or more containers sharing network/storage | Almost never directly — use a controller |
| Deployment | Stateless replicas with rolling updates | Web/API servers, workers |
| StatefulSet | Replicas with stable identity and storage | Databases, queues, anything needing stable DNS |
| DaemonSet | One pod per node | Log shippers, monitoring agents, CNI |
| Job | Run to completion | One-off batch tasks, migrations |
| CronJob | Scheduled Jobs | Backups, periodic cleanup |
| ReplicaSet | Internal — created by Deployment | Don't write directly |
Pods (the foundation)
A pod is a group of co-located containers. They share an IP, can talk over localhost, and live or die together. You almost never create a pod directly — controllers like Deployment do it for you. The schema is still worth knowing:
apiVersion: v1
kind: Pod
metadata:
name: hello
spec:
containers:
- name: app
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "500m", memory: "256Mi" }
readinessProbe:
httpGet: { path: /, port: 80 }
periodSeconds: 10
livenessProbe:
httpGet: { path: /, port: 80 }
periodSeconds: 20Two pieces every container should have:
- Resource
requests/limits— requests reserve capacity for scheduling; limits cap actual usage. Without them, a runaway pod can starve neighbors. readinessProbe/livenessProbe— readiness gates traffic ("ready to serve?"); liveness restarts the container if it deadlocks ("still alive?").
Deployment
The default for stateless services. Manages a ReplicaSet which manages pods, and handles rolling updates and rollbacks.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-server
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # at most 1 extra pod during update
maxUnavailable: 0 # never go below replicas
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myregistry/api-server:v1.2.3
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef: { name: db-credentials, key: url }
resources:
requests: { cpu: "250m", memory: "256Mi" }
limits: { cpu: "1000m", memory: "512Mi" }
readinessProbe:
httpGet: { path: /health, port: 3000 }
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: { path: /health, port: 3000 }
initialDelaySeconds: 15
periodSeconds: 20
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- { key: app, operator: In, values: [api-server] }
topologyKey: "kubernetes.io/hostname"The podAntiAffinity block tells the scheduler to spread replicas across nodes, so a single node failure doesn't take down the service.
StatefulSet
For workloads where each pod needs a stable name and its own persistent storage — databases, queues, anything where pod-1 ≠ pod-2.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres # headless Service for DNS
replicas: 3
selector:
matchLabels: { app: postgres }
template:
metadata:
labels: { app: postgres }
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports: [{ containerPort: 5432 }]
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef: { name: postgres-secret, key: password }
volumeMounts:
- { name: pgdata, mountPath: /var/lib/postgresql/data }
volumeClaimTemplates:
- metadata: { name: pgdata }
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests: { storage: 50Gi }Pods get stable names (postgres-0, postgres-1, postgres-2) and each gets its own PersistentVolumeClaim (pgdata-postgres-0, ...). DNS resolves postgres-0.postgres to a stable network identity.
StatefulSets are not a turnkey database. They give you stable pods and storage; replication, backups, and failover are your problem. Use a database operator (Zalando Postgres Operator, MongoDB Operator, ...) or a managed DB instead of rolling your own.
DaemonSet
One pod per node — for things every node should run.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels: { app: node-exporter }
template:
metadata:
labels: { app: node-exporter }
spec:
hostNetwork: true
hostPID: true
containers:
- name: node-exporter
image: prom/node-exporter:v1.7.0
ports: [{ containerPort: 9100, hostPort: 9100 }]
volumeMounts:
- { name: proc, mountPath: /host/proc, readOnly: true }
- { name: sys, mountPath: /host/sys, readOnly: true }
volumes:
- { name: proc, hostPath: { path: /proc } }
- { name: sys, hostPath: { path: /sys } }Typical DaemonSets: log shipper (Fluent Bit), metrics exporter (Node Exporter), CNI agent, storage driver.
Job
Run-to-completion workloads. A Job creates pods that run until they exit successfully.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
backoffLimit: 3 # retry 3 times on failure
completions: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: migrate
image: myregistry/migrator:v1.2.3
command: ["./migrate", "up"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef: { name: db-credentials, key: url }Run it once per release:
kubectl apply -f migration-job.yaml
kubectl logs -f job/db-migration
kubectl delete job/db-migration # clean up after successFor parallel processing of a work queue, set parallelism and completions together.
CronJob
Scheduled Jobs.
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: "0 2 * * *" # daily at 02:00
concurrencyPolicy: Forbid # don't start a new one if the last is still running
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: myregistry/db-backup:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef: { name: db-credentials, key: url }concurrencyPolicy options:
| Value | Behavior |
|---|---|
Allow (default) | Overlapping runs are fine |
Forbid | Skip the new run if the previous hasn't finished |
Replace | Kill the previous run and start the new one |
Choosing the Right Workload
Quick decision tree:
Need to run a service?
├── Stateless? → Deployment
└── Stateful (own storage, stable name)? → StatefulSet
Need to run on every node? → DaemonSet
Run-to-completion?
├── One-shot? → Job
└── On a schedule? → CronJobWhat's Next
You've got the right tool for every kind of workload. Now make them reachable — internally, externally, and from each other → Networking.