Deploying Immich on K3s with CNPG, VectorChord & Valkey

by Nithin Titta 16 min read

Author: [Your Name]
Published: [Date]
Tags: K3s, Kubernetes, Immich, PostgreSQL, CNPG, VectorChord, Valkey, Homelab


1. Introduction

Immich is an open-source, self-hosted photo and video management solution. It requires three main backend components: a PostgreSQL database (with vector search support for ML-powered features), a Redis-compatible cache, and a machine learning service.

In this post, I’ll walk through deploying Immich on a 3-node K3s homelab cluster with:

  • Cloud Native PostgreSQL (CNPG) operator for a highly-available PostgreSQL cluster
  • VectorChord (vchord) for vector similarity search instead of standard pgvector
  • Valkey as a Redis-compatible cache (built into the Immich Helm chart)
  • MetalLB for LoadBalancer IP allocation
  • Traefik ingress with Let’s Encrypt TLS

Everything runs on bare metal with no managed cloud services.


2. Prerequisites & Cluster Overview

2.1 K3s Cluster

My cluster consists of 3 nodes, all running as control-plane/etcd members:

  • 3 nodes, openSUSE Leap
  • K3s v1.34.6+k3s1
  • containerd 2.2.2
  • CNPG operator pre-installed
  • MetalLB pre-configured with IP pools
  • Traefik as the default ingress controller
  • cert-manager with Let’s Encrypt ClusterIssuer

2.2 Storage Classes

Three storage classes are available:

local-path Local SSD storage on each node — used for PostgreSQL data
truenas-nfs-ssd NFS-backed SSD storage from TrueNAS — used for ML cache and Valkey
nfs-immich-class NFS-backed HDD storage — used for the Immich media library

3. Setting Up the PostgreSQL Cluster with VectorChord

3.1 Why VectorChord?

Immich uses vector embeddings for ML-powered features like smart search, face recognition, and similar image clustering. The Immich documentation recommends using the ghcr.io/tensorchord/cloudnative-vectorchord image, which bundles PostgreSQL 18.4 with the VectorChord extension pre-installed.

VectorChord is a modern vector search engine for PostgreSQL that offers better performance and indexing capabilities compared to the standard pgvector extension.

3.2 Creating the CNPG Cluster

First, I created a dedicated namespace and a secret for the database credentials:

kubectl create namespace immich

kubectl create secret generic immich-postgres-secret \
  --namespace immich \
  --from-literal=username=immich \
  --from-literal=password=your-secure-password

Then I defined the CNPG Cluster manifest. Key details:

  • 3 instances for high availability (one per node)
  • VectorChord image: ghcr.io/tensorchord/cloudnative-vectorchord:18.4
  • local-path storage (20Gi each) for database performance
  • Pod anti-affinity to ensure each instance runs on a different node
  • postInitApplicationSQL to automatically create required extensions in the Immich database
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: immich-database
  namespace: immich
spec:
  instances: 3
  imageName: ghcr.io/tensorchord/cloudnative-vectorchord:18.4

  storage:
    size: 20Gi
    storageClass: local-path

  bootstrap:
    initdb:
      database: immich
      owner: immich
      secret:
        name: immich-postgres-secret
      postInitApplicationSQL:
        - CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
        - CREATE EXTENSION IF NOT EXISTS earthdistance CASCADE;

  postgresql:
    shared_preload_libraries:
      - "vchord.so"

  affinity:
    enablePodAntiAffinity: true
    topologyKey: kubernetes.io/hostname

  primaryUpdateStrategy: unsupervised
kubectl apply -f immich-cnpg-cluster.yaml

3.3 Monitoring Cluster Bootstrap

CNPG will create 3 pods, one on each node. The first pod initializes the database, creates the extensions, and then the other two join as replicas:

kubectl get pods -n immich -w
NAME                 READY   STATUS    RESTARTS   AGE
immich-database-1    1/1     Running   0          2m
immich-database-2    1/1     Running   0          1m
immich-database-3    1/1     Running   0          1m

3.4 Verifying the Extensions

Once all pods are ready, verify that VectorChord and earthdistance are installed:

kubectl exec -n immich pod/immich-database-1 -- \
  psql -U postgres -d immich -c "\dx"
                 List of installed extensions
     Name      | Version |   Schema   |         Description
