Polyaxon v3 is coming →

StatefulSet vs. Deployment: differences and use cases

Compare Kubernetes StatefulSets and Deployments, including identity, storage, scaling, rollout behavior, and common use cases.

November 16, 2022by Polyaxon

Not every replicated workload is stateless. Deployments treat their Pod replicas as interchangeable. StatefulSets give Pods stable identity and storage associations for workloads that care about order, names, and durable state.

The choice affects how a replacement finds its data, how peers discover each other, and how an update proceeds. Both controllers can use persistent storage; the important distinction is whether each replica needs its own identity and storage association.

What is a Deployment in Kubernetes?

A Deployment is a Kubernetes resource object that provides declarative updates for Pods that encapsulate application containers. It manages ReplicaSets to maintain the desired number of replicas and roll out changes to their Pod template. Every Pod has a unique name and UID, but a Deployment does not preserve a particular replica's name when replacing it.

Deployments are typically used to scale interchangeable replicas, perform controlled rollouts for application code, and perform rollbacks when necessary. Automatic scaling requires a separate controller, such as a configured Horizontal Pod Autoscaler; a Deployment does not change its replica count merely because traffic increases.

Kubernetes administrators rely on Deployments to manage a containerized application's lifecycle by defining the number of Pods to be deployed, the image to be used for the application, and how to perform code updates. Kubernetes Deployments help automate repeatable application updates, subsequently reducing the effort, time, and number of errors associated with manual updates.

Components of a Kubernetes Deployment

The Deployment manifest describes the workload. Services and persistent storage are separate resources that can support it, depending on the application's requirements:

  • Deployment template: This is a JSON or YAML configuration file that is used to define the Deployment's configuration specification. The Kubernetes Deployment controller relies on the desired state described in the manifest to manage ReplicaSets and their Pods. The JSON or YAML file includes a Pod template that defines what each Pod should look like, as well as other common parameters, such as:
    • Number of Pod replicas
    • Container image and its tag or digest
    • Secrets, ConfigMaps, and other settings injected into the Pod
    • Pod labels that match the Deployment selector and any Service selector
  • Service: Provides a stable network endpoint for selected Pods. A Service selects Pod labels; it does not select the Deployment object itself. A Deployment can run without a Service.
  • PersistentVolume and PersistentVolumeClaim: A PV represents provisioned storage, and a PVC requests storage that Pods can mount. Storage may be local or network-backed. Persistent storage is optional, and attaching it does not give Deployment replicas stable identities.

Deployment configuration manifest

The example keeps three interchangeable web replicas and one shared PVC. The later StatefulSet example uses the same web server with a separate claim for each replica, making the storage association easier to compare.

Use a development Kubernetes cluster with Linux nodes, kubectl, and permission to create the workloads, Services, and claims below. The shared Deployment claim requires a StorageClass that supports ReadWriteMany. The StatefulSet example requires a default StorageClass that can provision ReadWriteOnce volumes, or an explicit compatible class in its claim template. These capabilities come from the storage backend; a class named default is not automatically the cluster's default.

Check your context and available classes, then create a namespace for the examples:

kubectl config current-context
kubectl get storageclass
kubectl create namespace workload-demo

The two examples use different Pod labels so their controllers and Services can coexist. Save the Deployment below as darwin-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: darwin-deployment
  namespace: workload-demo
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 1
  selector:
    matchLabels:
      app: darwin-deployment
  replicas: 3
  template:
    metadata:
      labels:
        app: darwin-deployment
    spec:
      containers:
        - name: web-app
          image: nginx:1.30.4-alpine
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: http
            periodSeconds: 5
          volumeMounts:
            - name: darwin-volume
              mountPath: /usr/share/nginx/html/shared
              readOnly: true
      volumes:
        - name: darwin-volume
          persistentVolumeClaim:
            claimName: darwin-volume-claim

This Deployment runs three replicas of the official NGINX image. Each Pod mounts darwin-volume-claim at the same path. The volume name in volumeMounts matches the name in volumes; the claim name identifies the separate PVC.

