IvorySQL 5.4 on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and use of IvorySQL 5.4 on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images.
IvorySQL is HighGo's Oracle compatible distribution of PostgreSQL. This release is built on PostgreSQL 18.4, so you get the complete, unmodified PostgreSQL engine — the same SQL, the same MVCC, the same drivers, the same extension ecosystem — and on top of it an Oracle compatibility layer providing PL/iSQL, Oracle data types such as number, varchar2 and date, packages, the sys schema, and Oracle built ins including dual, sysdate, nvl and to_char.
The single most important thing to understand about IvorySQL is that one cluster answers on two ports.
| Port | Mode | SQL dialect |
|---|---|---|
5432 |
native PostgreSQL (ivorysql.compatible_mode = pg) |
standard PostgreSQL |
1521 |
Oracle compatibility (ivorysql.compatible_mode = oracle) |
Oracle SQL and PL/iSQL |
Both ports serve the same databases and the same data. A table created through the Oracle port is immediately visible through the PostgreSQL port and vice versa. The port your client connects to decides which dialect the parser uses — it is not a configuration setting you turn on, and putting ivorysql.compatible_mode into postgresql.conf will stop the server from starting. This means a migrating Oracle application and a native PostgreSQL reporting tool can share one database without either of them being rewritten.
Security by design — the image ships with no database at all. There is no cluster in the captured image: no data directory, no pg_hba.conf, no roles, no TLS key and no credential of any kind. On first boot, and only on first boot, each VM runs initdb for itself and then:
-
writes
pg_hba.confwithpeerfor the local socket andscram-sha-256for TCP, so the passwordlesstrustauthentication that stock PostgreSQL writes by default never exists on your VM -
generates a unique 32 character superuser password for this VM and applies it to every role that is able to log in, enumerated from the catalogue at run time rather than from a fixed list
-
generates a unique self signed TLS certificate, so no two VMs share a private key
-
writes everything to the root only file
/root/ivorysql-credentials.txtand only then creates the marker that allowsivorysql.serviceto start
Because ivorysql.service refuses to start until that marker exists, and because the bootstrap runs with no TCP listener at all on either port, there is no window in which the database is reachable before it has a password.
Both listeners are bound to loopback by default. In the shipped image 5432 and 1521 listen on 127.0.0.1 only, so a freshly deployed VM is not exposed on its Azure address even if the Network Security Group is open. Exposing the database is a deliberate step, documented below.
What is included:
-
IvorySQL 5.4 (PostgreSQL 18.4) compiled from the pinned upstream release tag
IvorySQL_5.4, installed under/usr/local/ivorysql/ivorysql-5and running under systemd asivorysql.service -
The Oracle compatibility extensions
ivorysql_oraandplisql, plusuuid-osspandgb18030_2022, enabled in the defaultivorysqldatabase -
A per VM superuser password and a per VM TLS certificate generated on first boot into a root only credentials file
-
A shipped self check,
ivorysql-selfcheck.sh, that verifies the whole security posture and the Oracle compatibility layer in one command -
No C toolchain in the image — the compiler and every development header used to build IvorySQL are removed before capture
-
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. You do not need to open
5432or1521to use the database on the VM itself, because both listeners are bound to loopback in the shipped image.
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for development, evaluation and light workloads; the shipped configuration sets shared_buffers to 512 MB to suit it. IvorySQL's own documentation recommends 4 vCPU or more for production, so for real workloads choose a larger size such as Standard_D4s_v5 and raise shared_buffers and effective_cache_size to match.
Deploy the virtual machine
Create the VM from the image, opening only SSH to your own network:
az vm create \
--resource-group my-resource-group \
--name my-ivorysql-vm \
--image <marketplace-image-urn> \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard \
--location eastus
# Then allow SSH from your own address only
az vm open-port \
--resource-group my-resource-group \
--name my-ivorysql-vm \
--port 22 --priority 1001
Connect and confirm the service is healthy
SSH to the VM as azureuser, then confirm both units are up. ivorysql-firstboot.service is a one shot that stays in the active (exited) state once it has done its work, and ivorysql.service is the database server itself:
systemctl is-active ivorysql.service ivorysql-firstboot.service
systemctl status ivorysql.service --no-pager --lines=0 | head -6
Expected output:
active
active
● ivorysql.service - IvorySQL 5.4 database server (Oracle-compatible PostgreSQL 18.4)
Loaded: loaded (/etc/systemd/system/ivorysql.service; enabled; preset: enabled)
Active: active (running) since Sun 2026-07-26 05:43:22 UTC; 36s ago
Docs: https://www.cloudimg.co.uk/guides/ivorysql-5-on-ubuntu-24-04-azure/
https://docs.ivorysql.org/
Process: 28161 ExecStartPre=/bin/mkdir -p /run/ivorysql (code=exited, status=0/SUCCESS)
Confirm the server version. This is the definitive check that you are running IvorySQL and not plain PostgreSQL — the banner reports both version numbers:
sudo -iu ivorysql psql -d ivorysql -w -c "SELECT version();"
version
------------------------------------------------------------------------------------------------------------------------
PostgreSQL 18.4 (IvorySQL 5.4) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, 64-bit
(1 row)
Now look at the two listeners. Both are bound to 127.0.0.1, which is the shipped default:
ss -tlnp | grep -E "5432|1521"
LISTEN 0 200 127.0.0.1:1521 0.0.0.0:* users:(("postgres",pid=28167,fd=7))
LISTEN 0 200 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=28167,fd=6))