---------------+---------+------------+------------------------------
 cube          | 1.5     | public     | data type for multidimensional cubes
 earthdistance | 1.2     | public     | calculate great-circle distances
 plpgsql       | 1.0     | pg_catalog | PL/pgSQL procedural language
 vchord        | 1.1.1   | public     | vchord vector search extension
 vector        | 0.8.3   | public     | vector data type and ivfflat and hnsw access methods
(5 rows)

Note: VectorChord bundles the vector extension (0.8.3) and provides its own vchord extension (1.1.1) on top. The cube extension is a dependency of earthdistance.


4. Deploying Immich via Helm

4.1 The Immich Helm Chart

Immich provides an official Helm chart hosted at oci://ghcr.io/immich-app/immich-charts/immich. The chart uses the bjw-s common library (v5.0.1) as a dependency and supports deploying all Immich components (server, machine learning, and optionally Valkey for caching).

4.2 Configuration Values

Here’s the values file I used. It configures:

  • Database connection to the CNPG cluster via the immich-database-rw service
  • Valkey enabled (built into the chart)
  • NFS PVC for the media library
  • MetalLB LoadBalancer for the server
  • Traefik ingress with Let’s Encrypt TLS
  • Resource limits for all components
# Shared settings applied to all components
controllers:
  main:
    containers:
      main:
        image:
          tag: v3.0.3
        env:
          DB_HOSTNAME: immich-database-rw
          DB_USERNAME:
            valueFrom:
              secretKeyRef:
                name: immich-postgres-secret
                key: username
          DB_PASSWORD:
            valueFrom:
              secretKeyRef:
                name: immich-postgres-secret
                key: password
          DB_DATABASE_NAME: immich
          REDIS_HOSTNAME: immich-valkey
          IMMICH_MACHINE_LEARNING_URL: 'http://immich-machine-learning:3003'

immich:
  metrics:
    enabled: false
  persistence:
    library:
      existingClaim: immich-nfs-pvc
  configurationKind: ConfigMap

# --- Server component ---
server:
  enabled: true
  controllers:
    main:
      replicas: 3
      containers:
        main:
          image:
            repository: ghcr.io/immich-app/immich-server
            pullPolicy: IfNotPresent
          resources:
            requests:
              memory: "1Gi"
              cpu: "500m"
            limits:
              memory: "2Gi"
              cpu: "1000m"
  service:
    main:
      type: LoadBalancer
      annotations:
        metallb.universe.tf/loadBalancerIPs: 10.0.9.11
      ports:
        http:
          port: 80
          targetPort: 2283
        https:
          port: 443
          targetPort: 2283
  ingress:
    main:
      enabled: true
      annotations:
        traefik.ingress.kubernetes.io/router.entrypoints: websecure
        traefik.ingress.kubernetes.io/router.tls: "true"
        cert-manager.io/cluster-issuer: "letsencrypt"
      hosts:
        - host: immich.example.com
          paths:
            - path: "/"
              pathType: Prefix
              service:
                identifier: main
      tls:
        - hosts:
            - immich.example.com

# --- Machine learning component ---
machine-learning:
  enabled: true
  controllers:
    main:
      replicas: 3
      containers:
        main:
          image:
            repository: ghcr.io/immich-app/immich-machine-learning
            pullPolicy: IfNotPresent
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "500m"
          env:
            TRANSFORMERS_CACHE: /cache
            HF_XET_CACHE: /cache/huggingface-xet
            MPLCONFIGDIR: /cache/matplotlib-config
  persistence:
    cache:
      enabled: true
      type: persistentVolumeClaim
      size: 10Gi
      accessMode: ReadWriteMany
      storageClass: truenas-nfs-ssd

# --- Valkey (Redis alternative) ---
valkey:
  enabled: true
  controllers:
    main:
      containers:
        main:
          image:
            repository: docker.io/valkey/valkey
            tag: 9.1-alpine
            pullPolicy: IfNotPresent
  persistence:
    data:
      enabled: true
      type: persistentVolumeClaim
      size: 5Gi
      accessMode: ReadWriteOnce
      storageClass: truenas-nfs-ssd

4.3 Deploying

helm upgrade --install immich \
  oci://ghcr.io/immich-app/immich-charts/immich \
  --namespace immich \
  -f values-immich.yaml

4.4 Verifying the Deployment

After a few minutes, all pods should be running:

kubectl get pods -n immich
NAME                                       READY   STATUS    RESTARTS   AGE
immich-database-1                          1/1     Running   0          5h
immich-database-2                          1/1     Running   0          5h
immich-database-3                          1/1     Running   0          5h
immich-machine-learning-57cbb9b648-h297d   1/1     Running   0          2m
immich-machine-learning-57cbb9b648-xvrx8   1/1     Running   0          2m
immich-machine-learning-57cbb9b648-xw2mv   1/1     Running   0          2m
immich-server-dd4966886-6hv8d              1/1     Running   0          17m
immich-server-dd4966886-8j4ml              1/1     Running   0          17m
immich-server-dd4966886-h59d8              1/1     Running   0          21m
immich-valkey-645c7d4899-vzw7b             1/1     Running   0          5h

Check that the server is responding:

curl -s http://10.0.9.11/api/server/ping
# {"res":"pong"}

curl -s http://10.0.9.11/api/server/version
# {"major":3,"minor":0,"patch":3}

5. Scaling to 3 Replicas

5.1 Why Scale?

Immich server and machine-learning components are stateless (the server reads from the shared NFS library, and ML workers share a cache). Scaling them to 3 replicas provides:

  • High availability — if a node goes down, the other replicas continue serving
  • Load distribution — requests are spread across all 3 nodes
  • Zero-downtime updates — rolling updates with no service interruption

5.2 Configuring Replicas

In the values file, I set replicas: 3 under both server.controllers.main and machine-learning.controllers.main. The ML cache PVC must use ReadWriteMany access mode since it’s shared across pods on different nodes.

5.3 Applying the Change

helm upgrade immich \
  oci://ghcr.io/immich-app/immich-charts/immich \
  --namespace immich \
  -f values-immich.yaml

6. Networking & Load Balancing

6.1 MetalLB IP Pools

I created a dedicated MetalLB IP pool for the DMZ/public VLAN:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: dmz-public
  namespace: metallb-system
spec:
  addresses:
    - 10.0.9.11-10.0.9.200
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: layer2-advert-dmz
  namespace: metallb-system
spec:
  ipAddressPools:
    - dmz-public
  interfaces:
    - enp0s20f0u1.9

The Immich server gets a static IP from this pool (10.0.9.11) via the annotation metallb.universe.tf/loadBalancerIPs: 10.0.9.11.

6.2 Ingress with Traefik & Let’s Encrypt

The Traefik ingress controller handles TLS termination with certificates from Let’s Encrypt (via cert-manager). The Immich web UI is accessible at https://immich.example.com.


7. Database Migration & Permission Fixes

7.1 The Migration Process

Immich runs database migrations automatically on startup. With 85+ migration files, the process completes in a few seconds on a fresh cluster.

7.2 Handling a Partial Restore

If restoring from a backup, you may encounter permission issues. The restore process creates tables owned by postgres instead of immich, causing the Immich server to crash with:

PostgresError: permission denied for table system_metadata

To fix this, change ownership of all tables to the immich user and grant proper permissions:

# Change ownership of all tables to immich
kubectl exec -n immich pod/immich-database-1 -- \
  psql -U postgres -d immich -c "
  ALTER SCHEMA public OWNER TO immich;
  ALTER TABLE public.system_metadata OWNER TO immich;
  ALTER TABLE public.migrations OWNER TO immich;
  -- ... repeat for all tables ...
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO immich;
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO immich;
"

You can generate the ALTER TABLE statements dynamically:

kubectl exec -n immich pod/immich-database-1 -- \
  psql -U postgres -d immich -t -c \
  "SELECT 'ALTER TABLE public.' || tablename || ' OWNER TO immich;' \
   FROM pg_tables WHERE schemaname = 'public';" \
  | kubectl exec -n immich pod/immich-database-1 -- psql -U postgres -d immich

7.3 Handling Missing Roles

If your backup references roles that don’t exist in the new cluster, create them:

kubectl exec -n immich pod/immich-database-1 -- \
  psql -U postgres -c "CREATE ROLE immiches WITH LOGIN SUPERUSER;"

kubectl exec -n immich pod/immich-database-1 -- \
  psql -U postgres -c "GRANT immiches TO postgres;"

8. Final Architecture

Component Replicas Storage Notes
immich-database (CNPG) 3 20Gi local-path each VectorChord 18.4, HA across nodes
immich-server 3 NFS (library via immich-nfs-pvc) MetalLB @ 10.0.9.11
immich-machine-learning 3 10Gi truenas-nfs-ssd (RWX) Shared cache across replicas
immich-valkey 1 5Gi truenas-nfs-ssd Redis-compatible cache