Save the following claim as darwin-volume-claim.yaml. Replace shared-filesystem with the actual name of your RWX-capable StorageClass:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: darwin-volume-claim
  namespace: workload-demo
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
  storageClassName: shared-filesystem

The shared directory starts empty and must be populated separately to serve shared files. Mounting only /usr/share/nginx/html/shared leaves NGINX's built-in welcome page at / available for the readiness probe. The web Pods mount the directory read-only; RWX support alone would not protect against unsafe concurrent writes.

Many local clusters provide only RWO storage by default. Such a class cannot satisfy this shared RWX claim. For a Deployment that does not need shared files, omit the PVC and the volumes and volumeMounts entries, and skip the commands that create, inspect, or delete darwin-volume-claim. Our persistent-volume guide explains the storage choices in more detail.

A Service provides a stable endpoint for the web Pods. Save this ClusterIP Service as service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: darwin-service
  namespace: workload-demo
spec:
  ports:
    - name: http
      port: 80
      targetPort: http
  selector:
    app: darwin-deployment
  type: ClusterIP

The Service selector matches the Deployment's Pod labels. ClusterIP exposes the endpoint inside the cluster. A NodePort Service can additionally expose a node port, but external access is not required to run the Deployment or compare these controllers.

Apply the claim, Service, and Deployment from the directory containing the files:

kubectl apply -f darwin-volume-claim.yaml
kubectl apply -f service.yaml
kubectl apply -f darwin-deployment.yaml
kubectl --namespace workload-demo rollout status deployment/darwin-deployment --timeout=300s

Discovering Deployment details

Administrators can use kubectl to inspect the Deployment and the Pods it controls. Check the Deployment's readiness and the shared claim:

kubectl --namespace workload-demo get deployment darwin-deployment
kubectl --namespace workload-demo get pvc darwin-volume-claim

To check the Pods created through its ReplicaSet, run:

kubectl --namespace workload-demo get pods -l app=darwin-deployment

Expect three ready replicas and one bound claim. Deployment Pod names include the Deployment name, a ReplicaSet template hash, and a generated suffix. A replacement gets a new name and UID.

If the rollout stalls, inspect the Pod and PVC events. An unsupported access mode, missing storage class, or image pull failure can prevent Pods from becoming ready:

kubectl --namespace workload-demo describe pods -l app=darwin-deployment
kubectl --namespace workload-demo describe pvc darwin-volume-claim

Scaling deployments

The kubectl command can also change the desired replica count. To increase darwin-deployment to five Pods, run:

kubectl --namespace workload-demo scale deployment/darwin-deployment --replicas=5

This is manual scaling. Update spec.replicas in the source manifest as well if five is the new desired state; reapplying a manifest that still specifies three can undo the change. For scaling based on load, configure an appropriate Horizontal Pod Autoscaler and its required metrics.

Kubernetes deployment strategies

The Deployment API has two built-in values for spec.strategy.type:

  • Recreate: Terminates the old revision's Pods before creating the new revision during an update. This creates an interruption for a service that depends on those Pods. It is not a general single-writer guarantee for every failure or manual Pod deletion.
  • RollingUpdate: Gradually replaces old replicas according to maxSurge and maxUnavailable. The example permits two extra replicas and one unavailable replica during a rollout. Updates are not necessarily one at a time, and zero downtime depends on readiness, capacity, application compatibility, and graceful shutdown.

Ramped is a common name for a gradual rollout, rather than another Deployment API strategy. Canary deployment keeps old and new versions running while evaluating a limited release. It usually needs separate Deployments and deliberate traffic routing, or a rollout controller; setting strategy.type: Canary is not supported by a standard Deployment.

Deployment revisions support rollback, but reverting a Pod template does not undo a database schema change or restore data on a volume. The Deployment documentation describes strategy settings, progress, and revision history.

What is a StatefulSet in Kubernetes?

A StatefulSet is a Kubernetes resource object that manages Pods with stable identities. Each replica has a numeric ordinal and a name such as darwin-0, and can have its own persistent storage. When a Pod is replaced, the replacement keeps that name and storage association, but receives a new Kubernetes UID and may have a different IP address or run on another node.

The controller uses a common Pod template, but the replicas are not interchangeable when the application depends on their identities or individual data. StatefulSets manage Pods directly and record template revisions with ControllerRevisions. They support rollback even though they do not create ReplicaSets.

StatefulSets are typically used for stable network identities, storage associated with individual replicas, or ordered startup and updates. The default OrderedReady policy creates Pods in ordinal order and waits for readiness; scaling down proceeds in reverse order. Parallel relaxes ordering for scaling when the application can handle it. A StatefulSet does not configure application-level replication or leader election.

Components of a Kubernetes StatefulSet configuration manifest

A Kubernetes StatefulSet configuration comprises the following:

  • StatefulSet: Defines the Pod template, selector, desired replica count, and update behavior.
  • Headless Service: Supplies the governing network domain for Pod DNS names. It has no virtual Service IP and returns Pod endpoints rather than providing ordinary Service load balancing.
  • Volume claim template: Creates a separate PVC for each replica when that replica needs persistent storage. Stable identity can also be useful without a claim template.

StatefulSet configuration manifest

Save the following configuration as statefulset.yaml. It uses the same namespace and NGINX image, but a separate selector and a claim template named www:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: darwin
  namespace: workload-demo
spec:
  selector:
    matchLabels:
      app: darwin-statefulset
  serviceName: "darwin-set"
  replicas: 3
  podManagementPolicy: OrderedReady
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: darwin-statefulset
    spec:
      containers:
        - name: darwin-app
          image: nginx:1.30.4-alpine
          ports:
            - containerPort: 80
              name: web
          readinessProbe:
            httpGet:
              path: /
              port: web
            periodSeconds: 5
          volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html/data
  volumeClaimTemplates:
    - metadata:
        name: www
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi

The www volume mount refers to the claim template. For the three replicas, Kubernetes creates www-darwin-0, www-darwin-1, and www-darwin-2. Each Pod mounts its own claim at /usr/share/nginx/html/data. There is no standalone darwin-claim.yaml to apply in this example.

The template omits storageClassName, so it uses the cluster's default class. If needed, add storageClassName under volumeClaimTemplates[].spec with a compatible class name. RWO allows read-write attachment from one node; it does not mean exactly one Pod. ReadWriteOncePod provides single-Pod access where supported by the CSI driver. Neither access mode makes backups or replicates the application's data. See the PersistentVolume documentation for backend support and access modes.

Save the governing headless Service as darwin-service.yaml. Its name, darwin-set, matches the StatefulSet's spec.serviceName, and its selector matches the StatefulSet's Pod labels:

apiVersion: v1
kind: Service
metadata:
  name: darwin-set
  namespace: workload-demo
spec:
  ports:
    - port: 80
      name: web
      targetPort: web
  clusterIP: None
  selector:
    app: darwin-statefulset

Create the headless Service before the StatefulSet, then wait for the Pods:

kubectl apply -f darwin-service.yaml
kubectl apply -f statefulset.yaml
kubectl --namespace workload-demo rollout status statefulset/darwin --timeout=300s

With storage provisioned and readiness passing, these commands create darwin-0, darwin-1, and darwin-2 in order. Ready Pods have names such as darwin-0.darwin-set.workload-demo.svc in cluster DNS. A replacement can retain that DNS name without retaining its IP address. Some clustered applications need discovery before readiness; their bootstrap configuration must account for the Service's ready-endpoint behavior.

Discovering StatefulSet details

Inspect the named Pods and the three claims:

kubectl --namespace workload-demo get pods -l app=darwin-statefulset
kubectl --namespace workload-demo get pvc www-darwin-0 www-darwin-1 www-darwin-2