Retrieve the per VM credentials
The superuser password was generated on this VM's first boot and exists nowhere else. It is written to a root only file:
sudo stat -c '%a %U:%G' /root/ivorysql-credentials.txt
sudo grep -E '^ivorysql\.(role|database|host|port|oracle_port|sslmode)=' /root/ivorysql-credentials.txt
600 root:root
ivorysql.role=ivorysql
ivorysql.database=ivorysql
ivorysql.host=127.0.0.1
ivorysql.port=5432
ivorysql.oracle_port=1521
ivorysql.sslmode=require
To read the password itself:
sudo cat /root/ivorysql-credentials.txt
Store it somewhere safe. The file is the only place it is written in plain text.

Verify the deployment in one command
The image ships a self check that exercises the whole security posture and the Oracle compatibility layer. Run it any time — it is read only apart from a temporary table it removes again:
sudo /usr/local/sbin/ivorysql-selfcheck.sh
It authenticates with the per VM credential over TLS, confirms the version pin, proves both ports are in the right mode, runs a PL/iSQL package, confirms pg_hba.conf contains no trust rule, enumerates every login capable role and proves each rejects its own name, the empty password and a list of published defaults, and confirms both ports refuse a connection on the VM's routable address. It finishes with a single RESULT healthy line.

Open a native PostgreSQL session on port 5432
Port 5432 behaves exactly like stock PostgreSQL. The ivorysql OS account authenticates over the local unix socket with peer, so no password is needed on the VM itself:
sudo -iu ivorysql psql -d ivorysql -w -c "SHOW ivorysql.compatible_mode;"
sudo -iu ivorysql psql -d ivorysql -w -c "SELECT rolname, rolsuper, rolcanlogin FROM pg_roles WHERE rolcanlogin ORDER BY 1;"
ivorysql.compatible_mode
--------------------------
pg
(1 row)
rolname | rolsuper | rolcanlogin
----------+----------+-------------
ivorysql | t | t
(1 row)
There is exactly one login capable role. Everything else in pg_roles is one of PostgreSQL's built in pg_* predefined roles, which cannot log in.
Four databases exist — the three PostgreSQL always creates plus IvorySQL's own default ivorysql database. Nothing has been seeded:
sudo -iu ivorysql psql -d ivorysql -w -c "SELECT datname FROM pg_database ORDER BY 1;"
sudo -iu ivorysql psql -d ivorysql -w -c "SELECT extname, extversion FROM pg_extension ORDER BY 1;"
datname
-----------
ivorysql
postgres
template0
template1
(4 rows)
extname | extversion
--------------+------------
gb18030_2022 | 1.0
ivorysql_ora | 1.0
plisql | 1.0
plpgsql | 1.0
uuid-ossp | 1.1
(5 rows)
To connect over TCP with the per VM password instead of the socket, read the password out of the credentials file:
PW=$(sudo sed -n 's/^ivorysql\.password=//p' /root/ivorysql-credentials.txt)
sudo -u ivorysql /usr/local/ivorysql/ivorysql-5/bin/psql -w \
"host=127.0.0.1 port=5432 dbname=ivorysql user=ivorysql password=$PW sslmode=require" \
-c "SELECT ssl, version AS tls FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
ssl comes back as t, confirming the session really is encrypted rather than merely having asked for encryption.
Open an Oracle compatibility session on port 1521
Connect to port 1521 and the very same server parses Oracle SQL:
sudo -iu ivorysql psql -d ivorysql -p 1521 -w -c "SHOW ivorysql.compatible_mode;"
sudo -iu ivorysql psql -d ivorysql -p 1521 -w -c "SELECT to_char(sysdate, 'YYYY-MM-DD') AS today FROM dual;"
ivorysql.compatible_mode
--------------------------
oracle
(1 row)
today
------------
2026-07-26
(1 row)
dual and sysdate are Oracle constructs that do not exist in PostgreSQL. On port 5432 the same query is correctly rejected, which is the clearest demonstration that the two dialects really are separate:
sudo -iu ivorysql psql -d ivorysql -w -c "SELECT 1 FROM dual;" 2>/dev/null || echo "rejected on the native port, as expected"
rejected on the native port, as expected
Work with Oracle data types and PL/iSQL packages
Create a table using Oracle types through the Oracle port:
sudo -iu ivorysql psql -d ivorysql -p 1521 -w <<'SQL'
CREATE TABLE demo_emp (
id number(6) PRIMARY KEY,
name varchar2(40) NOT NULL,
salary number(10,2),
hired date
);
INSERT INTO demo_emp VALUES (1, 'Ada Lovelace', 72000.00, sysdate);
INSERT INTO demo_emp VALUES (2, 'Grace Hopper', 81000.50, sysdate);
INSERT INTO demo_emp VALUES (3, 'Alan Turing', NULL, sysdate);
SELECT id, name, nvl(to_char(salary, 'FM999999.00'), 'unset') AS salary FROM demo_emp ORDER BY id;
SQL
CREATE TABLE
INSERT 0 1
INSERT 0 1
INSERT 0 1
id | name | salary
----+--------------+----------
1 | Ada Lovelace | 72000.00
2 | Grace Hopper | 81000.50
3 | Alan Turing | unset
(3 rows)
number(6), varchar2(40) and nvl() are Oracle spellings; IvorySQL maps them onto real PostgreSQL storage in the sys schema.
Now create a package — a named container for functions and procedures, and one of the Oracle features PostgreSQL has no equivalent for. Note the / terminator, exactly as in Oracle's SQL*Plus:
sudo -iu ivorysql psql -d ivorysql -p 1521 -w <<'SQL'
CREATE OR REPLACE PACKAGE hr_pkg IS
FUNCTION total_payroll RETURN number;
END hr_pkg;
/
CREATE OR REPLACE PACKAGE BODY hr_pkg IS
FUNCTION total_payroll RETURN number IS
t number;
BEGIN
SELECT sum(nvl(salary, 0)) INTO t FROM demo_emp;
RETURN t;
END;
END hr_pkg;
/
SELECT to_char(hr_pkg.total_payroll(), 'FM999999.00') AS total_payroll FROM dual;
SQL
CREATE PACKAGE
CREATE PACKAGE BODY
total_payroll
---------------
153000.50
(1 row)

