Kubernetes🔥 Production-Critical⭐ Interview Prep

Kubernetes StatefulSet vs Deployment — Every Question Answered

Everything about StatefulSet and Deployment in one place. What they are, how they work inside Kubernetes, how they differ on storage, identity, scaling, and failure — plus 15 interview questions with complete answers.

15 min read·Updated July 2026·Beginner → Staff Engineer

Introduction

Deployment vs StatefulSet — architecture overview
DEPLOYMENT (stateless)                       STATEFULSET (stateful)
──────────────────────────────────────       ──────────────────────────────────────
Deployment                                   StatefulSet
  └── ReplicaSet (manages pods)                └── StatefulSet Controller (direct)
        ├── pod-7d4b9c-abc12   anonymous            ├── mysql-0   ordinal name (permanent)
        ├── pod-7d4b9c-xyz99   anonymous            ├── mysql-1   ordinal name (permanent)
        └── pod-7d4b9c-def55   anonymous            └── mysql-2   ordinal name (permanent)

Storage: shared PVC (or none)                Storage: 1 PVC per pod
  └── shared-pvc (100Gi)                       ├── data-mysql-0 (100Gi  exclusive)
      all pods mount this same disk             ├── data-mysql-1 (100Gi  exclusive)
                                               └── data-mysql-2 (100Gi  exclusive)

Startup: all pods in parallel                Startup: 0→1→2  (each waits for Ready)
DNS:     shared Service IP                   DNS:     mysql-0.mysql-headless (per pod)
Crash:   new pod, new random name            Crash:   same name, same PVC reattached
Use for: API, web servers, workers           Use for: DBs, Kafka, Elasticsearch

What is a Deployment?

A Deployment is the Kubernetes object you use to run stateless applications. When you create a Deployment, Kubernetes creates a ReplicaSet under the hood, and that ReplicaSet creates and manages N pods. All pods in a Deployment are anonymous and interchangeable — any pod can handle any request, and if one crashes, Kubernetes replaces it with a fresh pod with a new random name and a clean start.

Use Deployment for: web servers, REST APIs, GraphQL services, background workers, ML inference servers — anything that does not write state to local disk and does not care which specific pod handles which request.

Deployment structure
# Create a Deployment
kubectl apply -f deployment.yaml

# What Kubernetes creates:
Deployment: my-api
  └── ReplicaSet: my-api-7d4b9c
        ├── Pod: my-api-7d4b9c-xk2pq  (random name)
        ├── Pod: my-api-7d4b9c-ab3df  (random name)
        └── Pod: my-api-7d4b9c-mp9tz  (random name)

What is a StatefulSet?

A StatefulSet is the Kubernetes object you use to run stateful applications. Unlike a Deployment, every pod in a StatefulSet has a permanent identity — a fixed name based on its ordinal number (mysql-0, mysql-1, mysql-2), its own dedicated PersistentVolumeClaim, and a stable DNS hostname. This identity survives pod restarts.

If mysql-1 crashes and Kubernetes restarts it, the new pod is still named mysql-1, gets the same PVC (data-mysql-1) reattached, and has the same DNS hostname. Nothing in the cluster has to reconfigure. Data is preserved.

StatefulSet structure
# Create a StatefulSet
kubectl apply -f statefulset.yaml

# What Kubernetes creates:
StatefulSet: mysql
  ├── Pod: mysql-0   PVC: data-mysql-0   DNS: mysql-0.mysql-headless.default.svc.cluster.local
  ├── Pod: mysql-1   PVC: data-mysql-1   DNS: mysql-1.mysql-headless.default.svc.cluster.local
  └── Pod: mysql-2   PVC: data-mysql-2   DNS: mysql-2.mysql-headless.default.svc.cluster.local

Why did Kubernetes introduce StatefulSet when Deployment already existed?

Deployment was built on a core assumption: all pods are identical and interchangeable. This works perfectly for stateless apps. But when teams tried to run databases (MySQL, PostgreSQL, MongoDB) as Deployments, they ran into a wall — a crashed pod came back with a new name, a new IP, and no memory of its old data. Replicas lost their replication source. Clusters became corrupted.

StatefulSet was introduced in Kubernetes 1.5 (reached stable in 1.9) specifically to fill this gap. It gives pods the three things Deployment cannot provide: a stable identity that survives restarts, a dedicated PVC per pod, and guaranteed ordered startup and shutdown. Deployment was not wrong — it was the right tool for 80% of workloads. StatefulSet handles the remaining 20%.

What problem does StatefulSet solve?

StatefulSet solves three problems that Deployment cannot:

  • Stable identity: pod names never change (mysql-0 is always mysql-0), so other pods can always find it by the same DNS hostname.
  • Per-pod storage: each pod gets its own PVC via volumeClaimTemplates. Data survives pod crashes and node changes.
  • Ordered lifecycle: pods start in order (0 → 1 → 2, each waiting for the previous to be Ready) and stop in reverse order (2 → 1 → 0).

What is the difference between stateless and stateful applications?

A stateless application does not store any data between requests. Every request is self-contained. Any pod can handle any request. If you delete a pod and start a new one, nothing is lost. State lives outside the app — in a database, a cache, or an external service.

A stateful application stores data on disk that must survive restarts. Each instance owns a specific slice of data. You cannot swap one instance for another because the replacement would not have the same data. Pod identity and storage continuity both matter.

Core Concepts

What is a stateless application?

A stateless application is one where each request is handled independently, with no reliance on local in-memory or on-disk state from a previous request. All persistent state is stored in an external system — a database, Redis, S3. Any pod can handle any request at any time. Horizontal scaling is trivial: add more pods.

Examples: nginx, express, Django, FastAPI, Spring Boot (when configured correctly), Rails.

What is a stateful application?

A stateful application maintains data on disk across requests and across restarts. Each instance has a unique role — a primary database writes data that replicas read. Each Kafka broker owns specific partitions. Each Elasticsearch node holds specific shards. You cannot replace an instance arbitrarily — the replacement would start with a clean disk and break the cluster.

Examples: PostgreSQL, MySQL, MongoDB, Apache Kafka, Elasticsearch, Redis Cluster, ZooKeeper.

Why are web applications usually stateless?

Web applications are designed stateless intentionally. Storing session data in a cookie or JWT (rather than in-process memory) means any pod can authenticate any request. Storing user data in a shared database means any pod can serve any user. This design choice makes horizontal scaling simple — add pods, remove pods, crash pods — the application keeps working. If web applications were stateful, every user would have to be pinned to a specific pod, which kills horizontal scalability.

Why are databases stateful?

Databases write data to disk. That is their entire purpose. The data a database stores cannot be reconstructed if the disk is lost — unlike a stateless app where you simply restart from code. In a MySQL primary-replica setup, each replica maintains a copy of data it received from the primary. If the replica pod is replaced with a fresh pod (clean disk), it has no data — it must rebuild from scratch, which can take hours for a large dataset. The pod's disk is its core state.

What are some real-world examples of stateless applications?

  • REST API servers (Go, Node.js, Python)
  • Web front-ends (Next.js, Nuxt)
  • Microservices that read from and write to a database
  • Background job workers (Celery, Sidekiq, BullMQ)
  • ML inference servers (Triton, TorchServe)
  • Reverse proxies and load balancers (nginx, HAProxy)

All of these can run as Deployments.

What are some real-world examples of stateful applications?

  • Relational databases: PostgreSQL, MySQL, MariaDB
  • Document databases: MongoDB, CouchDB
  • Message queues: Apache Kafka, RabbitMQ (with durable queues)
  • Search engines: Elasticsearch, OpenSearch
  • Key-value stores (clustered): Redis Cluster, Redis Sentinel
  • Coordination services: ZooKeeper, etcd (user-managed)
  • Time-series databases: InfluxDB, Prometheus (with remote storage)

All of these run as StatefulSets in Kubernetes.

Deployment Deep Dive

How does a Deployment work internally?

When you apply a Deployment manifest, the Deployment Controller insidekube-controller-manager creates a ReplicaSet. The ReplicaSet Controller then creates the requested number of pod objects in etcd. The Scheduler assigns each pod to a node. kubelet on each node starts the container.

The Deployment Controller runs a continuous reconciliation loop: desired state (replicas: 3) vs actual state (running pods). If actual < desired, it creates pods. If actual > desired, it deletes pods.

Deployment internal sequence
kubectl apply  API Server stores Deployment
Deployment Controller  creates ReplicaSet (desired: 3 pods)
ReplicaSet Controller  creates 3 Pod objects in etcd (all Pending)
Scheduler  assigns each pod to a node
kubelet  starts container on each node
Pods become Running

What Kubernetes resources does a Deployment create?

A Deployment creates exactly two types of resources: one ReplicaSet and N pods. On each rolling update, a new ReplicaSet is created (for the new version) and the old one is scaled to 0. Old ReplicaSets are kept for rollback history — by default, the last 10 are retained (controlled by revisionHistoryLimit).

Deployment resources
kubectl get all -l app=my-api

# NAME                        READY   STATUS
# pod/my-api-7d4b9c-xk2pq     1/1     Running
# pod/my-api-7d4b9c-ab3df     1/1     Running
#
# NAME                         DESIRED  CURRENT  READY
# replicaset/my-api-7d4b9c     2        2        2
# replicaset/my-api-6c3a8b     0        0        0   ← old RS, kept for rollback

How does a Deployment use ReplicaSets?

A Deployment never directly creates or manages pods — it delegates that to ReplicaSets. On every update (image change, env var change, etc.), the Deployment Controller creates a new ReplicaSetfor the updated spec, then scales it up while scaling the old ReplicaSet down. This is how rolling updates work: two ReplicaSets exist simultaneously during the transition, and the old one is preserved at 0 replicas for rollback.

How does a Deployment manage Pods?

The ReplicaSet Controller constantly watches the number of running pods matching its selector. If a pod crashes or is deleted, the controller detects the count dropped below the desired number and immediately creates a new pod — with a new random name and a fresh start. The new pod has no relationship to the old one. Deployment treats all pods as fungible.

How does a Deployment perform rolling updates?

Rolling updates are controlled by two fields: maxSurge (how many extra pods above desired count) and maxUnavailable (how many pods can be down at once). The default is 25% for both.

Deployment rolling update
# Example: 4 replicas, maxSurge: 1, maxUnavailable: 1
# Update triggered (new image)

Step 1: Create 1 new pod (surge)  total: 5 pods
Step 2: New pod passes readiness probe  Ready
Step 3: Delete 1 old pod  total: 4 pods
Step 4: Repeat until all 4 pods are on new version

# Commands:
kubectl set image deployment/my-api api=my-api:v2 -n production
kubectl rollout status deployment/my-api -n production

How does a Deployment perform rollbacks?

Kubernetes keeps the previous ReplicaSet at 0 replicas. A rollback simply scales the old ReplicaSet back up and scales the current one down. It is fast because the old pods just need to restart — the image is usually already cached on nodes.

Deployment rollback
# Rollback to previous version
kubectl rollout undo deployment/my-api -n production

# Rollback to a specific revision
kubectl rollout history deployment/my-api -n production
kubectl rollout undo deployment/my-api --to-revision=2 -n production

How does a Deployment handle Pod failures?

The ReplicaSet Controller watches the cluster for its pods. When a pod crashes (exits with any code), or when a node fails and pods become Unknown, the controller sees the actual count has dropped below the desired count and creates a replacement pod. The replacement has a new random name, a new IP, and starts fresh. For a stateless application, this is exactly correct behavior.

How does a Deployment scale Pods?

