Kubernetes Deployment vs StatefulSet vs DaemonSet: Complete Comparison with Examples

kubernetes deployment vs statefulset vs daemonset complete comparison

Table of Contents

  1. The Core Problem: Not All Workloads Are the Same
  2. Quick Decision Guide
  3. Kubernetes Deployment: Stateless Workloads
  4. Deployment YAML Example and Rolling Updates
  5. Deployment Scaling and Rollback
  6. Kubernetes StatefulSet: Stateful Workloads
  7. StatefulSet YAML Example with Persistent Storage
  8. StatefulSet Headless Service and DNS
  9. StatefulSet Ordered Startup and Shutdown
  10. Kubernetes DaemonSet: Node-Level Workloads
  11. DaemonSet YAML Example with Node Selector
  12. DaemonSet Updates and Tolerations
  13. Side-by-Side Comparison Table
  14. Real-World Architecture: Using All Three Together
  15. Common Mistakes and How to Avoid Them
  16. Frequently Asked Questions
  17. Next Steps

Three teams, three applications, three different problems. Team A builds an API — it needs to scale to handle traffic spikes, and any instance is identical to any other. Team B runs a database — it must remember which data belongs to which instance even after a restart. Team C deploys a log collector — it needs exactly one agent running on every node in the cluster, always.

Kubernetes has a dedicated workload controller for each scenario: Deployment, StatefulSet, and DaemonSet. Choosing the wrong one doesn’t always fail immediately — it fails quietly, in ways that are painful to debug later. This guide explains how each one works, with complete YAML examples and the reasoning behind every key decision.

The Core Problem: Not All Workloads Are the Same

StatefulSets are used to run stateful applications in Kubernetes, whereas Deployments support declarative updates for stateless Pods. DaemonSets are a third kind of Pod controller that ensures that all or some of the Nodes in your cluster are always running a replica of a Pod.

The fundamental distinction is between stateless, stateful, and node-level workloads:

Stateless workload (Deployment):
  Pod A = Pod B = Pod C = identical, interchangeable
  Any pod can handle any request
  Pods have no persistent identity
  Scale up by adding identical copies

Stateful workload (StatefulSet):
  Pod-0 ≠ Pod-1 ≠ Pod-2 — each has unique identity
  Pod-0 is always the primary DB replica
  Pod-1 always owns its specific data volume
  Pod-0 must start before Pod-1, which starts before Pod-2

Node-level workload (DaemonSet):
  Node 1 → exactly one pod
  Node 2 → exactly one pod
  Node 3 → exactly one pod
  New node added → pod auto-scheduled immediately

Quick Decision Guide

Before the deep dive, a fast decision tree:

Does your app store data that must survive pod restarts?
  YES → Does each replica need its own unique data?
          YES → StatefulSet
          NO  → Deployment + PersistentVolumeClaim
  NO  → Does it need to run on EVERY node?
          YES → DaemonSet
          NO  → Deployment

In practice:

WorkloadController
Web frontend, REST API, microserviceDeployment
MySQL, PostgreSQL, MongoDB, Redis clusterStatefulSet
Elasticsearch, Kafka, ZookeeperStatefulSet
Log collector (Fluentd, Filebeat, Alloy)DaemonSet
Metrics collector (Node Exporter)DaemonSet
Network plugin (Calico, Cilium, Flannel)DaemonSet
Security scanner (Falco)DaemonSet
Batch job, ML training (one-time)Job / CronJob

Kubernetes Deployment: Stateless Workloads

In a Kubernetes environment, frontend Pods are like “disposable actors” in a play — if one crashes, another steps in immediately, and nobody notices. That’s the Deployment controller in Kubernetes.

A Deployment manages a ReplicaSet — a set of identical, interchangeable pod replicas. The Deployment controller continuously reconciles the actual state with the desired state: if you ask for 5 replicas and one crashes, it immediately creates a replacement.

