Thumbnail

Kubernetes: Deployment vs StatefulSet vs DaemonSet

There is more than one way to run a workload in Kubernetes, and picking the right controller for the job is one of the first decisions you'll face. In this post, I'll go over the core workload objects: Pods, Deployments, StatefulSets, and DaemonSets. I'll explain what each one does, when to use it, and how they differ from each other.

Prerequisites

  • A beginner-level familiarity with Kubernetes concepts (nodes, pods, clusters).
  • kubectl installed and configured against a running cluster (local or cloud-managed).

Goals

  • Understand what Pods, Deployments, StatefulSets, and DaemonSets are.
  • Know when to reach for each workload type.
  • Be able to write a basic manifest for each one.

Pods: The Smallest Unit of Computation

A pod is the smallest unit of computation and replication in Kubernetes. If your application needs to scale up, you simply increase the number of pods. Since pods are scaled as a unit, all containers in a pod must scale together, regardless of their individual resource needs, which can lead to waste and higher costs.

To keep things efficient, pods should be as lean as possible: typically one main process plus any closely associated helper containers, commonly called sidecars.

In practice, you rarely create pods directly. Instead, Kubernetes provides higher-level abstractions called controllers that create and manage pods for you. A controller's job is to monitor the current state of a resource and take action to ensure the desired state is achieved. The three controllers covered here, Deployments, StatefulSets, and DaemonSets, are all built into Kubernetes, and each one solves a different use case.

Deployments: Managing Replicas Automatically

A Deployment is the most common way to run a workload. Its primary job is to declare how many replicas of a pod should be running at any given time. When you add a Deployment to the cluster, it automatically spins up the requested number of pods and monitors them. If a pod fails, the Deployment recreates it, no manual intervention needed.

Under the hood, a Deployment creates and manages a ReplicaSet, which is the lower-level controller that actually maintains the desired number of pod replicas. You typically don't interact with ReplicaSets directly because Deployments wrap them with additional features like rolling updates, rollbacks, and version history.

Sample Manifest

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

Here's what the key fields do:

  • .spec.replicas defines how many pod instances to run.
  • .spec.selector.matchLabels tells the Deployment which pods it owns. The labels here must match the ones in .spec.template.metadata.labels.
  • .spec.template is the pod template used to create each replica, including the container image, ports, and any other pod-level configuration.

Apply the manifest and verify:

kubectl apply -f deployment.yaml
kubectl get deployments
kubectl get pods

You should see the Deployment running alongside a ReplicaSet and three pods.

Exposing the Deployment

To expose your application to the outside world, you pair a Deployment with a Service object. Incoming requests are randomly redirected to one of the managed pods. Individual pods can't be directly targeted since each gets a unique, ephemeral pod ID.

Persisting Data in a Deployment

Because pods are ephemeral, any locally stored state is lost when a pod is rescheduled or deleted. To persist data, you need a Persistent Volume Claim (PVC).

Most cloud-managed Kubernetes services provide a default storage class that allocates volumes with ReadWriteOnce access, meaning the volume can only be attached to a single pod at a time. If your Deployment has more than one replica, the other pods will fail to mount the volume and get stuck in a ContainerCreating state.

Tip: A common pattern is using a PVC with Grafana to persist dashboards created through the UI. If you update the Grafana image with a single replica, the default rolling update strategy will try to spin up a new pod before destroying the old one, causing a mount conflict. The fix is to set the deployment strategy to Recreate, which tears down the old pod first before creating the new one.

If you need a volume mounted by multiple pods simultaneously, you can deploy a driver like EFS on AWS, a network storage solution that supports ReadWriteMany mode. This lets all replicas share the same volume, though that's usually not the desired behaviour for stateful workloads.

StatefulSets: When Identity Matters

For workloads that need a dedicated volume per pod and a stable network identity, the right tool is a StatefulSet.

StatefulSets work similarly to Deployments but add a critical feature: volumeClaimTemplates. This dynamically creates a dedicated Persistent Volume Claim for each pod the StatefulSet manages. If a pod gets rescheduled onto a different node, the same volume follows it, maintaining a consistent identity across restarts.

StatefulSets are commonly used for stateful applications such as databases or distributed file systems that require persistent storage and a stable identity to maintain data consistency.

Key Features

