Deployment guide

PostgreSQL 18 pgvectorscale

Overview

This guide shows you how to deploy and use PostgreSQL 18 with pgvector and pgvectorscale on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images.

The image is a vector search database. One PostgreSQL stores high-dimensional embeddings next to ordinary relational data and ranks rows by similarity to a query vector. Two complementary index families are available in the same database.

pgvector provides the vector column type, the distance operators (<-> Euclidean, <=> cosine, <#> inner product) and the HNSW and IVFFlat index types. It is installed from the official PostgreSQL PGDG repository as postgresql-18-pgvector.

pgvectorscale adds StreamingDiskANN, an approximate nearest-neighbor index inspired by Microsoft research. It keeps most of the index on disk rather than in memory, and adds statistical binary quantization. This lets a modest VM serve a large embedding collection.

pgvectorscale has no distribution package. SecureAnyCloud compiles it from the pinned upstream source at build time and ships only the finished extension, never the compiler that produced it.

Both extensions are under the permissive PostgreSQL License. Both are enabled in a default database named vectordb. It also ships a demo items table of two thousand 128-dimension vectors with a StreamingDiskANN index, so you can run a real similarity query the moment the VM boots.

Every VM gets its own database. The captured image contains the PostgreSQL binaries and both extensions but no database cluster at all. On first boot, each VM runs its own initdb, so no two deployments share a cluster identity, a certificate or a password. There is no default, blank or shared credential at any point, not even for a moment.

What you get:

  • PostgreSQL 18 from the official PGDG repository, running under systemd as postgresql.service
  • pgvector and pgvectorscale, both pre-enabled in the default vectordb database, with a demo items table and a StreamingDiskANN index
  • A per-VM database cluster, a per-VM TLS certificate and two per-VM passwords (the postgres superuser and the vectorapp application role), generated on first boot into a root-only credentials file
  • Loopback-only networking by default: the only port reachable from the network is TCP 22
  • pg_hba.conf requiring TLS and scram-sha-256 on every TCP connection, so opening the database later is a single safe step
  • Unattended security upgrades left enabled, so the appliance keeps receiving patches

Prerequisites

  • An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region
  • A subscription to this listing on Azure Marketplace
  • A network security group that allows TCP 22 from your administration network. Nothing else needs to be opened, because PostgreSQL is bound to loopback in the shipped image.

Recommended virtual machine size: Standard_B2ms (2 vCPU, 8 GB RAM).

StreamingDiskANN is designed to keep the working set on disk, so it tolerates modest memory well. For large collections and high query rates, choose a memory-rich size such as Standard_E4s_v5 and a premium data disk.

Deploy the virtual machine

Create the VM from the image, opening only SSH to your own network:

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:pgvectorscale-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-rg \
  --name pgvectorscale-1 \
  --image sac1647283583153:pgvectorscale-ubuntu-24-04:default:latest \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

First boot creates the database cluster, so wait a minute before connecting. You can watch it complete with systemctl status pgvectorscale-firstboot.service.

Retrieve your per-VM credentials

On first boot, the VM initializes its own cluster and generates its own passwords and TLS certificate. SSH in and read the root-only credentials file:

sudo cat /root/pgvectorscale-credentials.txt

The file is mode 0600, owned by root, and lists the per-VM postgres superuser password, the per-VM vectorapp application role password, the database name and both extension versions. Nothing is baked into the image, and every deployed VM generates its own.

Confirm the service is healthy

Check that PostgreSQL is active, that it is listening on loopback, that TLS is on, and that both extensions are enabled in the default database:

sudo systemctl is-active postgresql.service
sudo ss -tlnp | grep 5432
sudo -u postgres psql -tAc 'SHOW ssl;'
sudo -u postgres psql -d vectordb -c "SELECT extname, extversion FROM pg_extension WHERE extname IN ('vector','vectorscale') ORDER BY extname;"
sudo -u postgres psql -d vectordb -tAc "SELECT amname FROM pg_am WHERE amname='diskann';"

You should see the service active, PostgreSQL listening on 127.0.0.1:5432 only, ssl reported as on, both the vector and vectorscale extensions with their versions, and the diskann access method registered. That last check proves that pgvectorscale is functional rather than merely present.

Connect to the database

On the VM itself you can connect as the postgres superuser with no password, through the local unix socket:

sudo -u postgres psql -d vectordb -c "SELECT version();"

To connect over TCP, use the per-VM password from the credentials file. Every TCP connection, loopback included, must be TLS encrypted, so sslmode=require is mandatory. Replace <VECTORAPP_PASSWORD> with the value from the credentials file:

PGPASSWORD=<VECTORAPP_PASSWORD> psql "host=127.0.0.1 port=5432 dbname=vectordb user=vectorapp sslmode=require" -c "SELECT ssl, version AS tls FROM pg_stat_ssl WHERE pid = pg_backend_pid();"

The pg_stat_ssl view confirms that the connection is encrypted (ssl is t) and reports the negotiated TLS version.

Use the vectorapp role for your application and keep the postgres superuser for administration.

Run a similarity search over the demo collection

The default vectordb database ships a demo table called items: two thousand vectors of 128 dimensions with a StreamingDiskANN index already built over them. Query it by ordering on the cosine distance operator <=>:

sudo -u postgres psql -d vectordb <<'SQL' SELECT count(*) AS rows, vector_dims(embedding) AS dims FROM items GROUP BY 2; SELECT c.relname AS index_name, a.amname AS index_type FROM pg_class c JOIN pg_am a ON a.oid = c.relam WHERE c.relname = 'items_embedding_diskann'; SELECT id, label, round((embedding <=> (SELECT embedding FROM items WHERE id = 1))::numeric, 6) AS cosine_distance FROM items ORDER BY embedding <=> (SELECT embedding FROM items WHERE id = 1) LIMIT 5; SQL

The first row returned is id 1 itself at distance 0. It is followed by its four nearest neighbors in the collection, ranked by meaning rather than by any column value.

Build a StreamingDiskANN index on your own embeddings

Store your embeddings in a vector(N) column, then build a StreamingDiskANN index with USING diskann. Match the operator class to the distance operator you will query with: vector_cosine_ops for <=>, vector_l2_ops for <->.

sudo -u postgres psql -d vectordb <<'SQL' DROP TABLE IF EXISTS guide_docs; CREATE TABLE guide_docs ( id bigserial PRIMARY KEY, title text, embedding vector(128) ); INSERT INTO guide_docs (title, embedding) SELECT 'doc-' || g.i, v.emb FROM generate_series(1, 20000) AS g(i) CROSS JOIN LATERAL ( SELECT ('[' || string_agg(random()::text, ',') || ']')::vector(128) AS emb FROM generate_series(1, 128) AS d(k) WHERE g.i IS NOT NULL ) AS v; CREATE INDEX guide_docs_diskann ON guide_docs USING diskann (embedding vector_cosine_ops); ANALYZE guide_docs; SQL

Now confirm that the planner uses the index rather than scanning the table. EXPLAIN on an ORDER BY ... LIMIT query should report an index scan on guide_docs_diskann:

sudo -u postgres psql -d vectordb <<'SQL' EXPLAIN (COSTS ON) SELECT id, title FROM guide_docs ORDER BY embedding <=> (SELECT embedding FROM guide_docs WHERE id = 1) LIMIT 10; SQL

Seeing Index Scan using guide_docs_diskann in the plan is the difference between a vector column and a vector database. If you see Seq Scan instead, the collection is small enough that PostgreSQL judges a scan cheaper, which is the correct decision at that size.

Two query-time settings trade accuracy against speed. SET diskann.query_rescore = 50; raises recall by rescoring more candidates. SET diskann.query_search_list_size = 100; widens the search frontier. Tune them per session or per role.

Clean up the example when you are finished with it:

sudo -u postgres psql -d vectordb -c "DROP TABLE IF EXISTS guide_docs;"

Open the database to your application network

The shipped image binds PostgreSQL to loopback only, so the database is not reachable from the network until you decide otherwise. Exposing it is a deliberate two-step change. pg_hba.conf already requires TLS and the per-VM password, so no third step is needed to make it safe.

# 1. on the VM: bind to all interfaces and restart
sudo sed -i "s/^listen_addresses.*/listen_addresses = '*'/" /etc/postgresql/18/main/postgresql.conf
sudo systemctl restart postgresql
# 2. from your workstation: open 5432 to YOUR subnet only, never to the internet az network nsg rule create --resource-group my-rg --nsg-name <your-nsg> \
  --name allow-postgres --priority 900 --access Allow --protocol Tcp \
  --destination-port-ranges 5432 --source-address-prefixes <your-app-subnet-cidr>

Then tighten the 0.0.0.0/0 line in /etc/postgresql/18/main/pg_hba.conf to the same CIDR. Remote clients must still present the per-VM password over TLS. There is no plaintext TCP path on this image, on loopback or off it.

Security posture

No known credential, ever. The image ships no database cluster, so there is no role, no password and no certificate to discover. Both passwords are generated on your VM's first boot. They are set through psql standard input, so they never appear on a command line or in the journal, and are written to /root/pgvectorscale-credentials.txt at mode 0600.

Per-VM cluster identity. Each VM runs its own initdb, so two VMs from this image have different cluster system identifiers, different certificates and different passwords.

TLS on every TCP connection. pg_hba.conf carries only hostssl ... scram-sha-256 rules. Local administration stays passwordless through the Unix socket.

Loopback only by default. The only port this image exposes to the network is TCP 22. PostgreSQL 5432 is bound to 127.0.0.1 until you change it.

No build toolchain. pgvectorscale is compiled at image build time and the Rust and C toolchains are removed before capture, so the shipped image carries the extension but no compiler.

SSH hardening. Root login is disabled outright (PermitRootLogin no, not prohibit-password) and password authentication is off.

Kernel module baseline. The image ships a /etc/modprobe.d/dirtyfrag.conf baseline that disables the esp4, esp6, ipcomp, ipcomp4, ipcomp6 and rxrpc modules. If you intend to terminate IPsec on this VM, remove that file.

To rotate a password later, run sudo -u postgres psql -c "ALTER ROLE vectorapp PASSWORD '<new>'" and update the credentials file to match.

Operations

Manage the service through systemd and inspect logs with journalctl:

sudo systemctl status postgresql.service --no-pager sudo journalctl -u postgresql@18-main.service --no-pager | tail -20

Take a logical backup of a database with pg_dump, for example sudo -u postgres pg_dump vectordb > vectordb.sql. The data directory lives under /var/lib/postgresql/18/main and the first boot log is at /var/log/sac/sac-firstboot.log.

Support

Every SecureAnyCloud image includes 24/7 support. For questions about this PostgreSQL with pgvectorscale image, contact sales@secureanycloud.com.

Deploy on Azure

Launch PostgreSQL 18 with pgvector and pgvectorscale on Ubuntu 24.04 on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure