Databases Azure

Supavisor on Ubuntu 24.04 on Azure User Guide

| Product: Supavisor Postgres Connection Pooler on Ubuntu 24.04 LTS on Azure

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 6543 and session mode on port 5452

  • The Supavisor management and metrics API on 127.0.0.1:4000 (tenant CRUD plus a Prometheus /metrics endpoint), 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

Terminal showing supavisor.service and postgresql.service both reporting active, followed by the listening sockets: the transaction pooler on 0.0.0.0 port 6543 and the session pooler on 0.0.0.0 port 5452 are public while the management and metrics API on 127.0.0.1 port 4000 and PostgreSQL on 127.0.0.1 port 5432 are bound to loopback only

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;"

Terminal showing a psql connection through the pooler on port 6543 as user demo_user dot demo, returning the current user demo_user, the upstream port 5432 and PostgreSQL 16.14, and then the seeded greeting table row reading hello from a Supavisor pooled connection, proving the client is proxied to the upstream database

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;"

Terminal showing the Supavisor metadata: the tenants table with the demo tenant pointing at db_host 127.0.0.1 port 5432 database demo with require_user true, and the users table showing the demo tenant user demo_user in transaction mode with a pool size of five

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;"

Terminal showing eight client connections opened through the pooler, then a query against pg_stat_activity on the upstream demo database showing that they were multiplexed onto a single native backend connection whose application_name is Supavisor, demonstrating transaction mode connection pooling

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 to 127.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 /metrics endpoint is served on 127.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.