Supavisor on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and use of Supavisor on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Supavisor is Supabase's cloud native, scalable Postgres connection pooler. It absorbs very large numbers of client connections and multiplexes them onto a small, stateful pool of native PostgreSQL connections, in the same class as PgBouncer, with transaction and session pooling modes and a multi tenant model.
To make the appliance useful the moment it boots, the image also installs a local PostgreSQL 16 instance (from the official PostgreSQL PGDG repository) and wires up one pre configured demo tenant in front of it. On first boot Supavisor creates its metadata schema, starts pooling and seeds the demo tenant, so you can run a pooled query immediately and then point Supavisor at your own PostgreSQL databases.
How it works. Clients connect to Supavisor using the username form <db_user>.<tenant> (for the demo tenant, demo_user.demo). Supavisor looks the tenant up in its metadata database, opens or reuses a pool of native connections to that tenant's upstream PostgreSQL, and proxies your session through it. The port selects the pool mode: 6543 is transaction mode (the Supabase convention and the primary service surface) and 5452 is session mode, both with the same credential.
Security by design — only the pooler is public. The bundled PostgreSQL listens on 127.0.0.1 only, and Supavisor's management and metrics API on port 4000 is also bound to loopback only. The only ports you expose in your Network Security Group are TCP 22 for administration and TCP 6543 for the pooler (plus 5452 if you use session mode). The per tenant credential gates all access; PostgreSQL itself is never reachable except through the pooler.
Security by design — no baked credential. Nothing is baked into the image. On first boot every secret is generated uniquely for the VM: the PostgreSQL superuser password, the supavisor metadata role password and the demo_user tenant password, plus Supavisor's own SECRET_KEY_BASE, its 32 byte VAULT_ENC_KEY (which encrypts stored tenant passwords), the API and metrics JWT secrets and the Erlang release cookie. The live values are written to /etc/supavisor/supavisor.env (mode 0640, readable only by root and the service user) and summarised in the root only file /root/supavisor-credentials.txt. The metadata and demo databases are reset when the image is built, so every deployed VM starts from a clean state with fresh secrets.
What is included:
-
Supavisor 2.9.10 built from source as a self contained Erlang/OTP release at
/opt/supavisor, running under systemd -
A bundled local PostgreSQL 16 instance from the PGDG repository, serving both Supavisor's metadata database and the demo tenant, listening on the loopback interface only
-
A pre configured demo tenant so a pooled query works on first boot, with transaction mode on port
6543and session mode on port5452 -
The Supavisor management and metrics API on
127.0.0.1:4000(tenant CRUD plus a Prometheus/metricsendpoint), bound to loopback and JWT protected -
Per VM secrets generated on first boot, stored in a root and service only environment file and summarised in a root only file, never baked into the image
-
Unattended security upgrades left enabled so the appliance keeps receiving patches
Prerequisites
-
Active Azure subscription, SSH public key, VNet + subnet in the target region
-
Subscription to the Supavisor listing on Azure Marketplace
-
A Network Security Group allowing TCP 22 (admin) and TCP 6543 (the transaction pooler) from your client network, plus TCP 5452 if you use session mode. No PostgreSQL port is exposed by this appliance.
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for the demo and light pooling. For production pooling in front of a busy database, use Standard_D2s_v5 or larger so the Erlang VM has headroom for many thousands of client connections.
Step 1: Deploy from the Azure Portal
Search Supavisor in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 22 for administration and TCP 6543 for the pooler from your client network. Add TCP 5452 as well if you plan to use session mode. No inbound PostgreSQL port is required — the bundled PostgreSQL is loopback only and is reached only through the pooler.
Step 2: Deploy from the Azure CLI
RG="pooler-prod"; LOCATION="eastus"; VM_NAME="supavisor1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/supavisor-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$GALLERY_IMAGE_ID" \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku Standard
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 6543 --priority 1002
Step 3: Retrieve the connection details
SSH in as azureuser and read the root only credentials file. It contains the demo tenant's per VM password and the ready to use pooler connection string.
sudo cat /root/supavisor-credentials.txt
Step 4: Confirm the pooler is healthy
Check that both services are active and see which ports are public. Only the pooler ports listen on all interfaces; the PostgreSQL port and the management API stay on loopback.
sudo systemctl is-active supavisor postgresql
sudo ss -tlnp | grep -E ':(6543|5452|4000|5432)' | awk '{print $4}' | sort -u

