Skip to main content

Overview

pgvector adds a vector column type and approximate nearest-neighbor indexes to PostgreSQL, so embeddings live in the same database as the relational data they describe. This template deploys PostgreSQL 18 with pgvector 0.8.6 on a persistent volume, with an optional PgBouncer connection pooler and optional scheduled backups to AWS S3, GCS, or a self-hosted MinIO instance. The extension is not just installed in the image — the template creates it for you on first boot, in the database named in your credentials secret. A fresh install can store vectors and run similarity queries with no setup SQL. Database credentials are not template values. PostgreSQL reads its username, password and database name from a dictionary secret you create before installing, so no password passes through Helm or lands in the release.
pgvector on Control Plane operates as a single-replica deployment. Do not scale up the replica count, as this would result in multiple isolated instances rather than a replicated cluster. For automatic failover, see High Availability.

What Gets Created

  • Stateful pgvector Workload — (RELEASE_NAME-pgvector): a single replica of PostgreSQL 18 with pgvector 0.8.6, listening on port 5432.
  • Volume Set — (RELEASE_NAME-pgvector-vs): persistent storage for the data directory, with optional autoscaling and a final snapshot retained for 7 days.
  • First-Boot SQL Secret — (RELEASE_NAME-pgvector-init): an opaque secret mounted at /docker-entrypoint-initdb.d/00-pgvector.sql, holding the CREATE EXTENSION statements. It contains no credentials.
  • Identity & Policy — (RELEASE_NAME-pgvector-identity, RELEASE_NAME-pgvector-policy): an identity bound to the database, pooler and backup workloads, and a policy granting it reveal on exactly the secrets this release uses — nothing else. When backups are enabled, the identity also carries the Cloud Account binding the backup job uses to reach your bucket.
  • PgBouncer Workload (optional) — (RELEASE_NAME-pgbouncer): a connection pooler in front of PostgreSQL, created when pgbouncer.enabled: true.
  • Backup Cron Workload (optional) — (RELEASE_NAME-pgvector-backup): a scheduled pg_dumpall that writes compressed SQL dumps to AWS S3, GCS or MinIO, created when backup.enabled: true.
The template creates no credential secret of its own. Every password lives in the prerequisite secrets described below.
This template does not create a GVC. You must deploy it into an existing GVC.

Prerequisites

One secret must exist before you install. It holds the credentials your applications put in their connection strings. The values never pass through Helm, so they do not land in the release. Secrets are org-level, so no GVC flag is involved.
1

Create the database credentials secret

A dictionary secret holding exactly three keys — username, password and database. PostgreSQL creates that role and that database on first boot, and the template creates the vector extension inside it:
Set config.credentialsSecretName to the name you used. Secret names are org-wide, so give each release its own.
2

Read the secret back later

Pass -o yaml. A bare cpln secret reveal prints only a summary table, not the values:
Create the secret before installing, or the deployment wedges silently. The template refuses to render when config.credentialsSecretName is blank, but a name that points at a secret which does not exist installs “successfully” and then never starts. The container never runs, so cpln logs returns zero lines — there is nothing to log, and every summary surface just looks like a slow deploy. The one place the reason appears is status.versions[].message:
Use get-deployments — plain cpln workload get has no versions key and will show you nothing. Creating the missing secret repairs it on its own with no further action; recovery was measured here at 10 minutes 11 seconds. To skip the wait, run cpln workload force-redeployment RELEASE_NAME-pgvector --gvc GVC_NAME. Waiting does not leave you with a half-built database: once the secret exists, first boot runs normally and the vector extension is created.
Backups need a bucket, and for AWS or GCP a Control Plane Cloud Account, before they can be enabled — see Backup Prerequisites. MinIO backups need a second dictionary secret holding accessKey and secretKey. Nothing else is required.

Installation

To install, follow the instructions for your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Configuration

The default values.yaml for this template:

Credentials

  • config.credentialsSecretName — Name of the dictionary secret holding username, password and database. PostgreSQL creates that role and that database on first boot, and this is the credential your applications put in their connection strings.