One cluster, two dialects
The rows you just inserted through the Oracle port are ordinary PostgreSQL rows. Query them through the native port:
sudo -iu ivorysql psql -d ivorysql -w -c "SELECT id, name, salary FROM demo_emp ORDER BY id;"
id | name | salary
----+--------------+----------
1 | Ada Lovelace | 72000.00
2 | Grace Hopper | 81000.50
3 | Alan Turing |
(3 rows)
The only difference is presentation: the Oracle session rendered the NULL salary through nvl(), while the native session shows it as an empty PostgreSQL NULL. This is what makes IvorySQL useful for a migration — Oracle application code keeps talking Oracle on 1521 while your PostgreSQL reporting, backup and monitoring tooling keeps talking PostgreSQL on 5432, against one set of tables.
Remove the demonstration objects when you are done:
sudo -iu ivorysql psql -d ivorysql -p 1521 -w -c "DROP PACKAGE IF EXISTS hr_pkg;" -c "DROP TABLE IF EXISTS demo_emp;"
DROP PACKAGE
DROP TABLE
Security posture
The authentication rules generated on first boot are deliberately minimal. Note that no line uses trust, PostgreSQL's passwordless method, which is what a default initdb would otherwise have written for both local and TCP connections:
sudo grep -vE '^[[:space:]]*(#|$)' /var/lib/ivorysql/5/data/pg_hba.conf
local all all peer
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256
local replication all peer
host replication all 127.0.0.1/32 scram-sha-256
host replication all ::1/128 scram-sha-256
You can confirm for yourself that a wrong password is refused:
sudo -u ivorysql /usr/local/ivorysql/ivorysql-5/bin/psql -w \
"host=127.0.0.1 port=5432 dbname=ivorysql user=ivorysql password=not-the-password sslmode=require connect_timeout=8" \
-c "SELECT 1;" </dev/null 2>/dev/null || echo "wrong password refused, as expected"
wrong password refused, as expected
And that the database is not listening on the VM's own routable address:
ADDR=$(hostname -I | awk '{print $1}')
sudo -u ivorysql /usr/local/ivorysql/ivorysql-5/bin/psql -w \
"host=$ADDR port=5432 dbname=ivorysql user=ivorysql password=x sslmode=require connect_timeout=6" \
-c "SELECT 1;" </dev/null 2>/dev/null || echo "not reachable on $ADDR, as expected"
not reachable on 10.0.0.20, as expected
Exposing IvorySQL to remote clients
The shipped image is loopback only. Exposing the database is a deliberate, three part change: widen the listener, add a pg_hba.conf rule for your client network, and open the Network Security Group. Replace <your-mgmt-cidr> with your own CIDR — never use 0.0.0.0/0.
Edit the managed drop in rather than postgresql.conf, so upstream's file stays pristine:
sudo tee -a /var/lib/ivorysql/5/data/conf.d/cloudimg.conf >/dev/null <<'CONF'
listen_addresses = '*'
ivorysql.listen_addresses = '*'
CONF
sudo tee -a /var/lib/ivorysql/5/data/pg_hba.conf >/dev/null <<'HBA'
hostssl all all <your-mgmt-cidr> scram-sha-256
HBA
sudo systemctl restart ivorysql.service
Then open the ports you actually need to that same CIDR, and only that CIDR:
az network nsg rule create \
--resource-group my-resource-group \
--nsg-name my-ivorysql-vm-nsg \
--name allow-ivorysql \
--priority 1100 --protocol Tcp \
--destination-port-ranges 5432 1521 \
--source-address-prefixes <your-mgmt-cidr> \
--access Allow
Because the rule you added is hostssl, remote clients are accepted only over TLS and only with the per VM password. The certificate generated on first boot is self signed, so clients should use sslmode=require (encrypt without verifying the issuer) or be given the certificate to pin. For a publicly trusted certificate, replace server.crt and server.key in the data directory with your own pair and reload.
Rotating the superuser password
Choose your own password and update the credentials file to match:
sudo -iu ivorysql psql -d ivorysql -w -c "ALTER ROLE ivorysql PASSWORD '<new-password>';"
sudo sed -i "s|^ivorysql.password=.*|ivorysql.password=<new-password>|" /root/ivorysql-credentials.txt
Managing the service
sudo systemctl status ivorysql.service --no-pager --lines=0
sudo journalctl -u ivorysql.service -n 30 --no-pager
The server's configuration lives in /var/lib/ivorysql/5/data. Put your own settings in conf.d/cloudimg.conf, which is included last so its values win, and restart the service to apply them. Two settings are worth knowing about:
-
ivorysql.portsets the Oracle compatibility port,1521by default -
ivorysql.compatible_modeis reported per connection and is chosen by the port you connect to. Do not set it in a configuration file — the native postmaster rejects it at startup withcannot be changed in native PG modeand the server will not boot.
Backups
IvorySQL ships the standard PostgreSQL utilities under /usr/local/ivorysql/ivorysql-5/bin, so the familiar tools work unchanged:
sudo -iu ivorysql pg_dump -d ivorysql -Fc -f /var/lib/ivorysql/ivorysql-$(date +%F).dump
sudo -iu ivorysql pg_dumpall --globals-only -f /var/lib/ivorysql/globals-$(date +%F).sql
Restore with pg_restore. For point in time recovery, enable WAL archiving in conf.d/cloudimg.conf and ship the archive to Azure Blob Storage.
Troubleshooting
ivorysql.service will not start and reports a failed condition. The service is gated on /var/lib/cloudimg/ivorysql-bootstrap-ready, which first boot creates only after the cluster exists and the password has been set. Check that first boot finished:
systemctl status ivorysql-firstboot.service --no-pager --lines=0
sudo tail -20 /var/log/cloudimg-firstboot.log
SELECT ... FROM dual fails with relation "dual" does not exist. You are connected to port 5432, which is the native PostgreSQL port. Reconnect with -p 1521.
A client cannot connect from another machine. By design — both listeners are bound to loopback in the shipped image. Follow "Exposing IvorySQL to remote clients" above.
The server refuses to start after a configuration change. Check the journal; the most common cause is ivorysql.compatible_mode having been added to a configuration file, which the native postmaster rejects at startup.
Support
cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk with the VM name, region and the output of sudo /usr/local/sbin/ivorysql-selfcheck.sh.
Upstream project documentation: docs.ivorysql.org. IvorySQL is licensed under Apache 2.0, with the inherited PostgreSQL code under the PostgreSQL licence.