What makes a workload suitable for Deployment:

  • Any instance can handle any request (no session affinity to data)
  • Pods can start and stop in any order without affecting correctness
  • Scaling means adding more identical copies
  • No data needs to survive beyond the pod’s lifecycle

Core features:

  • Rolling updates — replace pods one at a time with zero downtime
  • Rollback — instantly revert to a previous version with one command
  • Scaling — add or remove replicas with a single kubectl command
  • Health checks — liveness and readiness probes gate traffic and replacements

Deployment YAML Example and Rolling Updates

# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-api
  namespace: production
  labels:
    app: web-api
    version: "1.2.3"
spec:
  replicas: 3

  selector:
    matchLabels:
      app: web-api

  # Rolling update strategy — the default
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # Allow 1 extra pod during update (4 pods max)
      maxUnavailable: 0    # Never go below 3 running pods during update

  template:
    metadata:
      labels:
        app: web-api
        version: "1.2.3"
    spec:
      # Security: never run as root
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000

      containers:
        - name: web-api
          image: myregistry.com/web-api:1.2.3    # Always pin — never :latest
          ports:
            - containerPort: 8080

          # Resource limits — always required in production
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"

          # Liveness probe — restart if unhealthy
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3

          # Readiness probe — only send traffic when ready
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5

          # Environment from ConfigMap and Secret
          envFrom:
            - configMapRef:
                name: web-api-config
          env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password

      # Spread pods across nodes for high availability
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: web-api

Expose the Deployment with a Service:

# web-api-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-api
  namespace: production
spec:
  selector:
    app: web-api        # Routes to any pod with this label
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP       # Internal only — expose via Ingress for external access

Perform a rolling update:

# Update the image — triggers a rolling update automatically
kubectl set image deployment/web-api \
  web-api=myregistry.com/web-api:1.2.4 \
  -n production

# Watch the rollout in real time
kubectl rollout status deployment/web-api -n production
# Waiting for deployment "web-api" rollout to finish: 1 out of 3 new replicas updated...
# Waiting for deployment "web-api" rollout to finish: 2 out of 3 new replicas updated...
# Waiting for deployment "web-api" rollout to finish: 1 old replicas are pending termination...
# deployment "web-api" successfully rolled out

Deployment Scaling and Rollback

# Scale up for a traffic spike
kubectl scale deployment/web-api --replicas=10 -n production

# Scale back down
kubectl scale deployment/web-api --replicas=3 -n production

# View rollout history
kubectl rollout history deployment/web-api -n production
# REVISION  CHANGE-CAUSE
# 1         Initial deployment v1.2.3
# 2         Updated to v1.2.4

# Rollback to previous version (if v1.2.4 has a bug)
kubectl rollout undo deployment/web-api -n production

# Rollback to a specific revision
kubectl rollout undo deployment/web-api --to-revision=1 -n production

The Recreate strategy (alternative to RollingUpdate):

strategy:
  type: Recreate    # Kill ALL old pods first, then create new ones

Use Recreate when your application cannot run two versions simultaneously — for example, if it holds a database lock that only one version understands. The tradeoff: brief downtime during the update.

Kubernetes StatefulSet: Stateful Workloads

StatefulSets are a great choice for assigning unique resources to containers and maintaining application state. Use cases for StatefulSets typically involve stateful apps that need access to persistent storage resources. StatefulSets let a container access the same data storage volume even if the container restarts or moves to another node. With a standard deployment, this wouldn’t be possible.

A StatefulSet provides three guarantees that a Deployment cannot:

1. Stable, unique network identity:
Each pod gets a predictable, permanent name: db-0, db-1, db-2. Even after a pod restarts, it comes back with the same name and DNS entry — not a random suffix like Deployment pods (db-7d9f4b-x8k2p).

2. Stable, persistent storage:
Each pod gets its own PersistentVolumeClaim, and that PVC is reattached to the same pod when it restarts — regardless of which physical node it lands on.