You can scale manually or automatically. Manual scaling changes spec.replicas in the Deployment. Automatic scaling via HPA (Horizontal Pod Autoscaler) watches CPU, memory, or custom metrics and adjusts the replica count on your behalf. When scaling up, all new pods start simultaneously. When scaling down, the newest pods die first.

Deployment scaling
# Manual scale
kubectl scale deployment/my-api --replicas=10 -n production

# Auto scale (HPA)
kubectl autoscale deployment/my-api --min=2 --max=20 --cpu-percent=70 -n production

Can a Deployment use Persistent Volumes?

Yes. You can add a PVC to a Deployment spec under volumes. However, all pods in the Deployment share the same PVC — they all mount the same volume. This works for read-only shared data (config files, static assets) but not for databases, where each instance must have exclusive access to its own disk. Mounting one PVC across multiple pods with ReadWriteOnce access is also impossible — the volume can only be mounted on one node at a time.

When should you use a Deployment?

  • Your application does not write data to local disk between requests
  • Any pod can handle any request (pods are interchangeable)
  • You need fast horizontal scaling (all pods start simultaneously)
  • You need easy rolling updates and rollbacks
  • You need HPA for automatic scaling

Use it for: web servers, REST APIs, workers, microservices, ML inference.

When should you avoid using a Deployment?

  • Your application writes data to disk that must survive pod restarts
  • Each pod instance must have its own dedicated storage
  • Pods must start in a specific order (primary before replicas)
  • Other pods need to reach a specific pod by a stable hostname
  • You are running a database, message queue, or search engine

StatefulSet Deep Dive

How does a StatefulSet work internally?

StatefulSet uses a dedicated StatefulSet Controller inside kube-controller-manager— it does not use a ReplicaSet. The controller manages pods directly by ordinal number. It knows the identity of every pod it owns and will only create a replacement with the same identity (same name, same PVC reference). It enforces sequential ordering: pod N is never created until pod N-1 is Running AND Ready.

StatefulSet controller logic
# StatefulSet controller reconciliation loop:
Desired: mysql-0, mysql-1, mysql-2 (all Running+Ready)
Actual:  mysql-0 (Running+Ready), mysql-1 (Missing), mysql-2 (Running+Ready)

Gap detected: mysql-1 is missing
Action: create pod named mysql-1 (not mysql-3, not a random name)
        with PVC data-mysql-1 (existing PVC reattached if present)
Wait:   mysql-1 must reach Running+Ready before any other action
Loop:   check again

Why was StatefulSet introduced?

Deployment assumes all pods are anonymous. This breaks databases because:

  • A crashed pod comes back with a new name → replication source hostname changes → replication breaks
  • All pods share storage (or none) → no per-replica data isolation
  • All pods start simultaneously → replica starts before primary is ready → connection fails

StatefulSet was introduced in Kubernetes 1.5 (stable 1.9) to give pods the identity, storage, and ordering guarantees that databases need but Deployment cannot provide.

How does a StatefulSet create Pods?

StatefulSet creates pods one at a time, in ordinal order, starting from 0. Each pod must reach Running phase AND pass its readiness probe (Ready=True) before the next pod is created. If a pod fails to become Ready, the StatefulSet is blocked — no further pods are created until the blocked pod recovers.

StatefulSet sequential pod creation
# Scale from 0 to 3 replicas:
t=0s:  Create mysql-0 + PVC data-mysql-0
       Waiting for mysql-0 to be Running+Ready...
t=45s: mysql-0 is Ready
       Create mysql-1 + PVC data-mysql-1
       Waiting for mysql-1 to be Running+Ready...
t=90s: mysql-1 is Ready
       Create mysql-2 + PVC data-mysql-2
       Waiting for mysql-2 to be Running+Ready...
t=130s: mysql-2 is Ready. StatefulSet is fully up.

How does a StatefulSet assign Pod names?

Pod names follow the pattern: <statefulset-name>-<ordinal>. A StatefulSet named mysql with 3 replicas creates pods namedmysql-0, mysql-1, and mysql-2. This name is permanent — it does not change if the pod crashes, restarts, or is rescheduled to a different node. The ordinal is always a zero-based integer.

What is stable Pod identity?

Stable Pod identity is the guarantee that a pod's name, DNS hostname, and storage binding remain the same across its entire lifetime — including crashes, restarts, and node changes. For mysql-1: its name is always mysql-1, its DNS is alwaysmysql-1.mysql-headless.default.svc.cluster.local, and its storage is alwaysdata-mysql-1. Nothing changes when the pod restarts.

Why is stable identity important?

Databases configure replication by pointing to a specific hostname. MySQL replica is configured: “replicate from mysql-0.mysql-headless”. If that hostname changes (as it would with a Deployment), the replica loses its source and replication breaks. Kafka brokers register themselves with ZooKeeper or KRaft by their hostname — a changed hostname looks like a brand-new broker, not a restarted one, and partition ownership becomes inconsistent. In distributed systems, identity is how nodes find each other and maintain cluster state.

What is ordinal indexing in StatefulSet?

The ordinal is the integer suffix in a pod name: 0 for mysql-0, 1 for mysql-1, etc. The ordinal controls three things:

  • Creation order: pods are created from ordinal 0 upward, one at a time
  • Deletion order: pods are deleted from the highest ordinal downward
  • Storage naming: PVC name = <template-name>-<pod-name>, so ordinal determines which PVC each pod gets

Init containers can extract the ordinal from the pod hostname using:
[[ $(hostname) =~ -([0-9]+)$ ]] && ordinal=$${BASH_REMATCH[1]}
and use it to decide whether to configure the pod as primary (ordinal 0) or replica (ordinal > 0).

How does StatefulSet manage storage?

StatefulSet uses volumeClaimTemplates in its spec — a template that describes a PVC. When the controller creates pod N, it first checks whether a PVC named<template-name>-<statefulset-name>-N already exists. If it does, the PVC is reattached. If it does not, a new PVC is created from the template. PVCs are never automatically deleted when a pod is removed.

What are volumeClaimTemplates?

volumeClaimTemplates is a list inside the StatefulSet spec that defines the PVC template each pod should receive. It looks exactly like a PVC definition — storage class, access mode, size — but instead of creating one PVC, Kubernetes creates one PVC per pod using this template as the blueprint.

volumeClaimTemplates example
volumeClaimTemplates:
- metadata:
    name: data          # PVC names: data-mysql-0, data-mysql-1, data-mysql-2
  spec:
    accessModes: ["ReadWriteOnce"]   # one node at a time — required for databases
    storageClassName: gp3-encrypted  # your cloud storage class
    resources:
      requests:
        storage: 100Gi

How does StatefulSet create PersistentVolumeClaims automatically?

When the StatefulSet controller is about to create pod N, it checks whether PVCdata-mysql-N already exists. If not, it creates it using the volumeClaimTemplatesspec. The cloud provider's CSI driver (EBS CSI, GCE PD CSI, Azure Disk CSI) then provisions an actual disk and creates a PersistentVolume that binds to the PVC. The pod starts only after the PVC is Bound.

How does StatefulSet recover after a Pod failure?

The StatefulSet controller detects the pod is gone (or not Ready). It creates a new pod with the same name(same ordinal). The new pod references the same PVC by name — so the same disk is reattached. If the pod was on a different node, the cloud volume detaches from the old node and reattaches to the new one (this takes 30–90 seconds for cloud storage). Once attached, the database process starts and resumes from exactly where it left off — no data loss, no reconfiguration needed.

How does StatefulSet perform rolling updates?

StatefulSet rolling updates go in reverse ordinal order: highest ordinal first. For a 3-replica MySQL: mysql-2 is updated first, then mysql-1, then mysql-0 (the primary, last). Each pod must be Running AND Ready before the next one is updated. This way, replicas are validated on the new version before the primary is touched.

StatefulSet rolling update
# Update image
kubectl set image statefulset/mysql mysql=mysql:8.1 -n production

# Watch the update (reverse ordinal):
# mysql-2 → deleted → recreated with mysql:8.1 → must be Ready
# mysql-1 → deleted → recreated with mysql:8.1 → must be Ready
# mysql-0 → deleted → recreated with mysql:8.1 → must be Ready

kubectl rollout status statefulset/mysql -n production

How does StatefulSet scale?

Scaling up adds pods in ordinal order. If you scale from 3 to 5, mysql-3 is created and must reach Ready before mysql-4 is created. PVCs are created for each new pod. Scaling down removes pods in reverse ordinal order (highest first), but keeps their PVCs.

StatefulSet scaling
# Scale up to 5
kubectl scale statefulset mysql --replicas=5 -n production
# Creates: mysql-3 (waits for Ready), then mysql-4

# Scale down to 2
kubectl scale statefulset mysql --replicas=2 -n production
# Deletes: mysql-4 (waits for termination), then mysql-3, then mysql-2
# Keeps:   PVCs data-mysql-2, data-mysql-3, data-mysql-4
StatefulSet vs Deployment — startup timeline
STATEFULSET scale-up: 0  3  (Sequential  OrderedReady)
────────────────────────────────────────────────────────────────────
t=  0s   Create mysql-0  +  PVC data-mysql-0
          [██████████████████████ initializing... ]
t= 45s   mysql-0  Running + Ready
           Create mysql-1  +  PVC data-mysql-1
          [██████████████████████ initializing... ]
t= 90s   mysql-1  Running + Ready
           Create mysql-2  +  PVC data-mysql-2
          [██████████████████████ initializing... ]
t=130s   mysql-2  Running + Ready     StatefulSet fully up

DEPLOYMENT scale-up: 0  3  (Parallel)
────────────────────────────────────────────────────────────────────
t=  0s   Create pod-abc  +  pod-xyz  +  pod-def  (all at once)
          [██████ ] [██████ ] [██████ ]
t= 30s   All 3 pods  Running + Ready     Deployment fully up

StatefulSet: ~130s   (safe  primary ready before replicas start)
Deployment:   ~30s   (fast  no ordering dependencies between pods)

Scale-down reverse order:  mysql-2  mysql-1  mysql-0
PVCs kept after scale-down: data-mysql-0, data-mysql-1, data-mysql-2

How does StatefulSet scale down?

Scale-down deletes pods from the highest ordinal downward, one at a time. Each pod must fully terminate before the next one is deleted. PVCs are never deletedduring scale-down — they are retained so data can be recovered if you scale back up. The pod at ordinal 0 is always the last to be removed.

When should you use a StatefulSet?

  • Your application writes data to local disk that must survive pod restarts
  • Each instance needs its own dedicated storage (not shared with other instances)
  • Other pods need to address a specific pod by a stable hostname
  • Pods must start in a specific order (e.g., primary before replicas)
  • Pod identity must survive restarts (Kafka broker ID, Elasticsearch node name, etc.)

Use it for: PostgreSQL, MySQL, MongoDB, Kafka, Elasticsearch, Redis Cluster, ZooKeeper.

When should you avoid using a StatefulSet?

  • Your application is stateless — StatefulSet adds sequential startup overhead for no benefit
  • You do not need per-pod storage — you are paying for N PVCs unnecessarily
  • You need fast, parallel scale-out — StatefulSet's sequential startup slows scaling
  • You are running a simple dev environment — a Deployment is simpler and faster

Using StatefulSet for a stateless app is like renting an apartment when you need a hotel — more expensive, more complex, and none of the benefits apply.

Deployment vs StatefulSet

What is the difference between Deployment and StatefulSet?

