Percona Server for PostgreSQL 18 on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and use of Percona Server for PostgreSQL 18 on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Percona Server for PostgreSQL is Percona's freely distributed distribution of PostgreSQL: the same upstream engine your applications already target, packaged with the production extension set that most teams otherwise assemble by hand. Client drivers, SQL, extensions and on disk format are all stock PostgreSQL, so moving a workload in or out is a restore rather than a rewrite.
Everything is installed from Percona's own APT repository at repo.percona.com (the ppg-18 release channel), not from source and not from a third party archive. The image is built around the percona-ppg-server-18 distribution package, so the server and its extensions are the exact builds Percona ships and support.
Security by design — the image contains no database. Most database images ship a ready made cluster and rotate its password on first boot. This one does not ship a cluster at all: the data directory in the captured image is empty, so there is no default account, no seeded password and nothing that could leak between deployments. On first boot percona-postgresql-firstboot.service runs initdb to build a brand new cluster on the dedicated data volume, mints a unique 32 character password for the postgres superuser, mints a unique self signed TLS server certificate, bootstraps the default database and extensions through a private server that binds no network port at all, and writes everything to the root only file /root/percona-postgresql-credentials.txt. Only then does it write the bootstrap ready marker that allows systemd to start PostgreSQL, so an uninitialised cluster can never be served.
What is included:
-
Percona Server for PostgreSQL 18 (
percona-ppg-server-18) from Percona's official repository, running under systemd aspostgresql@18-main.service -
pg_stat_monitor, Percona's query performance monitoring extension, pre loaded and enabled
-
pgAudit for session and object level audit logging, pre loaded and configured to record DDL and role changes
-
pg_repack for removing table and index bloat online, and wal2json for logical replication feeds, both installed and ready to enable
-
A dedicated 40 GB data volume mounted at
/var/lib/percona-postgresql, holding the cluster separately from the operating system disk -
A default database
appdb, a per VM superuser password and a per VM TLS certificate, all created on first boot -
pg_hba.confwith notrustrule on any path: local access is peer authenticated, remote access requires TLS plus a password -
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 for administration and TCP 5432 for database clients. In production, restrict 5432 to your application subnet rather than leaving it open to the internet.
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for development and light workloads. The shipped configuration is tuned for that size. For production throughput choose a larger size such as Standard_E2s_v5 or above, and raise shared_buffers and effective_cache_size in /etc/postgresql/18/main/conf.d/10-cloudimg.conf to match.
Deploy the virtual machine
Create the VM from the image, opening SSH and the database port to your own network:
az vm create \
--resource-group my-rg \
--name percona-pg-1 \
--image <this-marketplace-image> \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
az vm open-port --resource-group my-rg --name percona-pg-1 --port 5432 --priority 900
First boot takes roughly thirty seconds longer than a plain VM because the cluster is created from scratch at that point rather than being shipped inside the image.
Retrieve your per VM credentials
On first boot the VM generates its own superuser password and TLS certificate. SSH in and read the root only credentials file:
sudo cat /root/percona-postgresql-credentials.txt
The file is mode 0600, owned by root, and lists the host, port, role, per VM password, default database and the required SSL mode, along with ready to paste connection strings. Nothing is baked into the image, so every deployed VM has its own password and its own certificate.

Confirm the service is healthy
Check that PostgreSQL is active and listening, that the cluster is running on the dedicated data volume, and that TLS is enabled:
sudo systemctl is-active postgresql@18-main.service
sudo ss -tln | grep ':5432'
pg_lsclusters
df -h /var/lib/percona-postgresql
sudo -u postgres psql -tAc 'SHOW ssl;'
You should see the service report active, PostgreSQL listening on 5432, the cluster online with its data directory at /var/lib/percona-postgresql/18/main, that path mounted on its own 40 GB volume, and ssl reported as on.

Connect to the database
On the VM, administration works with no password at all through the local unix socket, because pg_hba.conf uses peer authentication there:
sudo -u postgres psql -d appdb -c "SELECT current_database(), current_user;"
To exercise the same path your applications will use, read the per VM password straight from the credentials file and connect over TCP with TLS required:
PGPASSWORD="$(sudo sed -n 's/^postgres.password=//p' /root/percona-postgresql-credentials.txt)" \
psql "host=127.0.0.1 port=5432 dbname=appdb user=postgres sslmode=require" \
-c "SELECT version();" \
-c "SELECT ssl, version AS tls, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
The version banner confirms you are running Percona Server for PostgreSQL, and the pg_stat_ssl row confirms the connection is genuinely encrypted (ssl is t) and reports the negotiated TLS version and cipher. A wrong password on the same path is refused outright.