The secret must exist before installing — see Prerequisites. The workload reads it through cpln://secret/... references, so the values appear in neither the Helm release nor the stored workload spec.
These credentials are only applied on first startup, when the data directory is empty. Rotating the secret afterwards does not change the stored password; change it inside PostgreSQL with ALTER ROLE ... WITH PASSWORD and update the secret to match. A cpln:// reference is also only resolved when a replica starts, so force a redeployment of the workload after updating a secret it already reads.

Extensions

  • config.extraExtensions — Additional extensions created alongside vector at first boot. Each entry must be an extension the image already ships, such as pg_trgm, pgcrypto or btree_gin. Names are validated at render time against ^[a-z][a-z0-9_]*$, so an entry with punctuation or capitals fails the install rather than reaching the database.
The vector extension itself is always created and does not need to be listed here.
config.extraExtensions is read only on first boot, and a name the image does not carry costs you that extension. The init script stops at the bad name, the container exits once, and the restart finds a populated data directory and skips initialization entirely — leaving a healthy server that has vector (created first) but not your extra extension, with nothing in the deployment status to say so. Adding an extension later is one statement:

Resources

  • resources.minCpu / resources.minMemory — Minimum CPU and memory guaranteed to the workload.
  • resources.maxCpu / resources.maxMemory — Maximum CPU and memory the workload can use.
Vector search is CPU-bound and HNSW index builds are far faster when the graph fits in memory, so these defaults are higher than the plain PostgreSQL template’s.
On a stateful workload the ratio of maxCpu to minCpu may not exceed 4:1. The shipped 1000m / 300m is 3.33:1; raising maxCpu without raising minCpu can cross the limit and is rejected when the workload is applied.

Storage

  • volumeset.capacity — Initial volume size in GiB (minimum 10).
  • volumeset.autoscaling.enabled — Allow the volume to grow as it fills. When enabled:
    • maxCapacity — Maximum volume size in GiB.
    • minFreePercentage — Trigger a scale-up when free space drops below this percentage.
    • scalingFactor — Multiply the current capacity by this factor when scaling up.
Growth is online — no restart is involved, and the filesystem expands within seconds of the volume resize. Expect roughly two minutes between crossing the threshold and the larger volume appearing, and note that the new size is rounded up.

Internal Access

  • internalAccess.type — Controls which workloads can connect to pgvector on port 5432:
  • internalAccess.workloads — The workload links allowed in, used with same-gvc or workload-list.

Public Access

  • publicAccess.enabled — Publish port 5432 through a TCP load balancer, reachable from outside the org. Off by default.
When enabled, Control Plane assigns a canonical endpoint. Read it from status.canonicalEndpoint in the workload:
Public connections are unencrypted. The image ships no TLS certificate, so the server reports ssl as off and a client that demands sslmode=require fails with a connection timeout rather than a clear TLS error. The libpq default of sslmode=prefer connects. Anyone who reaches the endpoint needs only the database password, so prefer internal access over RELEASE_NAME-pgvector.GVC_NAME.cpln.local where you can.
Firewall changes take 30 seconds to a few minutes to propagate — turning public access on was measured taking 136 seconds to become reachable. After changing internalAccess or publicAccess, re-test rather than trusting the first response.

PgBouncer Connection Pooling

PgBouncer is an optional connection pooler that sits in front of PostgreSQL and multiplexes application connections into a smaller pool of real database connections — useful for the bursty, short-lived connections typical of retrieval and embedding services. When enabled, it becomes the primary connection endpoint for your applications:
  • pgbouncer.enabled — Enable or disable PgBouncer.
  • pgbouncer.image — PgBouncer container image.
  • pgbouncer.poolMode — Controls how connections are reused:
  • pgbouncer.defaultPoolSize — Number of real PostgreSQL connections PgBouncer maintains per pool (default: 25).
  • pgbouncer.maxClientConn — Maximum number of client connections PgBouncer accepts (default: 1000). Clients past the limit are refused at connect time; the database itself is unaffected.
  • pgbouncer.replicas — Number of PgBouncer instances. PgBouncer is stateless and can be scaled horizontally.
  • pgbouncer.resources.cpu / pgbouncer.resources.memory — Resources allocated to each PgBouncer replica.