FeatureDeploymentStatefulSet
Manages pods viaReplicaSetStatefulSet Controller (direct)
Pod namesRandom suffix (pod-7d4b9c-abc12)Ordered ordinal (pod-0, pod-1, pod-2)
Pod identityAnonymous — pods are fungiblePermanent — ordinal survives restarts
DNS per podNo (shared Service IP)Yes (via headless service)
Storage per podShared PVC or noneDedicated PVC per pod (volumeClaimTemplates)
PVC on pod deletePVC lifecycle separate from podPVC kept, reattached on pod restart
PVC on scale-downN/A (shared)PVC kept — not deleted
Startup orderAll pods simultaneouslySequential: 0 → 1 → 2, each waits for Ready
Shutdown orderNewest dies firstHighest ordinal first: N → 0
Rolling updatemaxSurge + maxUnavailableReverse ordinal, one at a time
Service requiredRegular ClusterIPHeadless Service (+ optional ClusterIP)
Use caseWeb, API, workers, ML inferenceDBs, Kafka, Elasticsearch, Redis Cluster

Which one should you use for stateless applications?

Deployment. It is designed exactly for this. Pods start in parallel (fast scale-out), updates use maxSurge/maxUnavailable (configurable speed vs availability tradeoff), and HPA integrates seamlessly. No storage overhead, no headless service, no sequential startup delays.

Which one should you use for stateful applications?

StatefulSet. If your application writes to disk and that data must survive restarts, or if each instance needs its own storage, or if pods must start in order — use StatefulSet. For production databases, consider adding a Kubernetes Operator on top of the StatefulSet to handle automated failover, backups, and replica management.

Can a Deployment replace a StatefulSet?

No. There are three things a Deployment fundamentally cannot do that StatefulSet can:

  • Give each pod a permanent name that survives restarts
  • Give each pod its own dedicated PVC (via volumeClaimTemplates)
  • Guarantee ordered sequential startup (pod N-1 must be Ready before pod N starts)

You can work around some of these with custom controllers or operators, but then you have re-implemented StatefulSet. Use the right tool.

Can a StatefulSet replace a Deployment?

Technically yes, but it is the wrong choice for stateless apps. StatefulSet starts pods sequentially (much slower scale-out), creates PVCs you do not need (wasted cost), and requires a headless service (unnecessary complexity). You also lose the clean rolling update / rollback mechanism of Deployment. StatefulSet adds overhead that provides zero benefit for stateless applications.

Why is StatefulSet not used for every application?

  • Sequential startup is slow: 10 pods × 30 seconds each = 5 minutes to start. A Deployment starts all 10 simultaneously.
  • PVCs cost money: every pod gets a dedicated disk. For stateless apps this is pure waste.
  • Headless service is required: extra configuration that stateless apps do not need.
  • Complexity: more moving parts means more to debug and maintain.

Why is Deployment preferred for APIs and web servers?

APIs and web servers are stateless by design. Every request is independent. Any pod can handle any request. Deployment gives you fast parallel startup (critical for traffic spikes), simple HPA integration, clean rolling updates with maxSurge/maxUnavailable, and no storage or headless service overhead. It is the simplest tool that solves the problem correctly.

Why is StatefulSet preferred for databases?

Databases need all three things StatefulSet provides: stable identity (so replicas always know where the primary is), per-pod storage (so each replica keeps its own data copy), and ordered startup (so the primary is healthy before replicas try to connect). A Deployment gives none of these — and running a database without them causes data loss, replication failures, or both.

Storage

Can Deployments use PVCs?

Yes. You add a PVC to a Deployment pod template under spec.volumes, referencing an existing PVC by name. All pods in the Deployment share the same PVC — they all read and write to the same disk. This works for shared read-only data (config files, ML model weights) but is unsuitable for databases.

Deployment shared PVC
# Deployment with a shared PVC (all pods mount the same volume)
spec:
  template:
    spec:
      volumes:
      - name: shared-data
        persistentVolumeClaim:
          claimName: shared-pvc   # ONE PVC shared by all pods

# All 3 pods mount the SAME disk. Fine for read-only.
# For databases: two pods writing to the same disk = corruption.

If Deployments can use PVCs, why do we need StatefulSets?

Deployment gives you one shared PVC for all pods. Every pod reads and writes the same disk. StatefulSet gives you one dedicated PVC per pod. Each pod has exclusive access to its own disk.

For a database, shared storage is not an option. MySQL pod A and MySQL pod B cannot both write to the same data directory — they would corrupt each other's files immediately. Each replica needs its own disk containing its own copy of the data. Only StatefulSet volumeClaimTemplatesprovides this — Deployment has no equivalent.

How does storage differ between Deployment and StatefulSet?

storage comparison
DEPLOYMENT (shared storage):
  Pod A ─────────────────┐
  Pod B ─────────────────┤── shared-pvc (one disk, all pods)
  Pod C ─────────────────┘
   Same disk. All pods read and write the same data.

STATEFULSET (per-pod storage):
  mysql-0 ──── data-mysql-0 (its own 100Gi disk)
  mysql-1 ──── data-mysql-1 (its own 100Gi disk)
  mysql-2 ──── data-mysql-2 (its own 100Gi disk)
   Each pod has exclusive access to its own data.

What happens to storage when a Deployment Pod is recreated?

If the Deployment uses a shared PVC, the new pod mounts the same PVC — nothing is lost on the shared disk. If the Deployment uses no PVC (only emptyDir or container filesystem), all in-container data is gone. The new pod starts fresh. For a stateless application this is fine — there is nothing valuable to lose locally.

What happens to storage when a StatefulSet Pod is recreated?

The StatefulSet controller creates a new pod with the same name (e.g., mysql-1). The new pod binds to the existing PVC (data-mysql-1) — the same disk that the old pod was using. All data the old pod wrote to that disk is still there. The database process picks up exactly where it left off. If the pod moved to a different node, the cloud volume detaches from the old node and reattaches to the new one (usually 30–90 seconds).

Can multiple StatefulSet Pods share one PVC?

No — at least not with ReadWriteOnce (RWO) access mode, which is the correct mode for databases. RWO means only one node can mount the volume at a time. If two pods on different nodes tried to mount the same RWO PVC, the second one would fail to start. Even with ReadWriteMany (RWX), sharing a PVC between database pods causes data corruption — two database processes writing to the same directory simultaneously destroys the data structures.

Why can't multiple PostgreSQL replicas share one PVC?

PostgreSQL uses a data directory (/var/lib/postgresql/data) with a specific file format — WAL files, heap files, index files, and control files. The PostgreSQL process holds file locks on these files. If two PostgreSQL processes mount the same data directory simultaneously, they both try to lock the same files and both try to write WAL logs. The result is immediate data corruption — the files become inconsistent and the database cannot recover without a full restore. This is not a Kubernetes limitation; it is fundamental to how PostgreSQL (and all databases) work.

Identity & Networking

What is Pod identity?

Pod identity is the combination of properties that uniquely and permanently identify a pod: its name (mysql-1), its DNS hostname(mysql-1.mysql-headless.default.svc.cluster.local), and its storage binding(data-mysql-1). In a StatefulSet, all three survive pod restarts. In a Deployment, none of them survive — every restart creates a new random name, new IP, clean disk.

Why does Pod identity matter?

Distributed systems maintain cluster state by tracking node identities. When a MySQL replica restarts, the primary needs to know it is the same node resuming — not a new node joining. Kafka brokers register their broker ID (derived from their hostname) with the cluster metadata. Elasticsearch tracks shard assignments by node name. If the node identity changes on restart, the cluster cannot correlate the returning node with its stored state — it must treat it as a new member, which triggers expensive rebalancing or leaves the cluster in an inconsistent state.

What is stable DNS?

Stable DNS means a pod's DNS hostname never changes, even if the pod crashes, restarts, or moves to a different node. When a StatefulSet pod restarts on a new IP address (10.244.2.50 instead of 10.244.1.30), its DNS record is updated automatically. Other pods that reference it by hostname (mysql-1.mysql-headless) continue to resolve to the new IP without any reconfiguration.

How does StatefulSet provide stable DNS?

StatefulSet requires a headless service. When Kubernetes creates a pod in a StatefulSet, it registers a DNS A record for the pod in the format:<pod-name>.<service-name>.<namespace>.svc.cluster.local. This DNS record is updated whenever the pod's IP changes. The hostname stays constant — only the IP behind it changes.

per-pod DNS records
# DNS records created by StatefulSet (mysql) + headless service (mysql-headless):
mysql-0.mysql-headless.default.svc.cluster.local  10.244.1.5
mysql-1.mysql-headless.default.svc.cluster.local  10.244.2.3
mysql-2.mysql-headless.default.svc.cluster.local  10.244.3.8

# mysql-1 crashes, restarts on new IP:
mysql-1.mysql-headless.default.svc.cluster.local  10.244.2.99  (updated automatically)
# Other pods still use the same hostname → they resolve the new IP transparently

What is a Headless Service?

A Headless Service is a Kubernetes Service with clusterIP: None. A regular Service creates a virtual IP (ClusterIP) that load-balances traffic across all matching pods. A Headless Service creates no virtual IP — instead, DNS queries for the service return the list of pod IPs directly, and individual pod DNS records (pod-name.service-name.namespace.svc.cluster.local) are created for each pod.

headless service definition
apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
  namespace: production
spec:
  clusterIP: None        # This is what makes it headless
  selector:
    app: mysql
  ports:
  - port: 3306
Headless Service — DNS flow vs regular Service
REGULAR SERVICE (clusterIP: 10.96.100.1)
──────────────────────────────────────────────────────────────────
Client ──▶ mysql-service:3306 ──┬──▶ mysql-0  10.244.1.5
                                ├──▶ mysql-1  10.244.2.3  (load-balanced, any pod)
                                └──▶ mysql-2  10.244.3.8
            Cannot address mysql-0 specifically. Useless for replication config.

HEADLESS SERVICE (clusterIP: None)
──────────────────────────────────────────────────────────────────
 mysql-0.mysql-headless.default.svc.cluster.local    10.244.1.5
 mysql-1.mysql-headless.default.svc.cluster.local    10.244.2.3  (per-pod DNS records)
 mysql-2.mysql-headless.default.svc.cluster.local    10.244.3.8

Replica config: CHANGE MASTER TO MASTER_HOST='mysql-0.mysql-headless';

mysql-0 crashes  restarts on new IP 10.244.1.99:
  DNS auto-updates: mysql-0.mysql-headless  10.244.1.99
  Replica reconnects via same hostname  gets new IP  replication resumes
  Zero manual reconfiguration.

Why does StatefulSet require a Headless Service?

StatefulSet pods must be reachable by their individual hostname (e.g., mysql-1.mysql-headless) so that the primary can replicate to a specific replica, and so that replicas can connect to the primary by a stable address. A regular ClusterIP service load-balances across all pods — you cannot target a specific pod. A headless service creates per-pod DNS records that resolve directly to individual pod IPs, enabling pod-to-pod communication by stable hostname.

The StatefulSet spec has a required serviceName field that must match the headless service name. Without it, per-pod DNS records are not created.

Why doesn't a Deployment need a Headless Service?

Deployment pods are anonymous — no other component needs to address a specific pod by name. A Deployment's client sends a request to the Service IP, and the Service load-balances to any pod. The client does not care which pod handles the request. This is the correct design for stateless apps. Per-pod DNS records are unnecessary overhead for an anonymous, interchangeable pod set.

How do Pods communicate in a StatefulSet?

Pods in a StatefulSet communicate via the headless service DNS. A replica connecting to the primary uses: mysql-0.mysql-headless.default.svc.cluster.local:3306. When mysql-0 restarts with a new IP, the DNS record updates automatically — the replica reconnects to the same hostname and gets the new IP. No manual reconfiguration is needed.

Scaling

How does Deployment scale Pods?