3. Ordered, graceful deployment and scaling:
Pods start in order (db-0 first, then db-1, then db-2) and terminate in reverse order (db-2 first). No pod starts until all previous pods are Running and Ready.

Why databases need StatefulSets:

A database cluster (PostgreSQL, MongoDB, Redis Sentinel) has a primary node that accepts writes and secondaries that replicate from it. If pods had random names and random storage assignments:

  • The primary might get db-7d9f4b-x8k2p one restart and db-7d9f4b-r3n9q the next
  • Secondaries wouldn’t know which pod to replicate from
  • Volumes would be assigned to the wrong pods, corrupting data

StatefulSets solve all of this with stable identities.

StatefulSet YAML Example with Persistent Storage

# mongodb-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
  namespace: production
spec:
  serviceName: "mongodb-headless"    # Must reference the headless service below
  replicas: 3
  selector:
    matchLabels:
      app: mongodb

  template:
    metadata:
      labels:
        app: mongodb
    spec:
      # Terminate in order: pod-2 before pod-1 before pod-0
      terminationGracePeriodSeconds: 60

      containers:
        - name: mongodb
          image: mongo:7.0
          ports:
            - containerPort: 27017
              name: mongodb

          command:
            - mongod
            - "--replSet"
            - "rs0"
            - "--bind_ip_all"
            - "--keyFile"
            - "/etc/mongodb-keyfile"

          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "4Gi"

          # Mount persistent volume at /data/db
          volumeMounts:
            - name: mongodb-data
              mountPath: /data/db
            - name: mongodb-keyfile
              mountPath: /etc/mongodb-keyfile
              subPath: keyfile
              readOnly: true

          livenessProbe:
            exec:
              command:
                - mongosh
                - "--eval"
                - "db.adminCommand('ping')"
            initialDelaySeconds: 30
            periodSeconds: 10

      volumes:
        - name: mongodb-keyfile
          secret:
            secretName: mongodb-keyfile
            defaultMode: 0400    # Keyfile must be 400 — MongoDB rejects anything looser

  # VolumeClaimTemplates: each pod gets its OWN PVC
  # mongodb-data-mongodb-0, mongodb-data-mongodb-1, mongodb-data-mongodb-2
  volumeClaimTemplates:
    - metadata:
        name: mongodb-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: "fast-ssd"
        resources:
          requests:
            storage: 50Gi

The volumeClaimTemplates section is what makes StatefulSets unique — it creates a separate PersistentVolumeClaim for each pod automatically. When mongodb-0 restarts, it gets mongodb-data-mongodb-0 — always. When mongodb-1 restarts, it gets mongodb-data-mongodb-1 — always. The data never gets mixed up.

For a complete MongoDB setup with Docker Compose (non-Kubernetes), see our MongoDB with Docker Compose guide — the replica set concepts are identical.

StatefulSet Headless Service and DNS

A StatefulSet requires a headless service — a Service with clusterIP: None. Instead of load balancing across pods, it creates individual DNS entries for each pod:

# mongodb-headless-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mongodb-headless
  namespace: production
spec:
  clusterIP: None      # This is what makes it "headless"
  selector:
    app: mongodb
  ports:
    - port: 27017
      targetPort: 27017

With the headless service, each pod gets a predictable DNS entry:

mongodb-0.mongodb-headless.production.svc.cluster.local → Pod-0 IP
mongodb-1.mongodb-headless.production.svc.cluster.local → Pod-1 IP
mongodb-2.mongodb-headless.production.svc.cluster.local → Pod-2 IP

This is how MongoDB replica set members find each other after restarts — they connect by DNS name, not IP address. IPs change; DNS names don’t.

Also create a regular Service for the primary (write endpoint):

# mongodb-primary-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mongodb-primary
  namespace: production
spec:
  selector:
    app: mongodb
    role: primary     # Label set by the replica set initialization process
  ports:
    - port: 27017

Application connection string:

mongodb://app:password@mongodb-0.mongodb-headless.production.svc.cluster.local:27017,mongodb-1.mongodb-headless.production.svc.cluster.local:27017,mongodb-2.mongodb-headless.production.svc.cluster.local:27017/mydb?replicaSet=rs0

StatefulSet Ordered Startup and Shutdown

Unlike Deployments, where Pods are treated as interchangeable units, StatefulSets provide each Pod with a unique and stable identity. StatefulSets guarantee ordered deployment — Pod-0 must be Running and Ready before Pod-1 starts, which must be Running and Ready before Pod-2 starts.

# Scale up a StatefulSet — watch ordered startup
kubectl scale statefulset/mongodb --replicas=5 -n production
# mongodb-0 → Running → Ready
# mongodb-1 → (waits for mongodb-0 Ready) → Running → Ready
# mongodb-2 → (waits for mongodb-1 Ready) → Running → Ready
# ...

# Scale down — reverse order termination
kubectl scale statefulset/mongodb --replicas=2 -n production
# mongodb-4 → Terminating → Deleted
# mongodb-3 → (waits for mongodb-4 Deleted) → Terminating → Deleted
# ...

# View StatefulSet status
kubectl get statefulset mongodb -n production
# NAME      READY   AGE
# mongodb   3/3     5m

# Describe a specific pod identity
kubectl get pods -l app=mongodb -n production
# NAME          READY   STATUS    RESTARTS
# mongodb-0     1/1     Running   0
# mongodb-1     1/1     Running   0
# mongodb-2     1/1     Running   0

Update strategy for StatefulSets:

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 1    # Only update pods with ordinal >= 1 (canary rollout)
                      # Set partition: 0 to update all pods

The partition setting enables canary rollouts on StatefulSets: update mongodb-2 first, verify it works, then update mongodb-1, then mongodb-0 (the primary) — a controlled rolling update for stateful systems.

Kubernetes DaemonSet: Node-Level Workloads

A DaemonSet ensures that a single instance of a pod is running on each node in a cluster. While the earlier controller types ensure that a specific number of replicas are running across the cluster, DaemonSets are intended to run exactly one pod per node. This is particularly useful for running pods as system daemons or background processes that need to run on every node.

The key behavior that distinguishes DaemonSet:

  • When a new node joins the cluster, a DaemonSet pod is automatically scheduled on it
  • When a node is removed, its DaemonSet pod is garbage collected
  • You never set replicas — the count is always equal to the number of matching nodes

DaemonSets are like security guards at a party — they make sure a critical task runs at every entrance. You run a weather app with 10 replicas in a Deployment (Kubernetes might place 3 pods on Node A, 5 on Node B, 2 on Node C). But you deploy a security scanner as a DaemonSet — exactly 1 pod on Node A, 1 on Node B, 1 on Node C, always.

Why node-level agents need DaemonSets, not Deployments:

Consider a log collector like Grafana Alloy. It needs to read log files from /var/log on each node. If you deploy it as a Deployment with 3 replicas on a 10-node cluster:

  • 7 nodes have no log collector — their logs are never collected
  • 3 nodes might each have a log collector, but Kubernetes could schedule all 3 on the same node

A DaemonSet guarantees exactly one collector on every node, always.

DaemonSet YAML Example with Node Selector

# node-exporter-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
  labels:
    app: node-exporter