PgBouncer reads the same credentials secret and identity as the pgvector workload — no additional secrets or IAM configuration is required. Its userlist.txt and pgbouncer.ini are generated at startup from the username, password and database keys in that secret.
In transaction pool mode a plain SET hnsw.ef_search is not bound to your client — it can leak between clients. This is an isolation hazard, not a tuning inconvenience: PgBouncer hands the same server connection to different clients, so your setting may be lost, may be replaced by another client’s value, or may raise an error. Measured with 16 concurrent clients in transaction mode: 5 kept their own value, 9 read a different client’s value, and 2 got ERROR: unrecognized configuration parameter "hnsw.ef_search". Direct connections do not leak, and poolMode: session was clean 16 out of 16.It matters on this template specifically because ef_search is the recall knob — raising it from the default 40 to 400 took an exact-match test from 4 hits out of 5 to 5 out of 5. Under transaction pooling your recall tuning is therefore not merely lost; it can silently become another client’s.Two remedies, either of which is sufficient:
  • Scope the setting to a transaction with SET LOCAL, which was clean 16 out of 16:
  • Or set pgbouncer.poolMode: session, at the cost of the connection multiplexing PgBouncer is there for.
This is upstream PgBouncer behavior, not a template setting. PgBouncer is off by default, so a default install is not exposed to it.

Backup

Backup is disabled by default. When enabled, a cron workload runs pg_dumpall on the configured schedule and uploads compressed SQL dumps to AWS S3, GCS or MinIO. The job authenticates with the username and password from your credentials secret.
  • backup.enabled — Enable scheduled backups.
  • backup.image — Backup container image, matched to the server’s major version. The shipped 18.1.0 tag carries a PostgreSQL 18 client.
  • backup.schedule — Cron expression for backup frequency (default: daily at 2am UTC). Interval forms such as */15 * * * * are accepted.
  • backup.provideraws, gcp, or minio.
  • backup.resources.cpu / backup.resources.memory — Resources for the backup cron container.
  • backup.PROVIDER.prefix — Folder path within the bucket where backups are stored.
All three provider paths were exercised end to end at the shipped settings, with the resulting objects downloaded and read back, plus a full restore round-trip.
Changing backup.provider on an existing release leaves the old cloud binding attached. Control Plane merges an identity’s cloud-binding block and never removes one, so a release switched from aws to gcp keeps both bindings even though the template renders only the new one. Uninstall and reinstall to change providers cleanly.

Using pgvector

The vector extension is already created in the database named in your credentials secret, so there is no setup SQL to run:

Distance Operators

Index Types and Recall

Both index types build on this image and are chosen by the planner on cost — no enable_seqscan tricks required. Approximate indexes trade recall for speed, so an exact match can be missed at the default settings. Raising hnsw.ef_search to 400 turned a 4-of-5 exact-match result into 5 of 5. Large index builds are much faster after SET maintenance_work_mem = '512MB';.
With PgBouncer enabled in transaction mode, set these per query using SET LOCAL inside a transaction. A plain SET can leak to, or be overwritten by, another client — see PgBouncer Connection Pooling.

Dimension Limits

A vector column holds up to 16,000 dimensions but can only be indexed up to 2,000. That is the most common surprise: 1,536-dimension embeddings index fine, while 3,072-dimension ones are rejected with column cannot have more than 2000 dimensions for hnsw index. Use halfvec, which indexes up to 4,000 dimensions, or reduce the dimensionality.

Backup Prerequisites

Only needed when backup.enabled: true.

AWS S3

  1. Create an S3 bucket. Set backup.aws.bucket to its name and backup.aws.region to its region.
  2. If you do not have a Control Plane Cloud Account set up, follow the Create a Cloud Account guide. Set backup.aws.cloudAccountName to its name.
  3. Create an IAM policy with the following JSON, replacing YOUR_BUCKET_NAME:
  1. Set backup.aws.policyName to the name of the policy created in step 3. The template attaches it to the workload’s identity, and attaches nothing else — the bucket in your policy is the only storage the backup job can reach.
  2. Set backup.aws.prefix to the folder path where backups will be stored.

GCS

  1. Create a GCS bucket. Set backup.gcp.bucket to its name.
  2. If you do not have a Control Plane Cloud Account set up, follow the Create a Cloud Account guide. Set backup.gcp.cloudAccountName to its name.
  3. Add the Storage Admin role to the GCP service account associated with the Cloud Account. The template additionally binds the identity to roles/storage.objectAdmin on exactly the bucket in backup.gcp.bucket.
  4. Set backup.gcp.prefix to the folder path where backups will be stored.

MinIO

No Cloud Account is needed — credentials are supplied as a secret.
  1. Create a bucket in MinIO. Set backup.minio.bucket to its name.
  2. Set backup.minio.endpoint to the MinIO S3 API address including the port. For the MinIO template deployed in the same GVC, use http://WORKLOAD_NAME:9000.
  3. Create a dictionary secret holding exactly the keys accessKey and secretKey, and set backup.minio.credentialsSecretName to its name. For the MinIO template these are its admin.username and admin.password:
  1. Set backup.minio.prefix to the folder path where backups will be stored.
The policy this template creates grants the workload reveal on the database credentials secret, the first-boot SQL secret, and — only when backup.provider: minio — the MinIO credentials secret. Nothing else.

Restoring a Backup

The dumps are whole-cluster pg_dumpall output and contain CREATE EXTENSION IF NOT EXISTS vector, so restore into a database whose image carries pgvector. Replaying one into a stock postgres:18 fails at that line and the tables are never created. Run the following from a client with access to the backup bucket, using the username and password from your credentials secret. AWS S3:
GCS:
MinIO:
Use a PostgreSQL 18 psql for the restore. An older client reaches the end of a PG18 dump and prints invalid command \unrestrict. The restore still completes correctly, but the message is alarming and avoidable.

High Availability

This template is a single server on a single volume. A node failure reschedules the workload and reattaches the same volume, so the exposure is downtime, not data loss: Nothing serializes the rollout on a stateful workload, so treat every upgrade as a planned write outage of roughly a minute and a half. Data on the volume survives both events intact, including built HNSW graphs, which are not rebuilt. For automatic failover, use the PostgreSQL Highly Available template, whose image also carries pgvector. It is not the same build: Both hnsw and ivfflat exist in 0.8.0, so the gap is fixes and refinements rather than a missing index type. It is still a different pgvector release on a different database major, so verify against your own queries before treating the two as interchangeable.

Connecting

Important Notes

  • Create the credentials secret before installing. A reference to a secret that does not exist wedges the workload with no log output at all; see Prerequisites for the one command that shows the reason.
  • The vector extension is created by this template, not by the image. It lands in the database named in your credentials secret, on first boot only.
  • config.extraExtensions and the credentials are read only when the data directory is empty. Add an extension later with CREATE EXTENSION IF NOT EXISTS ..., and rotate a password with ALTER ROLE ... WITH PASSWORD, updating the secret to match.
  • PgBouncer in transaction mode leaks session settings between clients — including hnsw.ef_search. Use SET LOCAL inside a transaction, or poolMode: session. See PgBouncer Connection Pooling.
  • publicAccess is unencrypted — the image ships no TLS certificate, so sslmode=require fails. Prefer internal access.
  • Do not scale past one replica. A second stateful replica gets its own volume, which is a second empty database rather than a replica.
  • A cpln helm upgrade restarts the server — measured at 92 seconds of failed writes. The first upgrade after an install re-applies resources even when the values are byte-identical.
  • Restore with a PostgreSQL 18 psql, into an image that carries pgvector. A stock postgres:18 fails on the dump’s CREATE EXTENSION line.
  • Data lives on the volume set and survives redeploys; cpln helm uninstall deletes it, taking the database with it. Your credentials secret is yours and is left alone.
  • Firewall changes take 30 seconds to a few minutes to take effect. After changing internalAccess or publicAccess, re-test rather than trusting the first response.

External References

pgvector Documentation

Upstream reference for vector types, operators, and index tuning

pgvector Image

The image this template deploys, and its available tags

PostgreSQL 18 Documentation

Official documentation for the server version shipped here

PgBouncer Documentation

PgBouncer configuration reference

Cloud Accounts

Create a Control Plane Cloud Account for backup storage access

pgvector Template

View the source files, default values, and chart definition