The Deployment shares one explicitly configured claim, while this StatefulSet creates one claim per replica. If creation stops at darwin-0, inspect that Pod and www-darwin-0: with OrderedReady, a pending volume or failing readiness probe prevents the next Pod from starting.

Verify the storage association

For the disposable NGINX example, write a marker into darwin-0's volume and record the Pod UID:

kubectl --namespace workload-demo exec darwin-0 -- \
  sh -c 'hostname > /usr/share/nginx/html/data/identity.txt'
kubectl --namespace workload-demo get pod darwin-0 \
  -o custom-columns=NAME:.metadata.name,UID:.metadata.uid

Restart the StatefulSet through its rolling update mechanism, then read the marker:

kubectl --namespace workload-demo rollout restart statefulset/darwin
kubectl --namespace workload-demo rollout status statefulset/darwin --timeout=300s
kubectl --namespace workload-demo get pod darwin-0 \
  -o custom-columns=NAME:.metadata.name,UID:.metadata.uid
kubectl --namespace workload-demo exec darwin-0 -- \
  cat /usr/share/nginx/html/data/identity.txt

Expect the same Pod name, a new UID, and the saved value darwin-0. The replacement reuses www-darwin-0; the controller does not copy the file into the other replicas' volumes. This checks controlled Pod replacement, not recovery from storage loss or a cluster failure.

StatefulSet updates and rollback

The default rolling update replaces Pods from the highest ordinal downward and waits for readiness before continuing. OnDelete is an alternative when operators need to decide when each Pod adopts the new template. Choose update behavior to match the application's availability and coordination requirements.

For a StatefulSet with retained revisions, kubectl rollout history statefulset/darwin lists the history and kubectl rollout undo statefulset/darwin requests the previous revision; include --namespace workload-demo for this example. Rollback changes the Pod template, not the stored data.

If an ordered rollout stalls on a Pod that never becomes ready, restoring the working template may not be enough. The documented recovery procedure can also require deleting the broken Pod after reverting the template so the controller recreates it. Follow the StatefulSet rollback guidance and the application's recovery procedure. Avoid force-deleting a stateful Pod when the old process may still be running.

Kubernetes Deployment vs. StatefulSet: how to choose

The table below shows the primary differences between a StatefulSet and a Deployment:

AspectDeploymentStatefulSet
Data persistenceCan mount persistent storage; replica identity is replaceable.Can associate persistent storage with a stable replica identity.
Pod name and identityGenerated names; a replacement has a new name and UID.Stable name and ordinal; a replacement still has a new UID and may have a new IP.
InterchangeabilityReplicas of a revision share a template and are intended to be interchangeable.Replicas share a template but can have distinct identities and data.
Replacement behaviorA ReplicaSet maintains the desired replica count.The controller recreates the named replica and reuses its retained claims.
Volume claimsPods can reference an existing PVC, as in the shared-volume example; storage is not required.A claim template creates one PVC per replica per template; storage is not required.
Volume access modesChosen according to storage capabilities and the application's access pattern.Also chosen according to storage capabilities and the application's access pattern.
Pod networkingAn optional Service supplies a stable endpoint for interchangeable replicas.The governing headless Service supplies per-Pod DNS discovery; a separate client Service can also be used.
Scaling orderNo stable ordinal or application-specific ordering guarantee.Ordered creation and reverse-order scale-down by default; Parallel relaxes scale ordering.
Updates and rollbackConfigurable rolling or recreate updates, with revision-based rollback.Ordered rolling updates by default, with revision-based rollback and other update options.

When to use

A StatefulSet is better suited to workloads that require a stable identity or storage association for each replica, such as some databases, brokers, and clustered services. Its replicas are not tied to one Pod per cluster node. A Deployment is suitable for interchangeable application replicas, such as web servers using NGINX or Apache, including applications that keep their durable state elsewhere.

This practical scenario demonstrates how a StatefulSet differs from a Deployment:

Consider a web app that uses a relational database to store data. When traffic to the application increases, administrators intend to scale up the number of Pods to support the workload. A straightforward approach is to change the replica count within the Deployment's configuration manifest; then the Deployment controller will take care of scaling. Since new Pod replicas are assigned the same set of ConfigMaps and environment variables when starting, they communicate with the backend the same way as the original Pod. This works when sessions, database access, and other application state do not depend on a particular web replica. Database connections and backend capacity still need to support the additional traffic.

The relational database may also need more capacity, but adding database Pods requires a database-aware plan. A primary/replica architecture needs bootstrap, replication, leader selection, and client routing configured by the database tooling or an operator. A new replica might copy an initial dataset from a designated source and then follow a transaction log; StatefulSet ordering does not perform those steps or guarantee that the preceding Pod is the correct source.

A StatefulSet supplies stable replica names and, with claim templates, separate storage. When a Pod is replaced, the controller can reuse its retained PVC, provided the storage remains available and can attach where the replacement is scheduled. The database still owns consistency, recovery, and replication. Kubernetes' replicated stateful application tutorial shows how application-specific bootstrap is layered onto the controller. For a simpler database example, see deploying PostgreSQL on Kubernetes.

Use cases

RequirementTypical choiceWhy
Web API with sessions and durable state stored externallyDeploymentReplicas can serve requests interchangeably behind a Service.
Web replicas reading one shared filesystemDeployment with a compatible shared PVCShared data does not require stable per-replica identities.
Database or key-value store with stable member names and separate data directoriesStatefulSet, often managed by an operatorProvides identity and storage associations; database tooling supplies replication and failover.
Clustered service that depends on ordered member startupStatefulSet with OrderedReadyThe controller can wait for one member's readiness before creating the next.
Scaling a replicated service from metricsA compatible controller with a configured autoscalerBoth the metric policy and application semantics must support replica changes.
Training, data processing, or another task that finishesJob or a specialized workload controllerCompletion and retry behavior are different from maintaining a long-running replica count.

Both Deployments and StatefulSets support controlled updates. Sharing a Pod template, ConfigMaps, or environment variables is also common to both, so those requirements alone do not decide the choice.

Clean up the examples

For orderly shutdown, scale the StatefulSet to zero and wait for its Pods to terminate:

kubectl --namespace workload-demo scale statefulset/darwin --replicas=0
kubectl --namespace workload-demo wait --for=delete pod \
  -l app=darwin-statefulset --timeout=180s

If shutdown times out, inspect the remaining Pods before continuing. Once they have stopped, remove the controllers and Services:

kubectl delete -f darwin-deployment.yaml -f service.yaml -f statefulset.yaml -f darwin-service.yaml

The explicitly created Deployment claim and the StatefulSet's claims remain. Claim templates retain PVCs by default; a configured StatefulSet retention policy can change this behavior. Inspect the remaining storage:

kubectl --namespace workload-demo get pvc

Only when the example data is disposable, delete these four claims:

kubectl --namespace workload-demo delete pvc \
  darwin-volume-claim www-darwin-0 www-darwin-1 www-darwin-2

A PV with reclaim policy Delete can delete its underlying storage when the claim is removed; Retain requires separate administrator cleanup. If workload-demo contains no other resources you need, delete the namespace:

kubectl delete namespace workload-demo

Conclusion

Use Deployments for interchangeable replicated services. Use StatefulSets when stable replica identity, ordered lifecycle behavior, or storage associated with each replica matters. The distinction changes how Kubernetes creates, names, updates, and recovers Pods. Persistent storage alone does not determine the controller, and neither controller supplies database replication or backups.

For Polyaxon users, this matters around supporting services and platform infrastructure. Training jobs and model services have different lifecycles, and the Kubernetes primitive should match the lifecycle instead of fighting it. Polyaxon's job runtime covers tasks such as training and data processing, while its service runtime covers interactive applications and long-running tools. For the platform's metadata database, the PostgreSQL setup guide explains persistence and external database configuration.