Deployment scaling changes spec.replicas. The ReplicaSet Controller reconciles immediately: when scaling up, it creates all needed pods simultaneously; when scaling down, it deletes from newest to oldest. HPA can automate this based on CPU, memory, or custom metrics. Scale-out is fast because pods are created in parallel.

How does StatefulSet scale Pods?

StatefulSet scaling is sequential by default (podManagementPolicy: OrderedReady). When scaling from 3 to 5: mysql-3 is created first and must be Running AND Ready beforemysql-4 is created. Each new pod also gets a new PVC created. This makes StatefulSet scale-out significantly slower than Deployment.

Why does StatefulSet create Pods sequentially?

Ordered creation is essential for databases with primary-replica relationships. The primary (mysql-0) must be fully initialized and accepting connections before the first replica (mysql-1) starts — because the replica immediately connects to the primary to begin data synchronization. If the replica starts first, it tries to connect to a non-existent or still-initializing primary, which causes errors or a broken replication state.

Why does StatefulSet delete Pods in reverse order?

Reverse-order deletion during scale-down ensures replicas stop before the primary. If the primary (mysql-0) were stopped first, replicas would lose their replication source mid-operation and enter an error state. Stopping from highest to lowest ordinal means: all replicas stop first, then the primary stops last — the cluster drains cleanly.

Why does Deployment create Pods in parallel?

Deployment pods are stateless and have no dependencies between them. Pod A does not need Pod B to be ready before it can start. They are all equivalent. Parallel startup is simply faster — if you need 10 pods and they all start in parallel, you are ready in 30 seconds. Sequential startup would take 5 minutes. For stateless apps, there is no reason to be sequential.

Why does Deployment delete Pods randomly (newest first)?

The ReplicaSet Controller uses a heuristic: when scaling down, kill the newest pod first. The reasoning: older pods have warmer caches, more established connections, and have proven they can survive under load. A pod running for 2 hours is more stable than one running for 3 minutes. When capacity must be reduced, the least-proven pod is the best choice to remove. This is not random — it is age-based, newest first.

Updates

How does a Deployment perform rolling updates?

Deployment creates a new ReplicaSet for the updated pod spec, then gradually scales the new RS up while scaling the old RS down. The maxSurge field controls how many extra pods can exist during the transition, and maxUnavailable controls how many pods can be down. New and old pods run simultaneously during the rollout. The rollout is complete when the old RS is at 0.

How does StatefulSet perform rolling updates?

StatefulSet updates pods in reverse ordinal order, one at a time. The highest-ordinal pod is deleted and recreated with the new spec first, then the next, down to ordinal 0. Each pod must be Running AND Ready before the next is updated. The primary (ordinal 0) is the last pod to be updated.

Using partition: N in the rolling update strategy lets you update only pods with ordinal ≥ N, enabling canary releases on a single replica before rolling out to all.

Why are StatefulSet updates sequential?

For databases, it is critical to validate the new version on a replica before updating the primary. If the new image has a bug and mysql-2 crashes after update, mysql-0 (the primary) is still on the old, stable version — you can roll back without any data risk. If the primary were updated first and it crashed, the entire cluster would lose its write leader. Sequential reverse-ordinal updates are a risk-management strategy.

Can StatefulSet perform rollbacks?

Yes. kubectl rollout undo statefulset/<name> reverts the pod spec to the previous version. The rollback applies the previous spec in reverse ordinal order — exactly like a forward update. Unlike Deployment, there is no previous ReplicaSet to scale up — Kubernetes re-applies the old spec by updating pods in order.

StatefulSet rollback
# Rollback StatefulSet
kubectl rollout undo statefulset/mysql -n production

# Check history
kubectl rollout history statefulset/mysql -n production

# Rollback to specific revision
kubectl rollout undo statefulset/mysql --to-revision=2 -n production

How does rollback differ between Deployment and StatefulSet?

Deployment rollback: fast. The old ReplicaSet still exists at 0 replicas with the old pod spec. Rollback scales the old RS back up and the current RS down. Old pods restart from a cached image.

StatefulSet rollback: slower. There is no separate RS to switch back to. Kubernetes re-applies the old spec by updating pods one at a time in reverse ordinal order — the same sequential process as a forward update. Expect full rollout time, not instant RS swap.

Failure Scenarios

What happens if a Deployment Pod crashes?

The ReplicaSet Controller detects the pod is gone (actual count dropped below desired). It creates a new pod immediately — with a new random name, a new IP, and a clean start. Any in-container data from the crashed pod is gone. The new pod picks up from code, not from any local state. For a stateless application, this is exactly correct — the pod restores from external state (database, cache).

What happens if a StatefulSet Pod crashes?

The StatefulSet controller detects the pod is missing. It creates a new pod with the same ordinal name(e.g., mysql-1). The new pod references the existing PVC (data-mysql-1), which is reattached. The database process starts, reads its data from the PVC, and resumes. Other pods in the cluster continue using the same DNS hostname — they transparently reconnect to the new pod IP when the old one goes offline.

What happens if the node hosting a Deployment Pod fails?

The node goes NotReady. After ~40 seconds, pod status becomes Unknown. After the node.kubernetes.io/unreachable toleration expires (default ~5 minutes), the pod is evicted. The ReplicaSet Controller creates a new pod on a healthy node. Because Deployment pods are stateless, the new pod is fully functional immediately. Total recovery time: ~5–6 minutes.

What happens if the node hosting a StatefulSet Pod fails?

Same eviction sequence (node NotReady → pod Unknown → eviction after ~5 min). After eviction, the StatefulSet controller creates mysql-1 on a healthy node. The cloud volume (data-mysql-1) must detach from the failed node and reattach to the new node — this takes 30–90 seconds for most cloud providers. Total recovery: ~6–10 minutes.

If the storage is hostPath (local to the node), the pod is rescheduled back to the same nodeby Kubernetes topology constraints. If the node is permanently dead, the pod stays Pending forever. This is why local storage is not safe for production StatefulSet databases.

What happens if a PVC is deleted?

If the PVC backing a StatefulSet pod is deleted while the pod is running, Kubernetes blocks the deletion (a finalizer is added to the PVC). The PVC stays in Terminating state until the pod is stopped. When the pod is eventually deleted and recreated, the StatefulSet controller creates a new empty PVCwith the same name. The new pod starts with a blank disk. All previous data is lost. If the underlying PV had reclaimPolicy: Retain, the PV can be manually rebound.

What happens if a StatefulSet is deleted?

Pods are deleted in reverse ordinal order: mysql-2mysql-1mysql-0. Each pod waits for full termination before the next is deleted.PVCs are kept by default — they are not deleted when the StatefulSet is deleted. You must manually delete PVCs if you want to free the storage. In Kubernetes 1.27+, you can configure persistentVolumeClaimRetentionPolicy.whenDeleted: Deleteto auto-delete PVCs.

What happens if a Deployment is deleted?

The Deployment controller deletes the ReplicaSet, which deletes all pods. If the Deployment used a shared PVC, that PVC is not deleted — PVC lifecycle is independent of the Deployment. You must manually delete the PVC if you want to remove the storage. Deleting a Deployment is safe for stateless apps — nothing valuable is lost.

Database-Specific Questions

Can PostgreSQL run inside a Deployment?

A single-instance PostgreSQL can technically run as a Deployment with a PVC. But this is unsafe for two reasons: (1) if the pod is evicted and rescheduled to a different node, the PVC must reattach — this works with cloud storage but takes time and leaves a window of data unavailability; (2) if you ever try to add a replica (primary + standby), each replica must have its own disk — which requires StatefulSet. For anything beyond a single throwaway dev instance, use StatefulSet.

Why is PostgreSQL usually deployed using StatefulSet?

PostgreSQL in production runs as a primary with one or more streaming replicas. Each replica connects to the primary using a specific hostname and continuously streams WAL logs to stay in sync. If the primary pod restarts with a new name, replicas lose their source and replication breaks. Each replica also stores its own copy of the data — per-pod PVCs are required. StatefulSet provides both: stable primary hostname and dedicated per-pod storage.

Why is MySQL deployed using StatefulSet?

Same reasons as PostgreSQL. In a MySQL primary-replica setup, replicas are configured with:CHANGE MASTER TO MASTER_HOST='mysql-0.mysql-headless'. This hostname must be stable. If the primary pod restarts as a Deployment pod with a new name, the MASTER_HOST is invalid and replication fails. StatefulSet ensuresmysql-0 is always mysql-0, regardless of how many times it restarts.

Why is MongoDB deployed using StatefulSet?

MongoDB Replica Set members discover each other and vote for a primary using stable hostnames. Each member is initialized with: rs.add("mongo-1.mongo-headless:27017"). If a member pod restarts with a new hostname (as with a Deployment), the replica set treats it as a new unknown member and does not grant it voting rights. This can leave the replica set without quorum and unable to elect a primary. StatefulSet preserves hostnames across restarts.

Why is Apache Kafka deployed using StatefulSet?

Each Kafka broker has a broker ID — a unique integer that is stored on disk in the meta.properties file. Partition leadership and replica assignment are tracked by broker ID. If a broker restarts and gets a different hostname (Deployment behavior), it reads a different broker ID from disk — or finds no disk at all (if PVC is not attached) — and appears as a new broker. The cluster must rebalance partition assignments, which causes minutes of degraded performance or data inconsistency. StatefulSet ensures the broker always gets the same hostname and the same disk.

Why does database replication require stable identities?

Database replication is configured by pointing one server at another by hostname or IP. The replica maintains a persistent connection to the primary and continuously streams changes. If the primary's hostname or IP changes (as happens when a Deployment pod restarts), the replica loses its connection and cannot reconnect — it does not know the new address. In clustered databases (MongoDB Replica Set, Kafka, Elasticsearch), nodes track each other by hostname in persistent configuration stored in etcd or on disk. A changed hostname breaks cluster membership tracking and can cause split-brain or loss of quorum.

Why is stable networking important for distributed databases?

Distributed databases run consensus algorithms (Raft, Paxos, Zab) that require a majority of known nodes to agree. Each node in the quorum is identified by its hostname. If node identities change, the quorum loses track of which nodes are which — it cannot distinguish a returning node (should be accepted with its existing data) from a brand-new node (should start from scratch). A wrong identity means the cluster may reject a legitimate node, lose quorum, and stop accepting writes entirely. Stable networking is what makes the cluster self-healing rather than requiring manual intervention on every restart.

Production Best Practices

How should PostgreSQL be deployed in production?

Use a Kubernetes Operator (CloudNativePG, Crunchy Data PGO, Zalando postgres-operator) rather than a raw StatefulSet. Operators add automated failover, streaming replica management, backup scheduling, and connection pooling on top of the StatefulSet primitives. If using a raw StatefulSet: 3 replicas, cloud-backed PVCs (gp3, pd-ssd), pod anti-affinity across nodes and zones, PodDisruptionBudget (minAvailable: 2), Guaranteed QoS (requests == limits), and a dedicated write service pointing to the primary.

production mysql StatefulSet — all best practices
# production-mysql-statefulset.yaml  (all best practices in one file)
apiVersion: v1
kind: Service
metadata:
  name: mysql-headless          # headless — per-pod DNS records
  namespace: production
spec:
  clusterIP: None
  selector:
    app: mysql
  ports:
  - port: 3306
---
apiVersion: v1
kind: Service
metadata:
  name: mysql-write             # ClusterIP — points to primary only
  namespace: production
spec:
  selector:
    app: mysql
    role: primary               # label set by init container on mysql-0
  ports:
  - port: 3306
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: mysql-pdb
  namespace: production