spec:
  selector:
    matchLabels:
      app: node-exporter

  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1    # Update one node at a time

  template:
    metadata:
      labels:
        app: node-exporter
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9100"
    spec:
      # Run on the host network (required for node-level metrics)
      hostNetwork: true
      hostPID: true

      # Don't mount service account token (not needed)
      automountServiceAccountToken: false

      # Tolerate all taints — run on every node including control plane
      tolerations:
        - operator: Exists    # Match any taint key/value

      # Optional: run only on specific nodes
      # nodeSelector:
      #   node-role: worker

      containers:
        - name: node-exporter
          image: prom/node-exporter:latest
          args:
            - "--path.procfs=/host/proc"
            - "--path.sysfs=/host/sys"
            - "--path.rootfs=/rootfs"
            - "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
          ports:
            - containerPort: 9100
              hostPort: 9100      # Expose on host port — bypasses kube-proxy
          resources:
            requests:
              cpu: "100m"
              memory: "64Mi"
            limits:
              cpu: "250m"
              memory: "128Mi"
          securityContext:
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            runAsUser: 65534    # nobody user
          # Mount host filesystem for node-level access
          volumeMounts:
            - name: proc
              mountPath: /host/proc
              readOnly: true
            - name: sys
              mountPath: /host/sys
              readOnly: true
            - name: root
              mountPath: /rootfs
              readOnly: true

      volumes:
        - name: proc
          hostPath:
            path: /proc
        - name: sys
          hostPath:
            path: /sys
        - name: root
          hostPath:
            path: /

This is the exact Node Exporter DaemonSet pattern used in the Prometheus + Grafana monitoring stack — here running natively inside Kubernetes instead of Docker Compose, but the configuration principles are identical.

Log collection DaemonSet (Grafana Alloy):

# alloy-daemonset.yaml — ships pod logs to Grafana Loki
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: grafana-alloy
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: grafana-alloy
  template:
    metadata:
      labels:
        app: grafana-alloy
    spec:
      serviceAccountName: grafana-alloy   # Needs API access to discover pod metadata
      tolerations:
        - operator: Exists
      containers:
        - name: grafana-alloy
          image: grafana/alloy:v1.16.3
          args:
            - run
            - /etc/alloy/config.alloy
            - --server.http.listen-addr=0.0.0.0:12345
          volumeMounts:
            - name: config
              mountPath: /etc/alloy
            - name: varlog
              mountPath: /var/log
              readOnly: true
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: alloy-config
        - name: varlog
          hostPath:
            path: /var/log
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers

For the complete Loki + Alloy log aggregation setup, see our Grafana Loki with Docker Compose guide — the Alloy configuration is identical whether it runs as a DaemonSet in Kubernetes or as a container in Docker Compose.

DaemonSet Updates and Tolerations

# Check DaemonSet status
kubectl get daemonset node-exporter -n monitoring
# NAME            DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE
# node-exporter   3         3         3       3            3

# Desired = number of cluster nodes
# All other numbers should match Desired when healthy

# Trigger a rolling update by changing the image
kubectl set image daemonset/node-exporter \
  node-exporter=prom/node-exporter:v1.9.1 \
  -n monitoring

# Watch the update
kubectl rollout status daemonset/node-exporter -n monitoring

Tolerations explained:

By default, DaemonSet pods don’t run on the control plane node (which has a node-role.kubernetes.io/control-plane:NoSchedule taint). For infrastructure tools like monitoring agents or network plugins that must run on all nodes including the control plane:

tolerations:
  - operator: Exists    # Tolerate any taint — run on all nodes

# Or be more specific:
tolerations:
  - key: "node-role.kubernetes.io/control-plane"
    operator: "Exists"
    effect: "NoSchedule"

Run DaemonSet only on specific nodes:

nodeSelector:
  disk: ssd             # Only nodes with this label

# Or more powerful — use nodeAffinity:
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
            - key: node-type
              operator: In
              values:
                - worker
                - edge

Side-by-Side Comparison Table

Understanding the difference between each controller type is useful to understand which one to use in which scenario.