Unique Identity. Each pod in a StatefulSet gets an ordinal index (e.g., nginx-0, nginx-1, nginx-2) that stays the same even if the pod is deleted and recreated.

Persistent Network Identity. Each pod gets a stable, unique DNS name based on its ordinal index. This is essential for distributed systems like Kafka, where clients need to know the DNS name of a specific partition leader in order to connect directly to it. StatefulSets create and maintain both A records and SRV records for each pod.

Persistent Storage. Through volumeClaimTemplates, each pod gets its own PVC that survives restarts and rescheduling.

Sample Manifest

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-statefulset
  labels:
    app: nginx
spec:
  serviceName: nginx
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-data
          mountPath: /var/www/html
  volumeClaimTemplates:
  - metadata:
      name: nginx-data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi

The fields that differ from a Deployment:

  • .spec.serviceName specifies the headless Service that governs the network identity of the pods. This is required.
  • .spec.volumeClaimTemplates defines a PVC template. Kubernetes creates a separate 1Gi volume for each pod, using ReadWriteOnce access mode.
  • terminationGracePeriodSeconds gives Kubernetes a window to gracefully shut down the pod before forcefully terminating it. This is important for databases that need time to flush data to disk.
  • volumeMounts maps the volume into the container filesystem at /var/www/html.

Apply and verify:

kubectl apply -f statefulset.yaml
kubectl get statefulsets
kubectl get pods
kubectl get pvc

You should see three pods created sequentially (nginx-0, nginx-1, nginx-2) and three matching PVCs.

Additional Notes

  • Sequential pod creation is the default behaviour. For workloads with 50+ pods where order doesn't matter, set podManagementPolicy: Parallel to spin them all up at once.
  • Even stateless applications sometimes benefit from running as a StatefulSet when a strong, stable identity is required.

DaemonSets: One Pod Per Node

Sometimes you need exactly one pod running on every node in your cluster, for example a log collector or a monitoring agent. That's precisely what a DaemonSet is for.

DaemonSets automatically create as many pods as there are nodes. As the cluster scales up or down, pods are added and removed accordingly, no replica count to manage. This makes them ideal for system-level concerns like collecting logs, monitoring system performance, and managing network traffic across the entire cluster.

Sample Manifest

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
spec:
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
    spec:
      containers:
      - name: fluentd
        image: fluent/fluentd:v1.7.4-1.0
        volumeMounts:
        - name: varlog
          mountPath: /var/log
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log

Notice that there is no replicas field. The DaemonSet controller handles scheduling automatically, one pod per node.

The volumes field uses hostPath to mount the node's /var/log directory into the container. This allows Fluentd to read the node's log files directly.

Apply and verify:

kubectl apply -f daemonset.yaml
kubectl get daemonsets
kubectl get pods -o wide

You should see one pod per node in the NODE column.

Things to Keep in Mind

  • If your nodes have taints applied (a common practice to control pod placement), your DaemonSet pods need to tolerate all relevant taints to ensure they land on every node.
  • DaemonSets are also widely used for distributed storage solutions. If you're deploying Hadoop, MinIO, Rook, or Local Persistent Volumes, you'd typically provision Kubernetes nodes with attached disks and run a DaemonSet on each one, abstracting the local disks into a unified S3-compatible or filesystem interface for your applications.
  • Since a DaemonSet runs a single pod per node, rollouts are done one node at a time instead of one pod at a time.

Conclusion

Each Kubernetes controller solves a different problem. Deployments handle the vast majority of workloads where you need a certain number of identical, interchangeable pods. StatefulSets step in when pods need a stable identity and dedicated storage. DaemonSets ensure that every node in the cluster runs exactly one copy of a pod.

Here's a quick comparison:

Feature Deployment StatefulSet DaemonSet
Use case Stateless apps, APIs, web servers Databases, distributed systems (Kafka, Zookeeper) Log collectors, monitoring agents, node-level daemons
Replicas Configurable via replicas field Configurable via replicas field One per node (automatic)
Pod identity Random pod names, interchangeable Stable ordinal names (app-0, app-1) One unique pod per node
Network identity Accessed via Service (random routing) Stable DNS per pod via headless Service Runs locally on each node
Storage Shared PVC (or none) Dedicated PVC per pod via volumeClaimTemplates Typically hostPath volumes
Pod creation order All at once Sequential by default One per node as nodes join
Rolling updates One pod at a time One pod at a time One node at a time

Comments