spec:
  minAvailable: 2               # always keep 2 of 3 replicas up during drain
  selector:
    matchLabels:
      app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: production
spec:
  serviceName: mysql-headless   # must match headless service name
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: mysql
            topologyKey: kubernetes.io/hostname   # no two replicas on same node
      containers:
      - name: mysql
        image: mysql:8.0
        resources:
          requests:
            cpu: "2"
            memory: 8Gi
          limits:
            cpu: "2"            # requests == limits → Guaranteed QoS (evicted last)
            memory: 8Gi
        readinessProbe:
          exec:
            command: ["mysqladmin", "ping", "-h", "127.0.0.1"]
          initialDelaySeconds: 30
          periodSeconds: 10
          failureThreshold: 3
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: data                # PVCs: data-mysql-0, data-mysql-1, data-mysql-2
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: gp3-encrypted    # cloud-backed, encrypted, fast
      resources:
        requests:
          storage: 100Gi

Should you use StatefulSet for a single PostgreSQL instance?

Yes, in production — even for a single instance. A StatefulSet with replicas: 1gives you stable pod identity and PVC reattachment if the pod moves to a different node. A Deployment with a PVC technically works too, but you lose the guarantee that the same PVC is reattached on reschedule (Deployment does not control PVC binding). For development or CI, Deployment is fine — it is simpler and faster to set up.

Should you use Deployment for development databases?

Yes, for local development or ephemeral CI environments where you do not care about data persistence. A Deployment is simpler: no headless service, no volumeClaimTemplates, faster startup. Just use an emptyDir volume — the database starts fresh every time the pod restarts, which is fine for tests. Never use this pattern in staging or production.

What are common mistakes when deploying databases on Kubernetes?

  • Running as Deployment: pod crashes → new name → PVC not reattached → data loss.
  • Using hostPath or emptyDir: pod moves to new node → data gone.
  • No anti-affinity: two replicas on same node → node failure = total data loss.
  • No PodDisruptionBudget: node drain during upgrade evicts all DB pods simultaneously → cluster goes down.
  • requests ≠ limits: Burstable QoS → evicted before your API servers under node memory pressure.
  • No Operator: raw StatefulSet requires manual failover when primary crashes — could take minutes in production.

What are the production best practices for StatefulSets?

  • Use cloud-backed StorageClass (EBS gp3, GCE pd-ssd, Azure managed-csi) — not hostPath
  • Set requests == limits for memory on database containers (Guaranteed QoS)
  • Add podAntiAffinity across nodes and cloud zones
  • Create a PodDisruptionBudget: minAvailable: N/2 + 1 (quorum)
  • Use a Headless Service for DNS + a ClusterIP service for writes + a ClusterIP service for reads
  • Use an Operator for Day-2 operations (failover, backup, user management)
  • Test PVC expansion (resize) and restore (from backup) before you need it in production

What are the production best practices for Deployments?

  • Always set a readiness probe — without it, pods receive traffic before the app is ready
  • Set resource requests and limits on all containers
  • Add a preStop: exec: sleep 5 hook to let iptables rules propagate before SIGTERM
  • Handle SIGTERM gracefully — drain in-flight requests before exiting
  • Set revisionHistoryLimit to keep the last 3–5 ReplicaSets for rollback
  • Use HPA with custom metrics (request rate, queue depth) rather than just CPU

Complete YAML Examples

Show me a complete Deployment YAML with all best practices.

nginx-deployment.yaml
# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-api
  namespace: production
  labels:
    app: nginx-api
spec:
  replicas: 3
  revisionHistoryLimit: 5          # keep last 5 ReplicaSets for rollback
  selector:
    matchLabels:
      app: nginx-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                  # at most 4 pods at once during update
      maxUnavailable: 0            # always keep 3 pods available (zero-downtime)
  template:
    metadata:
      labels:
        app: nginx-api
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
        readinessProbe:            # traffic only sent after this passes
          httpGet:
            path: /healthz
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
          failureThreshold: 3
        livenessProbe:             # pod restarted if this fails
          httpGet:
            path: /healthz
            port: 80
          initialDelaySeconds: 15
          periodSeconds: 20
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sleep", "5"]   # drain connections before SIGTERM

Show me the Service YAML for a Deployment.

nginx-service.yaml + hpa.yaml
# nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-api
  namespace: production
spec:
  selector:
    app: nginx-api              # sends traffic to all Deployment pods
  ports:
  - name: http
    port: 80
    targetPort: 80
  type: ClusterIP               # internal access only
                                # use LoadBalancer or Ingress for external access
---
# Optional: HPA for automatic scaling
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nginx-api-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx-api
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Show me a complete StatefulSet YAML with all three service files.

mysql: 3 services + pdb + statefulset
# mysql-headless-service.yaml
# MANDATORY for StatefulSet — creates per-pod DNS records
apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
  namespace: production
spec:
  clusterIP: None               # this is what makes it headless
  selector:
    app: mysql
  ports:
  - name: mysql
    port: 3306
    targetPort: 3306
---
# mysql-write-service.yaml
# Points ONLY to the primary pod (labeled role: primary by init container)
apiVersion: v1
kind: Service
metadata:
  name: mysql-write
  namespace: production
spec:
  selector:
    app: mysql
    role: primary               # only pods with this label receive write traffic
  ports:
  - name: mysql
    port: 3306
    targetPort: 3306
  type: ClusterIP
---
# mysql-read-service.yaml
# Load-balances across ALL pods (primary + replicas are all readable)
apiVersion: v1
kind: Service
metadata:
  name: mysql-read
  namespace: production
spec:
  selector:
    app: mysql                  # all pods — reads can go to any
  ports:
  - name: mysql
    port: 3306
    targetPort: 3306
  type: ClusterIP
---
# mysql-pdb.yaml
# Prevents draining more than 1 pod at a time during node maintenance
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: mysql-pdb
  namespace: production
spec:
  minAvailable: 2               # always keep 2 of 3 replicas up
  selector:
    matchLabels:
      app: mysql
---
# mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: production
spec:
  serviceName: mysql-headless   # must match headless service above
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  updateStrategy:
    type: RollingUpdate          # updates reverse ordinal: mysql-2 → mysql-1 → mysql-0
  template:
    metadata:
      labels:
        app: mysql
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: mysql
            topologyKey: kubernetes.io/hostname    # no two replicas on same node
      containers:
      - name: mysql
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: root-password
        resources:
          requests:
            cpu: "2"
            memory: 8Gi
          limits:
            cpu: "2"            # requests == limits → Guaranteed QoS (evicted last)
            memory: 8Gi
        readinessProbe:
          exec:
            command: ["mysqladmin", "ping", "-h", "127.0.0.1"]
          initialDelaySeconds: 30
          periodSeconds: 10
          failureThreshold: 3
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: data                # creates: data-mysql-0, data-mysql-1, data-mysql-2
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: gp3-encrypted
      resources:
        requests:
          storage: 100Gi

PostgreSQL: How postgres-0 Becomes Primary

How does Kubernetes decide which pod is primary and which are replicas?

Kubernetes does not decide this automatically. Nothing in the StatefulSet or Kubernetes API knows what a “primary” or “replica” is. You teach it to the pods using the ordinal number embedded in the pod hostname.

The convention — used by every PostgreSQL, MySQL, and MongoDB StatefulSet in production — is:

  • Ordinal 0 (postgres-0) = primary. It is created first, initializes a fresh data directory, and starts accepting writes.
  • Ordinal 1, 2, N (postgres-1, postgres-2) = replicas. They start after the primary is Ready, copy data from the primary, and begin streaming replication.

This logic runs inside an init container — a container that runs to completion before the main database container starts. The init container reads the pod hostname, extracts the ordinal, and branches: if ordinal is 0, configure as primary; otherwise, configure as replica.

What does the init container do for postgres-0 (primary)?

For ordinal 0, the init container initializes a brand-new PostgreSQL data directory using initdband writes the configuration needed for replication: wal_level = replica,max_wal_senders = 10, and the replication user entry in pg_hba.conf.

init-primary.sh
# init container script — runs BEFORE the postgres container starts
#!/bin/bash
set -e

# Extract ordinal from hostname. postgres-0 → ordinal=0, postgres-2 → ordinal=2
[[ $(hostname) =~ -([0-9]+)$ ]] && ordinal=${BASH_REMATCH[1]}

PGDATA=/var/lib/postgresql/data

if [[ ${ordinal} -eq 0 ]]; then
  echo "==> ordinal 0: configuring as PRIMARY"

  # Only initialize if data directory is empty (first start)
  if [ -z "$(ls -A ${PGDATA})" ]; then
    gosu postgres initdb -D ${PGDATA}

    # Enable WAL streaming for replicas
    cat >> ${PGDATA}/postgresql.conf << EOF
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
hot_standby = on
synchronous_commit = local
EOF

    # Allow replicas to connect for replication
    echo "host replication replicator 0.0.0.0/0 md5" >> ${PGDATA}/pg_hba.conf
  fi

  echo "==> PRIMARY config done. Starting postgres..."
fi

What does the init container do for postgres-1 and postgres-2 (replicas)?

For ordinal > 0, the init container runs pg_basebackup — a PostgreSQL utility that copies the entire data directory from the primary over the network. This gives the replica a consistent starting snapshot. After the copy, the init container writes a standby.signal file (PostgreSQL 12+) and a primary_conninfo setting that tells the replica exactly where to stream WAL logs from: postgres-0.postgres-headless — the stable DNS hostname of the primary.

init-replica.sh
# init container script — continued for replicas (ordinal > 0)
else
  echo "==> ordinal ${ordinal}: configuring as REPLICA"

  # Only run base backup if data directory is empty
  if [ -z "$(ls -A ${PGDATA})" ]; then
    PRIMARY_HOST="postgres-0.postgres-headless.default.svc.cluster.local"

    echo "==> Waiting for primary at ${PRIMARY_HOST}..."
    # Retry until primary is ready (it might still be initializing)
    until pg_isready -h ${PRIMARY_HOST} -U postgres; do
      sleep 3
    done

    echo "==> Running pg_basebackup from primary..."
    pg_basebackup       -h ${PRIMARY_HOST}       -U replicator       -D ${PGDATA}       --wal-method=stream       --write-recovery-conf       -P

    # PostgreSQL 12+: signal file tells postgres to start in standby mode
    touch ${PGDATA}/standby.signal

    # Tell replica WHERE to stream WAL from (the primary's stable DNS)
    cat >> ${PGDATA}/postgresql.conf << EOF
primary_conninfo = 'host=${PRIMARY_HOST} port=5432 user=replicator application_name=$(hostname)'
recovery_target_timeline = 'latest'
EOF
  fi

  echo "==> REPLICA config done. Starting postgres..."
fi

Show me the complete PostgreSQL StatefulSet YAML with primary/replica init containers.

postgres StatefulSet — primary/replica with init containers
# postgres-headless-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
  namespace: production
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
  - port: 5432
---
# postgres-write-service.yaml  (points to primary only via label selector)
apiVersion: v1
kind: Service
metadata:
  name: postgres-write
  namespace: production
spec:
  selector:
    app: postgres
    role: primary
  ports:
  - port: 5432
  type: ClusterIP
---
# postgres-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: production
spec:
  serviceName: postgres-headless
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      initContainers:
      - name: init-postgres
        image: postgres:15
        command: ["/bin/bash", "/scripts/init.sh"]
        env:
        - name: PGDATA
          value: /var/lib/postgresql/data
        - name: PGPASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: replication-password
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
        - name: scripts
          mountPath: /scripts
      containers:
      - name: postgres
        image: postgres:15
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: root-password
        - name: PGDATA
          value: /var/lib/postgresql/data
        ports:
        - containerPort: 5432
        resources:
          requests:
            cpu: "2"
            memory: 8Gi
          limits:
            cpu: "2"            # Guaranteed QoS
            memory: 8Gi
        readinessProbe:
          exec:
            command: ["pg_isready", "-U", "postgres"]
          initialDelaySeconds: 20
          periodSeconds: 10
          failureThreshold: 6
        livenessProbe:
          exec:
            command: ["pg_isready", "-U", "postgres"]
          initialDelaySeconds: 60
          periodSeconds: 20
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
      volumes:
      - name: scripts
        configMap:
          name: postgres-init-scripts   # ConfigMap holding the init.sh script
          defaultMode: 0755
  volumeClaimTemplates:
  - metadata:
      name: data                # creates: data-postgres-0, data-postgres-1, data-postgres-2
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: gp3-encrypted
      resources:
        requests:
          storage: 100Gi

What happens if postgres-0 (the primary) crashes? Does postgres-1 automatically become the new primary?

No — not with a raw StatefulSet. When postgres-0 crashes, Kubernetes restarts it on the same or a new node and reattaches the same PVC (data-postgres-0). postgres-0 comes back as primary again because its init container sees ordinal 0 and finds an existing (non-empty) data directory, so it skips re-initialization and postgres starts normally.

postgres-1 and postgres-2 stay as replicas the whole time — they just pause streaming during the downtime and reconnect when postgres-0 comes back. There is no automatic promotion. This means: if postgres-0's disk is permanently destroyed, your cluster has no primary and the application cannot write. That window of unavailability is why production setups use an operator with leader election.

What is Patroni and how does it automate primary/replica failover?

Patroni is a Python-based high-availability agent for PostgreSQL. Every pod runs Patroni as the main process (not postgres directly — Patroni manages the postgres process). Patroni uses a distributed consensus store (etcd, ZooKeeper, or Kubernetes API itself) to runleader election.

  • On startup, all Patroni instances race to acquire a distributed lock. The winner becomes primary and starts postgres in read-write mode.
  • Losers become replicas and stream from the elected primary.
  • If the primary's Patroni loses the lock (pod crash, network partition), remaining Patroni instances immediately hold a new election. The winner promotes itself to primary in seconds — no human intervention.
  • Ordinal 0 is NOT always the primary with Patroni. Any pod can be elected primary based on which one wins the lock.
Patroni leader election — any pod can become primary
# How Patroni handles primary/replica — no ordinal dependency
postgres-0    Patroni running    won the etcd lock    PRIMARY (postgres in R/W)
postgres-1    Patroni running    lost the lock         replica (streaming from postgres-0)
postgres-2    Patroni running    lost the lock         replica (streaming from postgres-0)

postgres-0 crashes:
  postgres-1 Patroni: "lock is free, I'll take it"  promotes to PRIMARY (~10s)
  postgres-2 Patroni: lost the race                 replica (now streams from postgres-1)
  postgres-0 restarts: sees postgres-1 holds lock   rejoins as replica

# In production: use CloudNativePG or Zalando postgres-operator
# Both use Patroni under the hood and add backup, monitoring, user management

Summary: three ways to handle PostgreSQL primary/replica in Kubernetes

ApproachHow primary is chosenAuto failover?Use in
Init container + ordinalOrdinal 0 is always primaryNo — Kubernetes restarts itSimple setups, learning
Patroni (bare)Leader election via etcd/ZK/k8sYes (~10s)Production (self-managed)
CloudNativePG OperatorOperator controls electionYes (~5s)Production (recommended)
Zalando postgres-operatorPatroni inside each podYes (~10s)Production (alternative)

Connecting Your App to a StatefulSet Database

How does my application connect to a StatefulSet database?

Your app connects via the ClusterIP services — not the headless service. The headless service is for pod-to-pod DNS inside the StatefulSet itself (replication config). Your app should use two separate services: one for writes (points only to the primary) and one for reads (load-balances across all pods).

which service your app should use
# Three services your app talks to:

# 1. Writes — only reaches the primary (labeled role: primary)
POSTGRES_WRITE_HOST=postgres-write.production.svc.cluster.local

# 2. Reads — load-balances across primary + all replicas
POSTGRES_READ_HOST=postgres-read.production.svc.cluster.local

# 3. Headless — DO NOT use from your app
#    postgres-headless is for StatefulSet pods to find each other
#    DNS returns individual pod IPs, not a stable load-balanced address

Show me connection string examples for Node.js, Python, and Go.

connection strings — Node.js / Python / Go
// Node.js  separate read and write pools (mysql2 / pg library)
const writePool = mysql.createPool({ host: 'mysql-write', port: 3306, database: 'mydb' });
const readPool  = mysql.createPool({ host: 'mysql-read',  port: 3306, database: 'mydb' });

// writes go to primary
await writePool.query('INSERT INTO orders VALUES (?)', [data]);
// reads go to any replica
const rows = await readPool.query('SELECT * FROM orders WHERE id = ?', [id]);

---

# Python — SQLAlchemy with separate engines
from sqlalchemy import create_engine

write_engine = create_engine("postgresql://user:pass@postgres-write:5432/mydb")
read_engine  = create_engine("postgresql://user:pass@postgres-read:5432/mydb",
                              pool_size=10, max_overflow=20)

---

// Go  database/sql with two connections
writeDB, _ := sql.Open("postgres", "host=postgres-write port=5432 dbname=mydb")
readDB,  _ := sql.Open("postgres", "host=postgres-read  port=5432 dbname=mydb")

What is connection pooling and why does every production database need it?

PostgreSQL can handle a limited number of simultaneous connections (default: 100). If you have 50 API pods each opening 5 connections, that is 250 connections — your database is at capacity and new requests fail. Connection pooling puts a proxy (PgBouncer) between your app and the database. Apps connect to PgBouncer (thousands of connections allowed), and PgBouncer maintains a small pool (10–30 connections) to PostgreSQL — reusing idle connections instead of opening new ones.

why connection pooling matters
WITHOUT pooling:
50 API pods × 5 connections = 250 connections  PostgreSQL at limit, errors

WITH PgBouncer:
50 API pods × 5 connections  PgBouncer (250 client connections)
                             PostgreSQL (20 server connections, reused)

PgBouncer sits as a separate Deployment in your cluster:
  App  pgbouncer-service:5432  postgres-write:5432 (PostgreSQL primary)

Show me a PgBouncer Deployment YAML that sits in front of PostgreSQL.

pgbouncer deployment + config + service
# pgbouncer-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: pgbouncer-config
  namespace: production
data:
  pgbouncer.ini: |
    [databases]
    mydb = host=postgres-write port=5432 dbname=mydb

    [pgbouncer]
    listen_addr     = 0.0.0.0
    listen_port     = 5432
    auth_type       = md5
    auth_file       = /etc/pgbouncer/userlist.txt
    pool_mode       = transaction       # recommended for most apps
    max_client_conn = 1000              # total app connections allowed
    default_pool_size = 20             # connections PgBouncer keeps open to PostgreSQL
    server_idle_timeout = 600
    log_connections = 0
    log_disconnections = 0

  userlist.txt: |
    "myapp" "md5<hashed-password>"
---
# pgbouncer-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgbouncer
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pgbouncer
  template:
    metadata:
      labels:
        app: pgbouncer
    spec:
      containers:
      - name: pgbouncer
        image: bitnami/pgbouncer:1.21.0
        ports:
        - containerPort: 5432
        resources:
          requests:
            cpu: 100m
            memory: 64Mi
          limits:
            cpu: 500m
            memory: 128Mi
        volumeMounts:
        - name: config
          mountPath: /etc/pgbouncer
      volumes:
      - name: config
        configMap:
          name: pgbouncer-config
---
# pgbouncer-service.yaml  ← your app connects HERE, not to postgres directly
apiVersion: v1
kind: Service
metadata:
  name: pgbouncer
  namespace: production
spec:
  selector:
    app: pgbouncer
  ports:
  - port: 5432
  type: ClusterIP

PodManagementPolicy

What is PodManagementPolicy in StatefulSet?

podManagementPolicy controls how StatefulSet creates and deletes pods during scale operations. There are two values:

  • OrderedReady (default): pods are created one at a time in ordinal order (0→1→2), each waiting for Ready before the next starts. Deleted in reverse (2→1→0).
  • Parallel: all pods are created and deleted simultaneously, like a Deployment. No ordering, no waiting for Ready between pods.

This setting only affects scale operations. Rolling updates always go in reverse ordinal order regardless of this setting.

podManagementPolicy options
# OrderedReady (default) — for databases with primary/replica hierarchy
apiVersion: apps/v1
kind: StatefulSet
spec:
  podManagementPolicy: OrderedReady   # default, can be omitted
  # mysql-0 starts → Ready → mysql-1 starts → Ready → mysql-2 starts

---
# Parallel — for peer databases where all nodes are equal
apiVersion: apps/v1
kind: StatefulSet
spec:
  podManagementPolicy: Parallel
  # All pods start simultaneously — faster scale-out

When should I use PodManagementPolicy: Parallel?

Use Parallel when your StatefulSet pods are peers with no hierarchy — no pod needs to wait for another to be ready before it starts. In these systems, each pod independently discovers the cluster and joins it. Sequential startup adds unnecessary delay without any correctness benefit.

  • Apache Cassandra: every node is equal, any node can handle any request. Ring topology — nodes discover each other via seed nodes, not by waiting for ordinal 0.
  • Elasticsearch: initial cluster formation can happen in parallel. Nodes elect a master among themselves via leader election, not by ordinal.
  • Scylla DB: same as Cassandra (compatible protocol).

Which databases use Parallel vs OrderedReady?

DatabasePolicyWhy
PostgreSQLOrderedReadyPrimary (ordinal 0) must be ready before replicas start replication
MySQLOrderedReadySame — primary/replica setup requires ordered initialization
MongoDBOrderedReadyReplica set initialized from ordinal 0, others join after
Apache KafkaOrderedReadyBroker 0 holds metadata; others should join after cluster is stable
Apache CassandraParallelAll nodes are equal peers; seed-based discovery, no ordering needed
ElasticsearchParallelNodes elect master themselves; parallel startup is safe and faster
Scylla DBParallelCassandra-compatible ring topology
Redis ClusterOrderedReadySlots are assigned after all primaries are up; sequential is safer

Monitoring & Alerting

How do I check if my StatefulSet is healthy using kubectl?

kubectl health check commands
# 1. Top-level health check — READY column must match replicas
kubectl get statefulset -n production
# NAME    READY   AGE
# mysql   3/3     5d    ← healthy
# kafka   2/3     5d    ← DEGRADED — one pod is down

# 2. Check individual pod status
kubectl get pods -l app=mysql -n production
# NAME      READY   STATUS             RESTARTS   AGE
# mysql-0   1/1     Running            0          5d   ← healthy
# mysql-1   1/1     Running            0          5d   ← healthy
# mysql-2   0/1     CrashLoopBackOff   12         2h   ← PROBLEM

# 3. See what's wrong with the bad pod
kubectl describe pod mysql-2 -n production   # look at Events section at the bottom
kubectl logs mysql-2 -n production           # see the crash reason

# 4. Check PVC binding
kubectl get pvc -l app=mysql -n production
# NAME            STATUS   VOLUME           CAPACITY
# data-mysql-0    Bound    pvc-abc...       100Gi    ← ok
# data-mysql-1    Bound    pvc-def...       100Gi    ← ok
# data-mysql-2    Pending  <none>           <none>   ← PVC not bound = pod stuck