Extensions installed in PostgreSQL:

  • vchord 1.1.1 — vector search engine
  • vector 0.8.3 — vector data type (bundled with VectorChord)
  • earthdistance 1.2 — geospatial distance calculations
  • cube 1.5 — multidimensional data type (dependency of earthdistance)

9. Key Takeaways

  1. Use VectorChord for Immich — the official Immich docs recommend it over standard pgvector for better performance
  2. Dedicated CNPG cluster per app — gives each application its own PostgreSQL HA setup, avoiding the “shared database” single point of failure
  3. postInitApplicationSQL vs postInitSQL — use postInitApplicationSQL to run SQL against the application database (not the default postgres database)
  4. RWX for ML cache — when scaling ML replicas, the cache PVC must be ReadWriteMany
  5. Table ownership matters — after a DB restore, always verify table ownership matches the application user
  6. Helm chart with bjw-s — the Immich chart uses the bjw-s common library, which provides a consistent structure for controllers, services, persistence, and ingress

10. Useful Commands (Quick Reference)

Check cluster health

kubectl get clusters.postgresql.cnpg.io -n immich

Find the primary pod

kubectl get pods -n immich -l cnpg.io/instanceRole=primary

Check replication status

PRIMARY=$(kubectl get pods -n immich -l cnpg.io/instanceRole=primary -o name)
kubectl exec -n immich $PRIMARY -- \
  psql -U postgres -c "SELECT client_addr, state, sync_state FROM pg_stat_replication;"

Check installed extensions

kubectl exec -n immich pod/immich-database-1 -- \
  psql -U postgres -d immich -c "\dx"

Verify Immich server

curl -s http://<loadbalancer-ip>/api/server/ping
curl -s http://<loadbalancer-ip>/api/server/version

Check pod distribution across nodes

kubectl get pods -n immich -o \
  custom-columns="NAME:.metadata.name,NODE:.spec.nodeName"

Connect to the database

kubectl exec -n immich -it pod/immich-database-1 -- \
  psql -U postgres -d immich

Upgrade Immich to a new version

# Update the tag in values-immich.yaml, then:
helm upgrade immich \
  oci://ghcr.io/immich-app/immich-charts/immich \
  --namespace immich \
  -f values-immich.yaml

Have questions or feedback? Leave a comment below!

3.2 The Result

After scaling down, the old primary remained as the only instance. The WAL archive gap went undetected. When I tried to scale back up, new replicas could not catch up.

3.3 The Lesson

Never scale down a CNPG cluster without:

  1. Verifying replication health first
  2. Taking a fresh backup
  3. Confirming all replicas are fully in-sync
  4. Using a proper backup/restore approach for scaling down

4. Enabling pgvector

4.1 What is pgvector?

pgvector is an open-source extension that adds vector data types and similarity search to PostgreSQL. It’s essential for AI-powered applications, semantic search, and machine learning features like:

  • Similarity search (cosine distance, L2 distance)
  • Vector embeddings from AI models (OpenAI, etc.)
  • Similarity indexing (HNSW, IVFFlat)

Version installed: 0.8.1

4.2 Prerequisites: Finding Your Pod Name

Most commands require connecting to a PostgreSQL pod. First, list your pods:

# List all PostgreSQL pods in the postgres namespace
kubectl -n postgres get pods

# Expected output (example):
# NAME              READY   STATUS    RESTARTS   AGE
# pg-cluster-ha-3   1/1     Running   0          32s
# pg-cluster-ha-4   1/1     Running   0          5m
# pg-cluster-ha-5   1/1     Running   0          4m

Any pod works for read queries. The primary (newest) is usually the one with the most recent age. In this example, pg-cluster-ha-3 is the primary.

4.3 How to Enable pgvector on CNPG

The pre-built CNPG images based on Debian (like ghcr.io/cloudnative-pg/postgresql:18.1-system-trixie) already include pgvector — it just needs to be enabled.

Step 1: Apply shared_preload_libraries

Unlike most PostgreSQL settings, shared_preload_libraries requires a full server restart. CNPG 1.28 treats this as a top-level field (not under parameters):