FeatureDeploymentStatefulSetDaemonSet
Pod identityRandom suffix (pod-7d9f4b)Ordinal index (pod-0, pod-1)One per node
Pod countAny number you specifyAny number you specify= Number of matching nodes
ScalingInstant, any orderOrdered (one at a time)Auto with node count
StorageShared or ephemeralDedicated PVC per podNode filesystem access
DNSSingle Service endpointPer-pod DNS via headless svcPer-node hostPort
Startup orderParallel, any orderSequential (0 → 1 → 2)Per node, unordered
Shutdown orderAny orderReverse ordinal (2 → 1 → 0)Per node
Rolling updatemaxSurge/maxUnavailableOrdinal partitionmaxUnavailable
Rollbackkubectl rollout undoManual (complex)kubectl rollout undo
Typical use casesAPIs, frontends, workersDatabases, message queuesAgents, plugins, daemons
replicas field✅ Required✅ Required❌ Not used
Headless ServiceNot required✅ RequiredNot required
VolumeClaimTemplate❌ Not available✅ Yes❌ Not available

Real-World Architecture: Using All Three Together

A production e-commerce platform typically uses all three controllers simultaneously:

┌─────────────────────────────────────────────────────────────────┐
│  DAEMONSET (on every node)                                      │
│  ├── node-exporter (Prometheus metrics)                         │
│  ├── grafana-alloy (log collection → Loki)                      │
│  ├── calico-node (network plugin)                               │
│  └── falco (runtime security scanner)                           │
├─────────────────────────────────────────────────────────────────┤
│  DEPLOYMENT (stateless, scalable)                               │
│  ├── web-frontend (React app) — 3-10 replicas                   │
│  ├── api-gateway — 3-5 replicas                                 │
│  ├── order-service — 2-8 replicas                               │
│  ├── payment-service — 2 replicas (cost-sensitive)              │
│  └── notification-worker — 3 replicas                           │
├─────────────────────────────────────────────────────────────────┤
│  STATEFULSET (stateful, identity-sensitive)                     │
│  ├── postgresql (primary + 2 replicas) — 3 pods                 │
│  ├── redis-cluster — 6 pods (3 primary + 3 replica)             │
│  ├── kafka — 3 pods                                             │
│  └── elasticsearch — 3 pods                                     │
└─────────────────────────────────────────────────────────────────┘

The traffic flow:

User Request
  → NGINX Ingress Controller (Deployment)
      → api-gateway (Deployment)
          → order-service (Deployment)
              → postgresql (StatefulSet)  — write order to DB
              → redis-cluster (StatefulSet) — cache session
              → kafka (StatefulSet) — emit order-placed event
          → notification-worker (Deployment)  — consume Kafka event, send email

Simultaneously on every node:
  node-exporter (DaemonSet) → Prometheus → Grafana dashboard
  grafana-alloy (DaemonSet) → Loki → Grafana logs

Common Mistakes and How to Avoid Them

1. Using Deployment for a database:

# ❌ Wrong — Deployment for PostgreSQL
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgresql    # This will lose data on reschedule!
spec:
  replicas: 1         # Even with 1 replica, volume reattachment is not guaranteed
  template:
    spec:
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: postgresql-pvc    # Shared PVC — dangerous with >1 replica

The risk: if the pod is rescheduled to a different node, and the PVC uses a node-local storage class, the pod can’t start on the new node because the data is on the old one. A StatefulSet handles this correctly.

# ✅ Correct — StatefulSet for PostgreSQL
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgresql
spec:
  serviceName: postgresql-headless
  replicas: 1          # Single node, but now properly managed
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        storageClassName: "standard"   # Must be a storage class that supports the node
        resources:
          requests:
            storage: 50Gi

2. Using DaemonSet when you just want wide distribution:

# ❌ Wrong — DaemonSet for a web service
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: web-api    # Bad idea — one pod per node, can't scale independently

DaemonSets exist for node-level functionality, not for distributing application load. For wide distribution with Deployments, use topologySpreadConstraints instead.

3. Forgetting the headless Service for a StatefulSet:

# ❌ StatefulSet applied without headless service
# Pods come up but can't discover each other — replica set init fails

