ParadeDB 0.25.9 on AWS User Guide
Overview
This guide covers launching and verifying ParadeDB 0.25.9 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened PostgreSQL 18, PostGIS & Postgres Extension AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other PostgreSQL builds and extensions in the listing are described on the listing's guide hub.
ParadeDB's pg_search extension puts a search engine inside PostgreSQL. It builds a BM25 index, backed by the Tantivy search library, over the text columns you choose. A search is then an ordinary SQL query with the @@@ operator, and paradedb.score() returns the BM25 relevance score you can sort and filter on. Because the index lives in the same database as your data, there is no separate search cluster to run and no synchronisation pipeline to keep correct.
pgvector is included and enabled. pg_search declares requires = 'vector', so pgvector ships alongside it: the same database gives you BM25 keyword relevance and vector similarity search over embeddings.
What it is not. This is one PostgreSQL instance, not a distributed search cluster: there is no sharding across nodes, no separate analyzer and plugin ecosystem of the kind Elasticsearch offers, and no search UI (you query it with SQL). BM25 searches cannot be served from a physical standby; upstream ships that only in ParadeDB Enterprise, a separate commercial product that is not in this image. The extension here is ParadeDB Community, licensed under the GNU AGPL v3, with no licence key, activation check or trial timer.
What is on the image:
- PostgreSQL 18.6 from the PostgreSQL project's apt repository (
postgresql@18-main.service) withpg_searchpreloaded - ParadeDB pg_search 0.25.9 (the official ParadeDB package, pinned by its SHA-256 digest) and pgvector 0.8.7
- A brand-new cluster created on your instance's first boot, with a
searchdbdatabase, both extensions, asearchappapplication role and a 5,000-document demo corpus with a BM25 index - TLS with a certificate generated on your instance's first boot (TLS 1.2 minimum)
- A credential guard that runs on every boot and refuses to serve if a published or example password ever authenticates
Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3
Security posture
- No database ships in the image. The data directory is empty in the AMI. First boot runs
initdbon your instance, so no role, password, demo row or cluster identity is shared with any other instance. - No default password. First boot generates a 32-character password for the
searchappapplication role and writes it to/root/paradedb-credentials.txt, readable by root only. Thepostgressuperuser has no password at all: you use it on the instance withsudo -u postgres psql(peer authentication over the local socket), and it cannot log in over the network. - Nothing is served before first boot finishes. PostgreSQL will not start until first boot has created the cluster, the certificate and the password. While it sets the password, the server listens on its local socket only, with no TCP port and with statement logging off.
- Per-instance TLS certificate. First boot creates this instance's own private key and self-signed certificate under
/etc/postgresql/18/main/and turns TLS on. No key ships in the image, so no two instances share one. - Localhost only. PostgreSQL listens on
127.0.0.1:5432, and the listing's security group opens SSH (22) only. Reach the database through an SSH tunnel, or open it to your VPC as described in Step 7. - TLS on every TCP connection.
pg_hba.confaccepts TCP clients, loopback included, only overhostsslwithscram-sha-256; there is no plain-text TCP rule and notrustrule. - No metadata access for the database. A database user can make PostgreSQL open outbound connections (for example
COPY ... FROM PROGRAMas a superuser), so every PostgreSQL unit and both first-boot services are blocked from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). Amazon Time Sync (169.254.169.123) still works. If you add backups to S3 from inside the database, give them explicit credentials; they cannot use the instance role. - IMDSv2 required. The AMI is registered with
ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests. - Fails closed. If first boot cannot finish, PostgreSQL stays stopped and first boot tries again on the next boot.
- SSH. Key-only login for
ubuntu; root login, password and keyboard-interactive login and X11 forwarding are off. SSH host keys and the machine ID are generated fresh on every instance.
Prerequisites
- An AWS account and permission to launch EC2 instances and create security groups
- An EC2 key pair in the Region you launch in
- A VPC subnet, with a public IP on the instance if you connect over the internet
Recommended instance type: m5.large (2 vCPU, 8 GiB). The image's memory settings (shared_buffers = 1GB, maintenance_work_mem = 256MB) are sized for it; raise them in /etc/postgresql/18/main/conf.d/10-cloudimg.conf on larger instances. BM25 index builds use maintenance_work_mem.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| ParadeDB 0.25.9 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened PostgreSQL 18, PostGIS & Postgres Extension AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose ParadeDB 0.25.9 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: the listing's default group allows SSH (22) only. Restrict it to your own IP address. Do not open 5432 to the internet.
- Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR with your own:
SG=$(aws ec2 create-security-group --group-name paradedb \
--description "ParadeDB" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=paradedb}]'
You do not need --metadata-options: the AMI already makes IMDSv2 required.
Step 2: Connect and read your credentials
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot finishes within a minute of launch. Show the connection details (the password line is left out here; read it with sudo cat /root/paradedb-credentials.txt and keep it in your secrets manager):
sudo grep -v -e '^searchapp.password=' /root/paradedb-credentials.txt | head -16
# ParadeDB 0.25.9 - PostgreSQL 18 with pg_search (BM25) and pgvector (cloudimg AWS Marketplace image)
# Generated on THIS instance's first boot by paradedb-firstboot.service. This password exists
# nowhere else. The postgres superuser has NO password: use it on this instance with
# sudo -u postgres psql -d searchdb (peer authentication over the local socket)
# Rotate the application password (never on a command line) with:
# sudo -u postgres psql then \password searchapp
# and update this file to match.
searchapp.role=searchapp
searchapp.host=127.0.0.1
searchapp.port=5432
searchapp.database=searchdb
searchapp.sslmode=require
pg_search.version=0.25.9
pgvector.version=0.8.7
The commands below read the password from that file at run time, so it never appears on screen. In your own sessions you can instead put it in ~/.pgpass (mode 0600) as 127.0.0.1:5432:searchdb:searchapp:<SEARCHAPP_PASSWORD>.
Step 3: Confirm ParadeDB is running
psql --version
systemctl is-active postgresql@18-main.service paradedb-firstboot.service paradedb-credguard.service
sudo ss -Hltn
psql (PostgreSQL) 18.6 (Ubuntu 18.6-1.pgdg24.04+2)
active
active
active
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
LISTEN 0 200 127.0.0.1:5432 0.0.0.0:*
LISTEN 0 4096 [::]:22 [::]:*
PostgreSQL listens on 127.0.0.1:5432 only; port 53 on 127.0.0.53/127.0.0.54 is the operating system's local DNS stub. Now connect over TLS as searchapp, and show the extensions, the demo corpus and the TLS session:
export PGPASSWORD="$(sudo sed -n 's/^searchapp.password=//p' /root/paradedb-credentials.txt)"
psql "host=127.0.0.1 dbname=searchdb user=searchapp sslmode=require" \
-c "SELECT extname, extversion FROM pg_extension ORDER BY 1;" \
-c "SELECT count(*) AS documents FROM documents;" \
-c "SELECT ssl, version AS tls_version FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
extname | extversion
-----------+------------
pg_search | 0.25.9
plpgsql | 1.0
vector | 0.8.7
(3 rows)
documents
-----------
5000
(1 row)
ssl | tls_version
-----+-------------
t | TLSv1.3
(1 row)
Step 4: Run a BM25 search
The demo table documents holds 5,000 generated technical notes with a BM25 index (documents_bm25_idx) over id, title, body and category. The @@@ operator matches against the index and paradedb.score() returns the BM25 relevance score:
export PGPASSWORD="$(sudo sed -n 's/^searchapp.password=//p' /root/paradedb-credentials.txt)"
psql "host=127.0.0.1 dbname=searchdb user=searchapp sslmode=require" \
-c "SELECT id, title, round(paradedb.score(id)::numeric, 4) AS score FROM documents WHERE body @@@ 'relevance ranking retrieval' ORDER BY paradedb.score(id) DESC LIMIT 5;" \
-c "SELECT count(*) AS matches FROM documents WHERE body @@@ 'failover';"
id | title | score
------+----------------------+--------
195 | Operations note 195 | 4.1362
227 | Operations note 227 | 3.9567
1734 | Operations note 1734 | 3.8108
3551 | Operations note 3551 | 3.7746
1727 | Operations note 1727 | 3.7556
(5 rows)
matches
---------
1975
(1 row)
The demo text is generated on your instance's first boot, so your ids, scores and counts differ from these. Documents that mention more of the query terms, and mention them more often relative to their length, score higher.
Step 5: See the BM25 index in the query plan
EXPLAIN shows the planner answering the search with ParadeDB's custom scan over documents_bm25_idx, with the top-10 ordering by score pushed into the index rather than a sequential scan over the table:
export PGPASSWORD="$(sudo sed -n 's/^searchapp.password=//p' /root/paradedb-credentials.txt)"
psql "host=127.0.0.1 dbname=searchdb user=searchapp sslmode=require" \
-c "EXPLAIN SELECT id, paradedb.score(id) FROM documents WHERE body @@@ 'relevance' ORDER BY paradedb.score(id) DESC LIMIT 10;"
QUERY PLAN
---------------------------------------------------------------------------------------------------
Limit (cost=10.00..10.12 rows=10 width=12)
-> Custom Scan (ParadeDB Base Scan) on documents (cost=10.00..10.12 rows=10 width=12)
Table: documents
Index: documents_bm25_idx
Segment Count: 1
Exec Method: TopKScanExecState
Scores: true
TopK Order By: pdb.score() desc
TopK Limit: 10
Tantivy Query: {"with_index":{"query":{"parse_with_field":{"field":"body","query_string":"relevance","lenient":null,"conjunction_mode":null}}}}
(10 rows)
Step 6: Index your own table, with keywords and vectors
searchapp can create tables in searchdb. Create a small articles table with a text body and a 3-dimension embedding, add a BM25 index, then run a keyword search and a vector nearest-neighbour search against it:
export PGPASSWORD="$(sudo sed -n 's/^searchapp.password=//p' /root/paradedb-credentials.txt)"
psql "host=127.0.0.1 dbname=searchdb user=searchapp sslmode=require" <<'SQL'
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
embedding vector(3) NOT NULL
);
INSERT INTO articles (title, body, embedding) VALUES
('Tuning BM25 indexes', 'How segment merges and index retrieval settings change search latency', '[0.9,0.1,0.0]'),
('Vector search basics', 'Nearest neighbour retrieval over embeddings with pgvector', '[0.1,0.9,0.1]'),
('Hybrid retrieval', 'Combine keyword relevance with vector similarity for better ranking', '[0.5,0.5,0.1]'),
('Backups and restores', 'Point in time recovery for a PostgreSQL cluster', '[0.0,0.1,0.9]');
CREATE INDEX articles_bm25_idx ON articles USING bm25 (id, title, body) WITH (key_field = 'id');
SELECT title, round(paradedb.score(id)::numeric, 4) AS score
FROM articles WHERE body @@@ 'retrieval' ORDER BY paradedb.score(id) DESC;
SELECT title, round((embedding <-> '[0.2,0.8,0.1]')::numeric, 4) AS distance
FROM articles ORDER BY embedding <-> '[0.2,0.8,0.1]' LIMIT 2;
SQL
CREATE TABLE
INSERT 0 4
CREATE INDEX
title | score
----------------------+--------
Vector search basics | 0.7471
Tuning BM25 indexes | 0.6465
(2 rows)
title | distance
----------------------+----------
Vector search basics | 0.1414
Hybrid retrieval | 0.4243
(2 rows)
The BM25 index stays in step with the table: rows you insert, update or delete are searchable (or gone) as soon as the transaction commits. The key_field must be a unique column, normally the primary key. Remove the example when you are done with DROP TABLE articles;.
Step 7: Connect from your application
PostgreSQL accepts connections on localhost only. Two ways to reach it:
- SSH tunnel (no change to the instance). On your workstation run
ssh -N -L 5432:127.0.0.1:5432 ubuntu@<public-ip>, then connect your client to127.0.0.1:5432assearchappwithsslmode=requireand the password from the credentials file. - Open it to your VPC. In
/etc/postgresql/18/main/conf.d/10-cloudimg.confchangelisten_addresses = '127.0.0.1'tolisten_addresses = '*', restart PostgreSQL, then add an inbound rule for 5432 to the instance's security group from your application subnet or VPC CIDR only (never 0.0.0.0/0). Clients must use TLS (sslmode=require, orverify-fullwith this instance's certificate/etc/postgresql/18/main/cloudimg-server.crt) and the role's password:
sudo sed -i "s/^listen_addresses = '127.0.0.1'/listen_addresses = '*'/" /etc/postgresql/18/main/conf.d/10-cloudimg.conf
sudo systemctl restart postgresql@18-main.service
For more applications, create one least-privilege role each: in sudo -u postgres psql -d searchdb run CREATE ROLE app2 LOGIN; and then \password app2.
A wrong password is refused, the postgres superuser cannot log in over TCP at all (it has no password), and a plain-text TCP connection is refused before authentication:
export PGPASSWORD="$(sudo sed -n 's/^searchapp.password=//p' /root/paradedb-credentials.txt)"
OUT="$(PGPASSWORD=wrong-password psql "host=127.0.0.1 dbname=searchdb user=searchapp sslmode=require" -c "SELECT 1" 2>&1 || true)"
echo "${OUT#*failed: }"
OUT="$(psql "host=127.0.0.1 dbname=searchdb user=postgres sslmode=require" -c "SELECT 1" 2>&1 || true)"
echo "${OUT#*failed: }"
OUT="$(psql "host=127.0.0.1 dbname=searchdb user=searchapp sslmode=disable" -c "SELECT 1" 2>&1 || true)"
echo "${OUT#*failed: }"
FATAL: password authentication failed for user "searchapp"
FATAL: password authentication failed for user "postgres"
FATAL: no pg_hba.conf entry for host "127.0.0.1", user "searchapp", database "searchdb", no encryption
Step 8: Check the security controls
The PostgreSQL unit carries the metadata block, PostgreSQL listens on loopback with this instance's own certificate, only searchapp has a password, every TCP rule is TLS plus scram-sha-256, and SSH runs with the settings listed above:
systemctl show -p IPAddressDeny postgresql@18-main.service
sudo -u postgres psql -tAc "SHOW listen_addresses;" -c "SHOW ssl_cert_file;" -c "SHOW shared_preload_libraries;"
sudo -u postgres psql -tAc "SELECT rolname, rolpassword IS NOT NULL AS has_password FROM pg_authid WHERE rolcanlogin ORDER BY 1;"
sudo grep -vE '^[[:space:]]*(#|$)' /etc/postgresql/18/main/pg_hba.conf
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|x11forwarding) '
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
127.0.0.1
/etc/postgresql/18/main/cloudimg-server.crt
pg_search
postgres|f
searchapp|t
local all all peer
hostssl all all 127.0.0.1/32 scram-sha-256
hostssl all all ::1/128 scram-sha-256
hostssl all all 0.0.0.0/0 scram-sha-256
hostssl all all ::/0 scram-sha-256
local replication all peer
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
x11forwarding no
Maintenance
- Change the application password. Run
sudo -u postgres psqland type\password searchappat the prompt; it asks for the new password twice, sends only a SCRAM verifier and never puts it on a command line or in a log. Then update/root/paradedb-credentials.txt(the credential guard reads it on every boot) and your secrets manager. - Configuration. cloudimg's settings are in
/etc/postgresql/18/main/conf.d/10-cloudimg.conf(listener, memory, preloadedpg_search, logging) and20-cloudimg-tls.conf(this instance's certificate). Apply changes withsudo systemctl restart postgresql@18-main.service. - Updates.
sudo apt update && sudo apt upgradeinstalls PostgreSQL 18 minor releases and pgvector updates fromapt.postgresql.organd Ubuntu security updates (unattended security upgrades are on).pg_search0.25.9 is installed from ParadeDB's release package and stays at this version; a newer ParadeDB release arrives as a new version of this listing. - Backups. Take EBS snapshots of the volume or
pg_dump -Fc searchdbbackups regularly. BM25 indexes are rebuilt from the table data on restore. - Logs.
/var/log/postgresql/postgresql-18-main.logandjournalctl -u postgresql@18-main.service; first boot and the credential guard log tojournalctl -u paradedb-firstboot.serviceandjournalctl -u paradedb-credguard.service. - Licences. pg_search (ParadeDB Community) is under the GNU AGPL v3 (
AGPL-3.0-or-later); its source for this exact release is at github.com/paradedb/paradedb, tagv0.25.9. PostgreSQL and pgvector are under The PostgreSQL License./usr/share/doc/paradedb-cloudimg/ATTRIBUTION.txtandBUILD_VERSIONSlist the exact packages and digests.
Support
cloudimg supports this image by email at support@cloudimg.co.uk. Include the instance ID, the version (ParadeDB 0.25.9 on Ubuntu 24.04) and what you were doing.
PostgreSQL is a trademark of the PostgreSQL Community Association of Canada. ParadeDB is a trademark of its owner. Ubuntu is a trademark of Canonical Ltd. cloudimg is not affiliated with or endorsed by these organisations.