Free lesson · GenAI Platform Engineering
Deploy managed pgvector with Helm StatefulSet
Package pgvector as a Helm StatefulSet with persistent volumes, backup CronJobs, and health probes. Deploy and validate multi-tenant vector operations.
Course: AI Developer Platform Engineering · Chapter 13 · Vector DB as Platform Service
Free to read — no subscription required.
Introduction
When you add pgvector to a shared Kubernetes cluster, a standard Deployment destroys two invariants your vector store depends on: stable pod hostnames for streaming replication and persistent PVC bindings that survive pod restarts. Without those guarantees, a single pod recycle can orphan data or silently break standby replication. This lesson teaches you to deploy a production-grade pgvector instance using a Helm chart built around a StatefulSet, wire up the correct postgresql.conf tuning for ANN index workloads on a 16 GB pod, and choose a storage class that can sustain the random-IO profile vector index builds demand.
Key Terminology
volumeClaimTemplates— the StatefulSet field that provisions a dedicated PVC for each pod ordinal; when a pod is recreated, the controller rebinds it to its original PVC so data survives restarts without any manual persistent-volume management.podManagementPolicy: OrderedReady— a StatefulSet setting that serializes pod startup so each replica waits until its predecessor reachesReadybefore the controller schedules it, ensuringpgvector-0is fully initialized before any standby attempts to open a streaming replication connection.- Partition-aware rolling update — a
rollingUpdatestrategy controlled by thepartitionfield that restricts image upgrades to pod ordinals ≥partition; settingpartition: 1rolls only replicas first, giving operators a validation window before lowering to0to upgrade the primary. pg_isreadyreadiness probe — areadinessProbethat callspg_isreadyinstead of a TCP socket check; a TCP probe passes the moment the Postgres process starts, even while the instance is still replaying WAL in crash recovery, whereaspg_isreadysucceeds only once the instance genuinely accepts queries.maintenance_work_mem— a PostgreSQL parameter that controls the memory pool available for maintenance operations; HNSW index builds allocate from this pool, and the chart sets it to2GBso the graph traversal stays in RAM rather than spilling to disk, which can make index builds an order of magnitude slower at the default64MB.work_mem— a per-connection PostgreSQL parameter governing sort and hash memory; set to256MBon this pod but requires apgbouncerupstream that caps active server connections, because each simultaneous connection can independently allocate the full amount and exhaust pod RAM.
Concepts
Why StatefulSet Is Non-Negotiable for pgvector
pgvector's streaming replication model depends on two guarantees that a standard Deployment cannot provide. First, standby replicas locate the primary by hostname; the random names a Deployment assigns change on every pod recycle, silently severing the streaming connection. Second, each replica's data lives on a specific PVC bound to its ordinal. If a restarted pod binds to a fresh PVC instead of its original one, that replica must re-stream the entire dataset from scratch—or worse, comes up empty and diverges without alerting.
A StatefulSet solves both by assigning stable ordinal identities (pgvector-0, pgvector-1, …) that survive pod restarts and by ensuring each ordinal always rebinds to its original PVC via volumeClaimTemplates. These are not convenience features; they are correctness requirements for any stateful database running replication on Kubernetes. Choosing a Deployment here is not a configuration tradeoff—it destroys both invariants the database depends on.
Ordered Startup and Partition-Safe Upgrades
podManagementPolicy: OrderedReady forces the StatefulSet controller to start pgvector-0 and wait until it reports Ready before scheduling pgvector-1, and so on. Without this ordering, a replica that starts before the primary is fully initialized cannot establish a streaming replication connection and enters a crash loop. The pg_isready-based readinessProbe—rather than a plain TCP socket check—is what makes "Ready" meaningful here: a TCP socket opens the moment the Postgres process starts, but the instance may still be replaying WAL from crash recovery and will reject connections; pg_isready blocks until it genuinely accepts queries (see Code Walkthrough).
The partition field in rollingUpdate extends this discipline to live upgrades. Setting partition: 1 caps rolling updates to pod ordinals ≥ 1, so only replicas receive the new image. Once replication lag returns to zero and query health looks clean, lowering partition to 0 lets the controller upgrade the primary. This staged window is especially important when a pgvector image change touches on-disk index formats—you can catch incompatibilities on a replica before they affect the primary.
Tuning PostgreSQL Memory for ANN Index Workloads
HNSW index construction is a memory-intensive graph traversal that allocates from the maintenance_work_mem pool. At PostgreSQL's default of 64 MB, a large HNSW build spills to disk repeatedly, adding an order of magnitude to build time. Raising maintenance_work_mem to 2GB keeps the graph in RAM for the duration of the build.
The 16 GB pod budget distributes across three layers: shared_buffers = 4GB reserves a buffer cache using the standard 25 % of pod RAM rule; effective_cache_size = 10GB hints to the query planner how much OS page cache is available without being reserved; and work_mem = 256MB controls per-connection sort and hash memory. The work_mem value carries a multiplier risk—100 simultaneous connections can each allocate 256 MB independently, exhausting available RAM. This is why the chart assumes a pgbouncer connection pooler upstream: without it, the number of active server connections must be constrained by other means or work_mem lowered significantly (see Code Walkthrough).
Code Walkthrough
Now that you understand why StatefulSet's stable ordinal identity and per-pod PVCs are non-negotiable for pgvector, the following Helm chart encodes those guarantees in deployable YAML.
The chart lives at charts/pgvector-instance/. Its statefulset.yaml template captures the three decisions that diverge most from a generic Postgres deployment: ordered pod startup, per-pod volumeClaimTemplates, and a partition-aware rolling update that lets you validate replicas before touching the primary.
Code snippetyaml
1apiVersion: apps/v1 2kind: StatefulSet 3metadata: 4 name: {{ include "pgvector.fullname" . }} 5spec: 6 serviceName: {{ include "pgvector.fullname" . }}-headless 7 replicas: {{ .Values.replicaCount }} 8 podManagementPolicy: OrderedReady # replica 1 waits until primary 0 is Ready 9 updateStrategy: 10 type: RollingUpdate 11 rollingUpdate: 12 partition: {{ .Values.partition | default 0 }} 13 template: 14 spec: 15 terminationGracePeriodSeconds: 60 # pgvector needs time to flush WAL on shutdown 16 containers: 17 - name: postgres 18 image: "pgvector/pgvector:{{ .Values.image.tag }}" 19 ports: 20 - {name: pg, containerPort: 5432} 21 envFrom: 22 - secretRef: {name: {{ include "pgvector.fullname" . }}-creds} 23 volumeMounts: 24 - {name: data, mountPath: /var/lib/postgresql/data} 25 - {name: conf, mountPath: /etc/postgresql/conf.d} 26 resources: {{- toYaml .Values.resources | nindent 10 }} 27 readinessProbe: 28 exec: {command: ["pg_isready", "-U", "platform", "-d", "platform"]} 29 initialDelaySeconds: 10 30 periodSeconds: 5 31 volumeClaimTemplates: 32 - metadata: {name: data} 33 spec: 34 accessModes: [ReadWriteOnce] 35 storageClassName: {{ .Values.storageClass }} 36 resources: 37 requests: 38 storage: {{ .Values.storage }}
podManagementPolicy: OrderedReady ensures pgvector-0 is Ready before any replica attempts to stream from it. Set partition: 1 when upgrading a minor pgvector version so only replicas roll first; once validated, lower it to 0 to promote the primary upgrade. The readinessProbe uses pg_isready rather than a TCP socket check because a TCP probe will pass while Postgres is still in crash recovery — pg_isready blocks until the instance actually accepts queries.
The second artifact is the postgresql.conf ConfigMap mounted into conf.d/. ANN index builds (especially HNSW) draw heavily from maintenance_work_mem; the OLTP defaults force disk-spilling and can make index builds an order of magnitude slower.
Code snippetini
1# 16 GB pod, ~12 GB usable for Postgres 2shared_buffers = 4GB 3effective_cache_size = 10GB 4work_mem = 256MB # per-connection; cap active connections upstream 5maintenance_work_mem = 2GB # HNSW index build allocates from this pool 6max_parallel_maintenance_workers = 4 7max_parallel_workers_per_gather = 4 8 9wal_level = replica 10max_wal_senders = 4 11wal_keep_size = 2GB 12hot_standby = on 13 14shared_preload_libraries = 'pgvector'
shared_buffers = 4GB follows the standard 25 % of pod RAM ratio; higher values rarely help and starve the OS page cache. work_mem = 256MB is safe only when paired with a pgbouncer upstream that caps active connections — at 100 simultaneous connections, uncapped work_mem can exceed the pod's total RAM.
Verify by running helm install pgvector ./charts/pgvector-instance --set replicaCount=3 and confirming that kubectl get pods -l app=pgvector shows pgvector-0, pgvector-1, and pgvector-2 in Running state, each with a bound PVC reported by kubectl get pvc.
Do's and Don'ts
Having just walked through the chart manifests and postgresql.conf tuning, the following rules distil the failure modes most likely to bite when you ship this StatefulSet to a shared cluster.
Do's
- ✓Do set
podManagementPolicy: OrderedReadyand pair it with apg_isreadyreadiness probe — this combination ensures replicas never attempt streaming replication beforepgvector-0is fully accepting queries; a TCP socket probe passes during crash recovery and will allow replica startup against a not-yet-ready primary. - ✓Do set
maintenance_work_mem = 2GBin yourpostgresql.confConfigMap for HNSW index builds — HNSW construction allocates from this pool at build time, and leaving it at the OLTP default forces disk-spilling that can make index builds an order of magnitude slower on a 16 GB pod. - ✓Do use
partition: 1in therollingUpdatestrategy when upgrading a pgvector minor version — settingpartition: 1rolls only replicas first, letting you validate streaming replication health before the primary is touched; lower it to0only after replica validation passes.
Don'ts
- ✗Don't replace the
StatefulSetwith aDeploymentfor pgvector — aDeploymentprovides neither stable pod ordinals (required for streaming replication endpoints) nor per-podvolumeClaimTemplates, so a single pod recycle can orphan PVC bindings or silently break standby replication. - ✗Don't raise
work_membeyond 256 MB without apgbouncerupstream capping active connections — at 100 simultaneous connections, 256 MBwork_memalready consumes the pod's full usable RAM; without connection pooling, uncapped growth exceeds the 12 GB usable budget and triggers OOM kills. - ✗Don't use a storage
classthat cannot sustain random I/O for thevolumeClaimTemplatesdata PVC — HNSW and IVFFlat index builds generate a heavy random-IO profile; a throughput-optimized or network-attachedclasswith high sequential latency will serialize index writes and negate themax_parallel_maintenance_workers = 4parallelism configured inpostgresql.conf.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Platform Engineering subscription.
From · cancel anytime
More free lessons in AI Developer Platform Engineering
- Ch 9Deploy cost dashboards with Grafana
- Ch 10Deploy onboarding system with ArgoCD integration
- Ch 12Design tool registry model with MCP server metadata
- Ch 12Deploy MCP hub with Helm and agent integration
- Ch 13Deploy managed pgvector with Helm StatefulSetYou are here
- Ch 14Deploy evaluation platform with Helm and Grafana
- Ch 16Deploy SLA monitoring with Grafana dashboards