# 5. Real-time event stream
kubectl get events --sort-by=.lastTimestamp -n production | grep -i mysql

What does a stuck or degraded StatefulSet look like?

diagnosing a stuck StatefulSet
SYMPTOM                    LIKELY CAUSE                    HOW TO DIAGNOSE
──────────────────────────────────────────────────────────────────────────────
READY: 2/3                 Pod crashing or not ready       kubectl logs <pod>
                                                            kubectl describe pod <pod>

Pod stuck in Pending       PVC not bound                   kubectl describe pvc
                           No node has capacity            kubectl describe pod  Events
                           Affinity can't be satisfied     check node labels/taints

Pod stuck in               Volume attachment timeout       kubectl describe pod  Events
ContainerCreating          (cloud disk detach/reattach     "timeout waiting for volume"
                           from old node taking too long)

Pod in                     DB process failing to start     kubectl logs <pod>
CrashLoopBackOff           wrong config, PVC has corrupt   kubectl logs <pod> --previous
                           data, OOMKilled                 kubectl describe pod  Last State

Pod stuck in               Finalizer blocking deletion     kubectl describe pod  Finalizers
Terminating                PVC still mounted elsewhere     kubectl get pvc
                           App not handling SIGTERM        kubectl delete pod --grace-period=0 (last resort)

StatefulSet update         Pod not reaching Ready          check readiness probe
stuck at 2/3               (rolling update blocked)        kubectl rollout status sts/mysql

What Prometheus metrics should I monitor for a StatefulSet database?

Prometheus metrics to watch
# Kubernetes-level metrics (from kube-state-metrics)
kube_statefulset_replicas                        # desired replicas
kube_statefulset_replicas_ready                  # currently ready
kube_statefulset_replicas_current                # currently running
kube_pod_container_status_restarts_total         # restart count per container
kubelet_volume_stats_used_bytes                  # disk used per PVC
kubelet_volume_stats_capacity_bytes              # disk capacity per PVC

# Alert: replicas not matching desired
kube_statefulset_replicas_ready != kube_statefulset_replicas

# Alert: disk over 80% full
kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes > 0.8

# PostgreSQL-specific (from postgres_exporter)
pg_up                                            # 1 = reachable, 0 = down
pg_replication_lag                               # seconds behind primary (on replicas)
pg_database_size_bytes                           # database disk usage
pg_stat_activity_count                           # active connections
pg_locks_count                                   # lock count (high = contention)

# MySQL-specific (from mysqld_exporter)
mysql_up
mysql_slave_status_seconds_behind_master         # replication lag
mysql_global_status_threads_connected            # active connections

Show me a PrometheusRule YAML with StatefulSet alerts.

PrometheusRule — StatefulSet + PostgreSQL alerts
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: statefulset-database-alerts
  namespace: production
  labels:
    release: prometheus   # must match your Prometheus Operator selector
spec:
  groups:
  - name: statefulset.rules
    rules:

    - alert: StatefulSetNotFullyReady
      expr: kube_statefulset_replicas_ready != kube_statefulset_replicas
      for: 5m
      labels:
        severity: critical
      annotations:
        summary: "StatefulSet {{ $labels.statefulset }} is degraded"
        description: "{{ $labels.statefulset }} has {{ $value }} ready pods, expected {{ $labels.replicas }}"

    - alert: StatefulSetPodCrashLooping
      expr: rate(kube_pod_container_status_restarts_total{pod=~".*-[0-9]+"}[15m]) > 0.1
      for: 5m
      labels:
        severity: critical
      annotations:
        summary: "Pod {{ $labels.pod }} is crash-looping"

    - alert: PVCDiskUsageCritical
      expr: (kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) > 0.9
      for: 5m
      labels:
        severity: critical
      annotations:
        summary: "PVC {{ $labels.persistentvolumeclaim }} is over 90% full  expand now"

    - alert: PVCDiskUsageWarning
      expr: (kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) > 0.8
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "PVC {{ $labels.persistentvolumeclaim }} is over 80% full"

    - alert: PostgreSQLReplicationLagHigh
      expr: pg_replication_lag > 30
      for: 2m
      labels:
        severity: warning
      annotations:
        summary: "PostgreSQL replica {{ $labels.instance }} is {{ $value }}s behind primary"

    - alert: PostgreSQLDown
      expr: pg_up == 0
      for: 1m
      labels:
        severity: critical
      annotations:
        summary: "PostgreSQL instance {{ $labels.instance }} is down"

Backup & Restore

What are the three main ways to back up a StatefulSet database?

MethodHow it worksPoint-in-time?Best for
pg_dump CronJobSQL dump via pg_dump, uploaded to S3No — snapshot onlySmall DBs < 50 GB
WAL-G / pgBackRestContinuous WAL streaming to S3Yes — any point in timeLarge DBs, production
Velero + CSI snapshotsPVC volume snapshot at infrastructure levelNo — snapshot onlyAny database, fast restore

Show me a pg_dump CronJob YAML that uploads to S3 nightly.

postgres-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: postgres-backup
  namespace: production
spec:
  schedule: "0 2 * * *"          # 2:00 AM every day
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: backup
            image: postgres:15
            command:
            - /bin/bash
            - -c
            - |
              set -e
              DATE=$(date +%Y-%m-%d-%H%M)
              FILENAME=mydb-${DATE}.sql.gz

              echo "Starting backup at ${DATE}..."
              pg_dump -h postgres-write -U postgres mydb | gzip > /tmp/${FILENAME}

              echo "Uploading to S3..."
              aws s3 cp /tmp/${FILENAME} s3://my-backup-bucket/postgres/${FILENAME}

              echo "Backup complete: ${FILENAME}"
              # Verify the backup is readable
              gunzip -t /tmp/${FILENAME} && echo "Backup file is valid"
            env:
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: root-password
            - name: AWS_DEFAULT_REGION
              value: us-east-1
            # AWS credentials via IRSA (IAM Roles for Service Accounts) — no hardcoded keys
          serviceAccountName: postgres-backup-sa  # IRSA: this SA has S3 write permission

What is WAL-G and why is it better than pg_dump for large databases?

pg_dump takes a logical snapshot — it reads every row and dumps SQL. For a 500 GB database this takes hours. If you run pg_dump at 2am and the database crashes at 2pm, you lose 12 hours of data. WAL-G continuously streams Write-Ahead Logs (WAL) to S3 as they are generated — giving you point-in-time recovery (PITR) to any second in the past.

WAL-G — continuous archiving to S3
# How WAL-G works:
postgres-0 writes data  WAL log generated  WAL-G streams to S3 (every 60s)

# Restore to any point in time:
# "restore to 14:35:22 yesterday"
WAL-G restore-base  latest base backup from S3
WAL-G replay WAL    apply logs up to target time  database at exact state

# WAL-G config (environment variables for the postgres container):
WALG_S3_PREFIX=s3://my-backup-bucket/walg
WALG_COMPRESSION_METHOD=brotli
AWS_REGION=us-east-1
WALG_DELTA_MAX_STEPS=6          # keep 6 delta backups between full backups
PGDATA=/var/lib/postgresql/data

# Run full base backup weekly (add to postgres container command or init container):
# wal-g backup-push ${PGDATA}

# Add to postgresql.conf to enable WAL archiving:
# archive_mode = on
# archive_command = 'wal-g wal-push %p'
# archive_timeout = 60

How do I test a database restore? (The step everyone skips)

A backup you have never tested is not a backup — it is a hope. The standard pattern is to run a weekly restore test as a CronJob in a separate namespace. It restores the latest backup to a temporary PostgreSQL pod, runs SQL integrity checks, then deletes itself.

weekly restore test CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
  name: postgres-restore-test
  namespace: backup-testing         # isolated namespace, no impact on production