# ✅ Always apply the headless service first
kubectl apply -f mongodb-headless-service.yaml
kubectl apply -f mongodb-statefulset.yaml

4. Scaling a StatefulSet down too aggressively:

# ❌ Scaling MongoDB from 3 to 1 while writes are happening
kubectl scale statefulset/mongodb --replicas=1
# This terminates pod-2 and pod-1 immediately
# If pod-0 wasn't the primary, the cluster loses quorum during termination

# ✅ Scale down one replica at a time, verifying replica set health between steps
kubectl scale statefulset/mongodb --replicas=2
kubectl exec mongodb-0 -- mongosh --eval "rs.status()"  # Verify still healthy
kubectl scale statefulset/mongodb --replicas=1

5. Not setting terminationGracePeriodSeconds on StatefulSets:

spec:
  template:
    spec:
      # Default is 30 seconds — often too short for databases
      # Set to enough time for graceful shutdown + checkpoint flushing
      terminationGracePeriodSeconds: 60    # Or longer for large databases

Frequently Asked Questions

Can I convert a Deployment to a StatefulSet?
Not directly — they use different APIs and different storage models. The typical migration path: create the StatefulSet, migrate data to the new persistent volumes (using a backup/restore procedure), then delete the old Deployment. For databases like Redis or MongoDB, this usually means taking a snapshot and restoring it into the StatefulSet-managed pods.

Can a StatefulSet have zero replicas?
Yes — kubectl scale statefulset/mongodb --replicas=0 terminates all pods in reverse order and is the correct way to “pause” a StatefulSet without deleting it or its PVCs. Useful for maintenance.

What happens to DaemonSet pods when a node is cordoned?
Cordoning (kubectl cordon node-1) prevents new pods from being scheduled but does not evict existing pods — existing DaemonSet pods keep running. Draining (kubectl drain) with --ignore-daemonsets flag is required to move DaemonSet pods, and they’re immediately recreated on the node when it’s uncordoned.

Can a Deployment use persistent storage?
Yes, but only shared volumes that all replicas access together (e.g., a shared NFS mount with ReadWriteMany access mode), or each pod gets the same PVC (which only works with ReadWriteMany storage). StatefulSets are the right choice when each replica needs its own dedicated storage.

Why does my StatefulSet pod have -0 in its name?
That’s the ordinal index — pod-name-0 is the first pod, pod-name-1 is the second. This is intentional and permanent. The name persists across restarts, which is what gives the pod its stable DNS identity.

When does a DaemonSet make sense with nodeSelector?
When you have heterogeneous nodes and some pods should only run on specific hardware — for example, a GPU monitoring agent that only makes sense on GPU-enabled nodes: nodeSelector: accelerator: nvidia-tesla.

Next Steps

With a solid understanding of Kubernetes workload controllers:

  • Set up your cluster — if you don’t have a running Kubernetes cluster yet, our Install Kubernetes Cluster on Ubuntu 26.04 LTS guide covers the full kubeadm setup from scratch.
  • Add monitoring — deploy Node Exporter as a DaemonSet and Prometheus + Grafana as Deployments using the patterns in our Prometheus + Grafana Monitoring Stack guide.
  • Add log collection — deploy Grafana Alloy as a DaemonSet to collect logs from all pods and ship them to Loki — see our Grafana Loki guide for the Loki setup.
  • Harden your workloads — apply Pod Security Standards (Restricted profile), network policies, and RBAC to all three controller types — the KubeSphere and Kubernetes Security guide covers the full hardening checklist.
  • Try Redis or MongoDB — if you’re running Redis or MongoDB in Docker Compose today, the StatefulSet patterns in this article are the Kubernetes equivalent — see our Redis with Docker Compose and MongoDB with Docker Compose guides for the Docker-based version of the same concepts.
(Visited 3 times, 1 visits today)

You may also like