kubectl -n postgres patch clusters.postgresql.cnpg.io pg-cluster-ha \
  --patch '{"spec":{"postgresql":{"shared_preload_libraries":["vector"]}}}' \
  --type=merge

This triggers an automatic restart of the database — expect a brief outage (a few seconds for a single instance).

Step 2: Verify it loaded

# Replace pg-cluster-ha-3 with your actual primary pod name
kubectl -n postgres exec pg-cluster-ha-3 -- \
  psql -U postgres -c "SHOW shared_preload_libraries;"
 shared_preload_libraries
--------------------------
 vector
(1 row)

Step 3: Create the extension in each database

# Create the vector extension in the "app" database
kubectl -n postgres exec pg-cluster-ha-3 -- \
  psql -U postgres -d app -c "CREATE EXTENSION IF NOT EXISTS vector;"

Step 4: Test it works

# Test vector distance calculation
kubectl -n postgres exec pg-cluster-ha-3 -- \
  psql -U postgres -d app -c \
  "SELECT '[0.1,0.2,0.3]'::vector '<=>' '[0.4,0.5,0.6]'::vector AS distance;"
      distance
--------------------
 0.5196152525944904
(1 row)

Step 5: Update the Cluster Config

After patching, update your cluster YAML to persist the change:

spec:
  postgresql:
    shared_preload_libraries:
      - vector

4.4 Alternative: Connect Without Knowing Pod Names

If you prefer not to look up pod names, you can launch a temporary psql client pod:

# Launch a temporary psql client connected to the primary service
kubectl run psql-client --rm -it --image=postgres:18 --namespace=postgres -- \
  psql -h pg-cluster-ha-rw.postgres.svc.cluster.local -U postgres -d app

This connects directly to the read-write ClusterIP service, which always routes to the primary. Type \q to exit and the temporary pod will be cleaned up automatically.


5. Scaling Back to 3 Instances

5.1 Update the Config File

Changed instances back from 1 to 3, and re-enabled pod anti-affinity:

spec:
  instances: 3
  affinity:
    enablePodAntiAffinity: true
    topologyKey: kubernetes.io/hostname

5.2 Apply the Changes

kubectl -n postgres patch clusters.postgresql.cnpg.io pg-cluster-ha \
  --patch '{"spec":{"instances":3,"affinity":{"enablePodAntiAffinity":true,"topologyKey":"kubernetes.io/hostname"}}}' \
  --type=merge

5.3 Monitor the Process

# Watch pods come up (press Ctrl+C to stop)
kubectl -n postgres get pods -w | grep pg-cluster

CNPG will:

  1. Create a new pod (pg-cluster-ha-4) and start streaming from primary
  2. Create a second new pod (pg-cluster-ha-5) and start streaming
  3. The old primary (pg-cluster-ha-3) gets restarted with the new config

5.4 Handle Stuck Pods

If a pod gets stuck in “Terminating&,nbsp;force delete it:

kubectl -n postgres delete pod pg-cluster-ha-3 --force --grace-period=0

Then wait for CNPG to provision a fresh replacement.

5.5 Verify Everything

# Check pod status
kubectl -n postgres get pods
NAME              READY   STATUS    RESTARTS   AGE
pg-cluster-ha-3   1/1     Running   0          32s
pg-cluster-ha-4   1/1     Running   0          5m
pg-cluster-ha-5   1/1     Running   0          4m
# Check cluster status
kubectl -n postgres get clusters.postgresql.cnpg.io pg-cluster-ha
NAME            INSTANCES   READY   STATUS                     PRIMARY
pg-cluster-ha   3           3       Cluster in healthy state   pg-cluster-ha-4

5.6 Verify Node Distribution

# Check which node each pod runs on
kubectl -n postgres get pods -o custom-columns="NAME:.metadata.name,NODE:.spec.nodeName"
NAME              NODE
pg-cluster-ha-3   hpe233.ntitta.local
pg-cluster-ha-4   hpe232.ntitta.local
pg-cluster-ha-5   hpe234.ntitta.local

Each pod is on a separate node — exactly what anti-affinity provides.

5.7 Verify Replication Health

# First, identify the primary pod automatically
PRIMARY_POD=$(kubectl -n postgres get pods -o jsonpath='{.items[].metadata.name}' -l cnpg.io/instanceRole=primary -n postgres)
echo "Primary pod: $PRIMARY_POD"