spec:
  schedule: "0 4 * * 0"            # 4:00 AM every Sunday
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          initContainers:
          - name: restore
            image: postgres:15
            command:
            - /bin/bash
            - -c
            - |
              # Restore latest backup from S3
              aws s3 cp s3://my-backup-bucket/postgres/ /tmp/                 --recursive --exclude "*" --include "*.sql.gz"                 --no-sign-request
              LATEST=$(ls -t /tmp/*.sql.gz | head -1)
              echo "Restoring: ${LATEST}"
              gunzip -c ${LATEST} | psql -h localhost -U postgres mydb
          containers:
          - name: verify
            image: postgres:15
            command:
            - /bin/bash
            - -c
            - |
              # Run integrity checks
              ROW_COUNT=$(psql -h localhost -U postgres -t -c "SELECT COUNT(*) FROM orders;")
              echo "Orders table row count: ${ROW_COUNT}"
              if [ "${ROW_COUNT}" -lt 1000 ]; then
                echo "ERROR: row count too low  backup may be corrupt"
                exit 1
              fi
              echo "Restore test PASSED"

Common Misconceptions

Is StatefulSet only for databases?

No. StatefulSet is for any application that needs stable pod identity, per-pod storage, or ordered lifecycle. This includes: distributed coordination services (ZooKeeper), message brokers (Kafka), search engines (Elasticsearch), caching clusters (Redis Cluster), and even some ML training workloads where each worker owns a shard of data. Databases are the most common example, but they are not the only one.

Can a Deployment be stateful?

Technically, a Deployment can use a PVC for shared storage — so it has some persistence. But it is not “stateful” in the way the term is used in the context of Kubernetes workloads. True statefulness requires per-pod storage, stable identity, and ordered lifecycle — none of which Deployment provides. A Deployment with a shared PVC is better called “a stateless app with shared storage”, not a stateful workload.

Can a StatefulSet be stateless?

Technically yes — you can run nginx in a StatefulSet. It will work. But it is wasteful: sequential startup slows your rollouts, PVCs cost money you do not need to spend, and the headless service adds configuration complexity for no benefit. StatefulSet for a stateless app is over-engineering. Use Deployment.

Does StatefulSet automatically replicate database data?

No. This is the most important misconception to clear up. StatefulSet manages pod identity, pod ordering, and storage binding. It does not replicate data between pods. Data replication is the responsibility of the database software itself: MySQL Group Replication, PostgreSQL streaming replication, Kafka's ISR (In-Sync Replicas), MongoDB Replica Set protocol. StatefulSet creates the environment where replication can work correctly — it does not perform the replication.

Does StatefulSet guarantee data persistence?

StatefulSet guarantees that PVCs are retained across pod restarts and scale-down events. It does not guarantee the underlying storage is durable. If the PV's reclaimPolicy is Delete and the PVC is accidentally deleted, the disk is gone. If you use hostPath and the node dies permanently, the data is gone. Use cloud-backed storage with reclaimPolicy: Retain and a backup strategy for real persistence guarantees.

Does Deployment always lose data?

No. A Deployment with a cloud-backed shared PVC retains data across pod restarts — the PVC is not deleted when a pod is replaced. However, the new pod gets the same shared disk as all other pods, not a pod-specific disk. For stateless apps where no per-pod state is needed, Deployment does not lose any relevant data because there is no per-pod data to lose.

Can a Deployment have stable storage?

Yes — shared stable storage. All Deployment pods mount the same PVC. What Deployment cannot have is per-pod stable storage — each pod having its own dedicated PVC that persists and reattaches across restarts. That requires StatefulSet volumeClaimTemplates.

Is a Headless Service mandatory for every StatefulSet?

Technically: it is required by the StatefulSet spec (serviceName must reference an existing service). But you can create a headless service with an empty selector, effectively making it do nothing. Practically: if your StatefulSet pods need to discover each other by hostname (most databases, Kafka, Elasticsearch), the headless service is essential. If your StatefulSet pods are completely independent and never communicate with each other (an unusual case), the headless service is a formality with no functional impact.

Interview Questions

These are the 15 questions that come up most in Kubernetes interviews — from junior to senior level. Read the question. Try to answer it yourself first. Then read the answer below.

15 Interview Questions — Read the Question First, Then the Answer

Q1: Explain Deployment vs StatefulSet.

Answer: Deployment is for stateless apps — pods are anonymous, interchangeable, start in parallel, and a crashed pod is replaced with a new random name. StatefulSet is for stateful apps — each pod has a permanent ordinal name (mysql-0, mysql-1), its own PVC via volumeClaimTemplates, a stable DNS hostname via a headless service, and ordered sequential startup. Use Deployment for APIs and workers. Use StatefulSet for databases, Kafka, and Elasticsearch.

Common wrong answer: "StatefulSet has storage, Deployment does not." Both can use PVCs. The difference is PER-POD storage and stable identity.

Q2: Why was StatefulSet introduced?

Answer: Deployment's core assumption is that pods are anonymous and interchangeable. When engineers tried to run databases as Deployments, crashed pods came back with new names, breaking replica replication. Per-pod storage was impossible (only shared PVCs). Replicas started before primaries were ready. StatefulSet was introduced in Kubernetes 1.5 (stable 1.9) to give pods stable identity, per-pod PVCs, and ordered lifecycle — the three things databases need that Deployment cannot provide.

Common wrong answer: "Deployment cannot use storage." It can. The problem was per-pod storage and stable identity.

Q3: Can PostgreSQL be deployed as a Deployment?

Answer: A single-instance PostgreSQL can run as a Deployment with a PVC, but it is unsafe in production. The moment you need a replica (primary + standby), you need per-pod storage and stable hostname — which requires StatefulSet. Even for a single instance, StatefulSet is safer because it guarantees the same PVC is reattached on pod reschedule, which Deployment does not guarantee.

Common wrong answer: "No, Deployment cannot use storage." It can — but only shared storage, not per-pod storage.

Q4: Why is StatefulSet recommended for databases?

Answer: Three reasons: (1) Stable identity — a crashed pod comes back with the same name and hostname, so replica replication source config stays valid. (2) Per-pod PVCs — each replica has its own disk with its own data copy, preventing corruption. (3) Ordered startup — primary (ordinal 0) starts first and becomes Ready before replicas start, preventing replicas from connecting to a non-existent primary.

Common wrong answer: "Because databases need storage." That's only part of it. Stable identity and ordered startup are equally important.

Q5: What is stable identity?

Answer: Stable identity is the guarantee that a StatefulSet pod's name, DNS hostname, and storage binding remain the same across its entire lifetime — including crashes, restarts, and rescheduling to different nodes. For mysql-1: name is always mysql-1, DNS is always mysql-1.mysql-headless.default.svc.cluster.local, and storage is always data-mysql-1. None of these change on restart.

Common wrong answer: "Stable identity means the pod always runs on the same node." No — the pod can move to any node. The identity (name, DNS, storage) stays constant, not the node.

Q6: Why is stable identity important?

Answer: Distributed databases track cluster membership by node hostname. A MySQL replica is configured to replicate from a specific hostname — if that hostname changes on restart, replication breaks and must be manually reconfigured. Kafka stores broker IDs by hostname. MongoDB tracks replica set members by hostname. Elasticsearch assigns shard ownership by node name. In all these systems, an identity change on restart forces the cluster to treat the node as a stranger, triggering expensive rebalancing or manual intervention.

Common wrong answer: "It's just for convenience, so you know which pod is which." It's a correctness requirement, not cosmetic.

Q7: What is a Headless Service?

Answer: A Headless Service is a Kubernetes Service with clusterIP: None. A regular Service creates a virtual IP and load-balances traffic across pods. A Headless Service creates no virtual IP — instead it creates individual DNS A records for each pod: pod-name.service-name.namespace.svc.cluster.local. StatefulSet uses a headless service so each pod has its own stable DNS hostname that other pods can use to address it directly.

Common wrong answer: "A Headless Service has no endpoints." It has endpoints — it just has no virtual IP. DNS queries return pod IPs directly.

Q8: Why does StatefulSet need a Headless Service?

Answer: StatefulSet pods need to be addressable by individual hostname — mysql-1 must reach mysql-0 directly by name to configure replication. A regular ClusterIP service load-balances to any pod, so you cannot target mysql-0 specifically. A headless service creates per-pod DNS records (mysql-0.mysql-headless, mysql-1.mysql-headless, etc.) that resolve directly to individual pod IPs. The StatefulSet spec's serviceName field must reference this headless service, which is what triggers per-pod DNS record creation.

Common wrong answer: "StatefulSet needs a headless service so the pods can be accessed from outside the cluster." No — headless service is for pod-to-pod communication within the cluster, not external access.

Q9: Can a Deployment use PVCs?

Answer: Yes. You can add a PVC reference under spec.template.spec.volumes in a Deployment. All pods in the Deployment share the same PVC. This works for shared read-only data or shared file systems where concurrent reads are safe. It does not work for databases because two database processes cannot safely write to the same data directory simultaneously.

Common wrong answer: "No, only StatefulSet can use PVCs." Deployment can use PVCs — just not per-pod, dedicated PVCs.

Q10: If Deployment can use PVCs, why does StatefulSet exist?

Answer: Deployment gives you ONE shared PVC for all pods. StatefulSet gives you ONE PVC per pod via volumeClaimTemplates. This is the core difference. For a database, shared storage causes corruption — two MySQL processes cannot write to the same data directory. Each replica must have exclusive access to its own disk. Additionally, StatefulSet provides stable pod names and ordered startup — both impossible with Deployment.

Common wrong answer: "StatefulSet is just Deployment with storage." StatefulSet adds three things Deployment cannot do: per-pod storage, stable identity, and ordered lifecycle.

Q11: What happens when a StatefulSet Pod crashes?

Answer: The StatefulSet controller creates a new pod with the SAME name (same ordinal). The new pod references the same PVC — the disk is reattached. The database process starts on the new pod and resumes from the data on the reattached disk. Other pods continue pointing to the same DNS hostname, which now resolves to the new pod's IP. No reconfiguration is needed. Data is preserved.

Common wrong answer: "A new pod is created with a new name." That's Deployment behavior. StatefulSet always recreates with the same ordinal name.

Q12: What happens when a Deployment Pod crashes?

Answer: The ReplicaSet Controller creates a new pod with a random name and a fresh start. Any local in-container data from the crashed pod is lost. For a stateless application this is correct — there is nothing valuable to lose locally. The new pod reads state from an external database or cache and begins handling traffic immediately.

Common wrong answer: "Deployment tries to restart the same pod." No — ReplicaSet creates an entirely new pod object with a new name. The crashed pod is gone.

Q13: How does StatefulSet handle scaling?

Answer: Scale-up: pods are created one at a time in ordinal order. mysql-3 must be Running AND Ready before mysql-4 is created. Each new pod gets a new PVC created automatically. Scale-down: pods are deleted one at a time in reverse ordinal order. mysql-4 is deleted first, then mysql-3. PVCs are kept — never deleted automatically. You can set podManagementPolicy: Parallel to skip ordering (useful for some distributed systems like Cassandra).

Common wrong answer: "StatefulSet scales all pods simultaneously." That is Deployment/Parallel behavior. Default StatefulSet scaling is sequential.

Q14: How does Deployment handle scaling?

Answer: Scale-up: all new pods are created simultaneously. No ordering. All pods start in parallel. Scale-down: newest pods are deleted first. The ReplicaSet Controller maintains the pod count. HPA can automate scaling based on CPU, memory, or custom metrics. Scaling is fast because there are no ordering dependencies between pods.

Common wrong answer: "Deployment scales by creating pods on the same node as existing pods." No — Scheduler places pods on any eligible node based on resources and constraints.

Q15: Which workloads should use Deployment, and which should use StatefulSet?

Answer: Use Deployment for: any app where pods are anonymous and interchangeable — web servers, REST APIs, GraphQL services, background workers, ML inference, microservices that read/write to an external database. Use StatefulSet for: any app where pod identity, per-pod storage, or ordered startup matters — PostgreSQL, MySQL, MongoDB, Apache Kafka, Elasticsearch, Redis Cluster, ZooKeeper. Simple rule: if replacing a crashed pod with a brand-new fresh pod (different name, clean disk) would break your application, use StatefulSet.

Common wrong answer: "Stateful apps should use StatefulSet, but stateless apps can also use it for safety." StatefulSet for stateless apps is unnecessary complexity with real cost: slower scaling, wasted PVCs, extra headless service.

Decision tree — Deployment vs StatefulSet
SHOULD I USE DEPLOYMENT OR STATEFULSET?
═══════════════════════════════════════════════════════════════════════════════

 Start ──▶ Does each pod need its own EXCLUSIVE disk with individual data?
                          
             YES ─────────┴──────── NO
                                    
                                    
        STATEFULSET           Does pod identity (stable name/hostname)
                                matter to other services?
                                         
                              YES ───────┴──────── NO
                                                   
                                                   
                         STATEFULSET          Do pods need to start
                                               in a specific ORDER?
                                                      
                                          YES ────────┴──────── NO
                                                                
                                                                
                                     STATEFULSET          DEPLOYMENT 

───────────────────────────────────────────────────────────────────────────────
 QUICK RULE: If replacing a crashed pod with a brand-new fresh pod
             (different name, clean disk) would break your application
              use StatefulSet.   Otherwise  use Deployment.
───────────────────────────────────────────────────────────────────────────────
 Deployment   web, API, REST, GraphQL, workers, ML inference
 StatefulSet  PostgreSQL, MySQL, MongoDB, Kafka, Elasticsearch, Redis Cluster

Key Takeaways

1.

Deployment = stateless. Pods are anonymous, parallel startup, interchangeable. Use for APIs and workers.

2.

StatefulSet = stateful. Pods have ordinal identity (mysql-0,1,2), survive restarts. Use for databases.

3.

The three things StatefulSet adds: stable identity, per-pod PVC, ordered startup/shutdown.

4.

Pod name in StatefulSet: <statefulset-name>-<ordinal>. Always the same, even after crash.

5.

Headless service (clusterIP: None) is required by StatefulSet — creates per-pod DNS records.

6.

volumeClaimTemplates creates one PVC per pod. PVCs are never auto-deleted on scale-down.

7.

StatefulSet starts pods one at a time (0 → 1 → 2). Each must be Ready before the next.

8.

StatefulSet stops pods one at a time in reverse (N → 0). Primary is always last to stop.

9.

Rolling updates go in reverse ordinal order — replicas updated before the primary.

10.

Deployment cannot replace StatefulSet. StatefulSet can technically replace Deployment but shouldn't.

11.

StatefulSet does NOT replicate database data — that is the database software's job.

12.

hostPath storage is dangerous. Use cloud-backed StorageClass (EBS, GCE PD, Azure Disk).

13.

In production, use a Kubernetes Operator on top of StatefulSet for automated failover and backups.

14.

PVCs survive StatefulSet deletion by default. Manual cleanup required.

15.

The one-sentence rule: if a fresh-pod replacement would break your app, use StatefulSet.

About the author

Ravi Kapoor

Senior DevOps Engineer & Technical Writer

CKA & AWS SA-Pro Certified9 yrs — Atlassian & FintechKubernetes open-source contributor

Ravi is a senior DevOps engineer with 9 years of experience building cloud-native infrastructure at Atlassian and multiple fintech companies. CKA and AWS Solutions Architect Professional certified, he has managed Kubernetes clusters serving millions of daily users and contributes to open-source tooling.

Related Articles