Step 5: Run a query through the pooler
Connect to the demo tenant through the transaction pooler on port 6543. The demo password is read from the credentials file for this example; from your own machine you would use the connection string printed in Step 3. The query shows that the client is proxied to the upstream PostgreSQL and returns real rows.
PW=$(sudo sed -n 's/^pooler.demo.password=//p' /root/supavisor-credentials.txt)
PGPASSWORD="$PW" psql "host=127.0.0.1 port=6543 dbname=demo user=demo_user.demo sslmode=disable" \
-c "SELECT current_user, inet_server_port() AS upstream_port, substring(version() from 'PostgreSQL [0-9.]+') AS server;"
PGPASSWORD="$PW" psql "host=127.0.0.1 port=6543 dbname=demo user=demo_user.demo sslmode=disable" \
-c "SELECT id, message, created_at FROM greeting;"

Step 6: Inspect the tenant configuration
Supavisor stores its tenants and their users in the _supavisor schema of the metadata database. This is the multi tenant model: each tenant maps to an upstream PostgreSQL, and each tenant user has a pool size and a pool mode.
sudo -u postgres psql -d supavisor \
-c "SELECT external_id AS tenant, db_host, db_port, db_database, require_user FROM _supavisor.tenants;"
sudo -u postgres psql -d supavisor \
-c "SELECT tenant_external_id AS tenant, db_user, mode_type, pool_size FROM _supavisor.users;"

Step 7: See connection pooling in action
Open several client connections through the pooler in quick succession, then look at the upstream PostgreSQL. Because transaction pooling multiplexes many clients onto a small pool, the many client connections are served by only a handful of native backend connections, all labelled Supavisor.
PW=$(sudo sed -n 's/^pooler.demo.password=//p' /root/supavisor-credentials.txt)
for i in $(seq 1 8); do PGPASSWORD="$PW" psql "host=127.0.0.1 port=6543 dbname=demo user=demo_user.demo sslmode=disable" -tAc 'select 1;' >/dev/null; done
sudo -u postgres psql -d demo \
-c "SELECT usename, application_name, state, count(*) AS upstream_backends FROM pg_stat_activity WHERE usename='demo_user' GROUP BY 1,2,3;"

Step 8: Connect your own application
From your application or workstation, connect through the pooler using the connection string from Step 3. Use the transaction pooler on port 6543 for serverless and high concurrency workloads, or the session pooler on port 5452 when you need a full PostgreSQL session (the same credential works on both; the port selects the mode).
# transaction mode (recommended for serverless / high concurrency)
psql "postgres://demo_user.demo:<password>@<public-ip>:6543/demo"
# session mode (a full PostgreSQL session; open TCP 5452 in your NSG first)
psql "postgres://demo_user.demo:<password>@<public-ip>:5452/demo"
Step 9: Point Supavisor at your own database
To pool your own PostgreSQL, add a tenant through the loopback management API. Sign a JWT with the API_JWT_SECRET from /etc/supavisor/supavisor.env, then create a tenant that references your upstream database and one or more users. The example below is illustrative; replace the bearer token with your signed JWT.
curl -sX PUT http://127.0.0.1:4000/api/tenants/myapp \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{"tenant":{"db_host":"your-db.example.com","db_port":5432,"db_database":"appdb","require_user":true,"users":[{"db_user":"app","db_password":"<db-password>","pool_size":15,"mode_type":"transaction"}]}}'
Clients then connect as app.myapp on port 6543. See the Supavisor documentation for the full tenant and metrics API.
Security notes
-
Only the pooler is public. PostgreSQL (
5432) and the Supavisor management and metrics API (4000) are bound to127.0.0.1. Expose only TCP 22 and TCP 6543 (and 5452 for session mode) in your Network Security Group. -
No baked credential. Every database password and every Supavisor secret is generated on first boot and stored only in
/etc/supavisor/supavisor.env(root and service user) and/root/supavisor-credentials.txt(root only). Stored tenant passwords are encrypted with a per VM key. -
Metrics. A Prometheus
/metricsendpoint is served on127.0.0.1:4000, protected by a per VM metrics JWT secret. Scrape it locally or through a tunnel; do not expose port 4000. -
Keep it patched. Unattended security upgrades remain enabled so the base OS keeps receiving updates.
Support
This image is maintained by cloudimg with 24/7 support. For deployment help or issues, contact support through the cloudimg listing on Azure Marketplace.