# Check replication from the primary
kubectl -n postgres exec $PRIMARY_POD -- \
  psql -U postgres -c \
  "SELECT client_addr, state, sync_state, sent_lsn, flush_lsn, replay_lsn FROM pg_stat_replication;"
 client_addr |   state   | sync_state |  sent_lsn   |  flush_lsn  | replay_lsn
-------------+-----------+------------+-------------+-------------+-------------
 10.42.2.225 | streaming | async      | 65/CB059138 | 65/CB059138 | 65/CB059138
 10.42.1.125 | streaming | async      | 65/CB059138 | 65/CB059138 | 65/CB059138
(2 rows)

Both replicas are streaming and fully in-sync with the primary.

5.8 Verify pgvector on the New Primary

# Verify pgvector extension on primary
PRIMARY_POD=$(kubectl -n postgres get pods -o jsonpath='{.items[].metadata.name}' -l cnpg.io/instanceRole=primary -n postgres)
kubectl -n postgres exec $PRIMARY_POD -- \
  psql -U postgres -d app -c \
  "SELECT '[0.1,0.2,0.3]'::vector '<=>' '[0.4,0.5,0.6]'::vector AS distance;"
      distance
--------------------
 0.5196152525944904
(1 row)

6. Final Cluster State

Cluster: pg-cluster-ha
Status: Healthy (3/3 instances)
Primary: pg-cluster-ha-4 (on Node 2)
Replicas: pg-cluster-ha-3 (Node 3), pg-cluster-ha-5 (Node 4)
pgvector: 0.8.1 (enabled via shared_preload_libraries)
Storage: 20Gi local-path per instance
HA: True (pod anti-affinity ensures 1 pod per node)

7. Key Takeaways

  1. CNPG’s shared_preload_libraries requires a full restart — plan for downtime
  2. Never scale down CNPG without verifying replication health first
  3. WAL gaps are silent — monitor replication lag continuously
  4. When scaling up, expect the old primary to restart (not a switchover)
  5. Stuck pods sometimes need force deletion — use with caution
  6. pgvector is included in the CNPG system-trixie image — just enable it
  7. Always keep config files in sync with live cluster state

8. Reference: Full Cluster YAML (Anonymized)

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-cluster-ha
  namespace: postgres
spec:
  instances: 3
  primaryUpdateStrategy: unsupervised
  enableSuperuserAccess: true

  affinity:
    enablePodAntiAffinity: true
    topologyKey: kubernetes.io/hostname

  postgresql:
    shared_preload_libraries:
      - vector

  storage:
    size: 20Gi
    storageClass: local-path

  managed:
    services:
      additional:
        - selectorType: rw
          serviceTemplate:
            metadata:
              name: pg-cluster-ha-lb
              annotations:
                metallb.universe.tf/loadBalancerIPs: 172.16.24.199
            spec:
              type: LoadBalancer

9. Useful Commands (Quick Reference)

Check cluster health

kubectl -n postgres get clusters.postgresql.cnpg.io

Find the primary pod

kubectl -n postgres get pods -l cnpg.io/instanceRole=primary

Check replication status

# Get primary pod name automatically
PRIMARY_POD=$(kubectl -n postgres get pods -o jsonpath='{.items[].metadata.name}' -l cnpg.io/instanceRole=primary -n postgres)

# Query replication
kubectl -n postgres exec $PRIMARY_POD -- \
  psql -U postgres -c "SELECT * FROM pg_stat_replication;"

Check pgvector version

# Get primary pod name
kubectl -n postgres get pods -l cnpg.io/instanceRole=primary

# Check vector extension version (replace  with actual name)
kubectl -n postgres exec  -- \
  psql -U postgres -c "SELECT extversion FROM pg_extension WHERE extname = 'vector';"

Scale cluster

kubectl -n postgres patch clusters.postgresql.cnpg.io pg-cluster-ha \
  --patch '{"spec":{"instances":N}}' --type=merge

Check node distribution

kubectl -n postgres get pods -o \
  custom-columns="NAME:.metadata.name,NODE:.spec.nodeName"

Connect to PostgreSQL directly

# Option A: Temporary psql client (auto-cleanup on exit)
kubectl run psql-client --rm -it --image=postgres:18 \
  --namespace=postgres -- \
  psql -h pg-cluster-ha-rw.postgres.svc.cluster.local \
  -U postgres -d app

# Option B: exec into a running pod
kubectl -n postgres exec -it  -- psql -U postgres -d app