From a remote client, connect over TLS with the per VM password from the credentials file. Replace <vm-ip> with your VM's address and <POSTGRES_PASSWORD> with the value from the file:
PGPASSWORD=<POSTGRES_PASSWORD> psql "host=<vm-ip> port=5432 dbname=appdb user=postgres sslmode=require" -c "SELECT now();"
Use the Percona extension set
The distribution's extensions are already installed and pre loaded. List them, then read live query telemetry from pg_stat_monitor:
sudo -u postgres psql -d appdb -c '\dx'
sudo -u postgres psql -d appdb -c "SELECT left(query,60) AS query, calls, round(mean_exec_time::numeric,2) AS mean_ms FROM pg_stat_monitor WHERE query IS NOT NULL ORDER BY calls DESC LIMIT 10;"
sudo -u postgres psql -d appdb -c 'SHOW shared_preload_libraries;'
sudo -u postgres psql -d appdb -c 'SHOW pgaudit.log;'
pg_stat_monitor is Percona's replacement for pg_stat_statements. It aggregates statistics into time buckets and records client, plan and histogram detail alongside the usual call counts and timings, so you can see not just which statements are slow but when they were slow and from where. pgaudit.log ships set to ddl, role, which records schema changes and role or grant changes in the PostgreSQL log without the volume of full statement auditing. Raise it to write or all if your compliance regime requires it.

The demo table seeded on first boot is there to prove the cluster is live end to end. Read it, and drop it whenever you no longer need it:
sudo -u postgres psql -d appdb -c "SELECT * FROM cloudimg_demo;"
pg_repack and wal2json are installed but not enabled, because both change runtime behaviour and should be a deliberate choice. Enable either when you need it:
sudo -u postgres psql -d appdb -c "CREATE EXTENSION IF NOT EXISTS pg_repack;"
Create your application role and database
Do not point your application at the postgres superuser. Create a dedicated role and database for it instead, choosing your own password:
sudo -u postgres psql -c "CREATE ROLE appuser LOGIN PASSWORD 'choose-a-strong-password';"
sudo -u postgres psql -c "CREATE DATABASE myapp OWNER appuser;"
The new role authenticates exactly like the superuser does: scram-sha-256 over TLS from remote clients, and it is covered by the same pg_hba.conf rules with no further configuration.
Security posture
-
No baked credential, and no baked database. The captured image ships an empty data directory. The cluster, the
postgrespassword and the TLS certificate are all created on the customer's first boot and written to/root/percona-postgresql-credentials.txt(mode0600, root only). -
The database cannot start before it is initialised.
postgresql@18-main.serviceis enabled so it survives every reboot, but it carriesConditionPathExists=/var/lib/cloudimg/percona-postgresql-ready. First boot writes that marker only after the credentials file exists, so systemd physically refuses to serve an uninitialised cluster. Ordering alone would not give that guarantee. -
No
trustauthentication anywhere.pg_hba.confusespeeron the local unix socket,scram-sha-256on loopback TCP, andhostssl ... scram-sha-256for every remote address. A plaintext remote connection is refused before authentication is even attempted. -
Per VM TLS. Each VM mints its own self signed certificate on first boot, so no certificate material is shared between deployments. Replace it with a certificate from your own authority by pointing
ssl_cert_fileandssl_key_filein/etc/postgresql/18/main/conf.d/10-cloudimg.confat your files and reloading. -
Network. Expose only TCP 22 and TCP 5432 in your NSG, and restrict 5432 to your application subnet. The NSG is the first layer of defence, TLS the second and the per VM password the third.
To rotate the superuser password later, set a new one and update the credentials file to match:
sudo -u postgres psql -c "ALTER ROLE postgres PASSWORD 'your-new-password';"
Operations
Manage the database through systemd:
sudo systemctl is-active postgresql@18-main.service
systemctl show postgresql@18-main.service -p Id,ActiveState,SubState,MainPID
Inspect logs with sudo journalctl -u postgresql@18-main.service --no-pager | tail -20, or read the cluster log directly at /var/log/postgresql/postgresql-18-main.log. The first boot log is at /var/log/cloudimg-firstboot.log.
Configuration lives in /etc/postgresql/18/main/. cloudimg's tuning is isolated in conf.d/10-cloudimg.conf so it survives package upgrades; edit that file and reload with sudo systemctl reload postgresql@18-main.service for most settings, or restart for shared_buffers and shared_preload_libraries.
Take a logical backup of a database with pg_dump:
sudo -u postgres pg_dump appdb > /var/tmp/appdb-backup.sql
ls -lh /var/tmp/appdb-backup.sql
pg_dump writes schema and data only. Avoid pg_dumpall for routine backups unless you intend to capture role passwords as well, and treat any file it produces as a secret. To grow the database volume, resize the data disk in the Azure portal and then extend the filesystem with sudo resize2fs /dev/disk/azure/scsi1/lun0.
Support
Every cloudimg image includes 24/7 support. If you have any questions about this Percona Server for PostgreSQL image, contact us at support@cloudimg.co.uk.