PostgreSQL 18 with pgvector and pgvectorscale on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and use of PostgreSQL 18 with pgvector and pgvectorscale on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. It is a vector search database: one PostgreSQL that stores high dimensional embeddings next to ordinary relational data and ranks rows by similarity to a query vector, with two complementary index families available in the same database.
-
pgvector provides the
vectorcolumn type, the distance operators (<->Euclidean,<=>cosine,<#>inner product) and the HNSW and IVFFlat index types. It is installed from the official PostgreSQL PGDG repository aspostgresql-18-pgvector. -
pgvectorscale adds StreamingDiskANN, an approximate nearest neighbour index inspired by Microsoft research that keeps most of the index on disk rather than in memory, plus statistical binary quantisation. It is what lets a modest VM serve a large embedding collection. pgvectorscale has no distribution package: cloudimg 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, and both are enabled in a default database named vectordb, which 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 is included:
-
PostgreSQL 18 from the official PGDG repository, running under systemd as
postgresql.service -
pgvector and pgvectorscale both pre enabled in the default
vectordbdatabase, with a demoitemstable and a StreamingDiskANN index -
A per VM database cluster, a per VM TLS certificate and two per VM passwords (the
postgressuperuser and thevectorappapplication 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.confrequiring TLS andscram-sha-256on 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
-
Active Azure subscription, an SSH public key, and a VNet plus subnet in the target region
-
Subscription to this listing on Azure Marketplace
-
A Network Security Group allowing TCP 22 from your administration network. Nothing else needs to be opened: 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, but 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:
az vm create \
--resource-group my-rg \
--name pgvectorscale-1 \
--image <this-marketplace-image> \
--size Standard_B2ms \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
First boot creates the database cluster, so give it a minute before connecting. You can watch it complete with systemctl status pgvectorscale-firstboot.service.
Retrieve your per VM credentials
On the first boot the VM initialises 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: 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 line is the one that proves 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, is required to be TLS encrypted, so sslmode=require is not optional. 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 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, followed by its four nearest neighbours 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 the planner actually 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, and 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, and 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 the customer VM's first boot, set through psql standard input so they never appear on a command line or in the journal, and written to
/root/pgvectorscale-credentials.txtat mode0600. -
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.confcarries onlyhostssl ... scram-sha-256rules. Local administration stays password less 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.1until 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, notprohibit-password) and password authentication is off. -
Kernel module baseline. The image ships a
/etc/modprobe.d/dirtyfrag.confbaseline that disables theesp4,esp6,ipcomp,ipcomp4,ipcomp6andrxrpcmodules. 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/cloudimg-firstboot.log.
Support
Every cloudimg image includes 24/7 support. If you have any questions about this PostgreSQL with pgvectorscale image, contact us at support@cloudimg.co.uk.