Databases Azure

DBLab Engine on Ubuntu 24.04 on Azure User Guide

| Product: DBLab Engine on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of DBLab Engine on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images.

DBLab Engine (Database Lab Engine, DLE) is postgres.ai's database-branching engine for PostgreSQL. It takes a snapshot of a source database and then hands out thin clones: full-size, fully writable PostgreSQL instances that are created in seconds and cost almost nothing on disk, because they share their unchanged blocks with the snapshot and only store what you actually modify.

That single property changes how teams work with production-scale data. A destructive migration can be rehearsed against real data and thrown away. A slow query can be tuned against a real plan on a real dataset. Every engineer, and every CI job, can have their own copy of a database that would be far too large and far too slow to duplicate conventionally.

This image bundles the complete appliance on a single virtual machine:

  • DBLab Engine 4.1.3 — the engine and REST API on TCP 2345
  • The Community Edition web UI — served through nginx on TCP 80
  • A bundled demo source database — PostgreSQL 18 with a 50,000-row orders table, so the appliance clones something real the moment it boots
  • A dedicated ZFS pool — built at first boot on a separate 32 GiB data disk, which is what makes cloning copy-on-write

The appliance is self-demonstrating and standalone. It does not need an external database to be useful on day one: first boot creates the pool, seeds the demo source database, takes the first snapshot, and starts the engine. You can create your first thin clone minutes after the machine comes up, then repoint the engine at your own PostgreSQL when you are ready (Step 11).

What is included:

  • DBLab Engine 4.1.3 and the Community Edition UI (Apache Licence 2.0), pinned to immutable container image digests
  • ZFS from Ubuntu's kernel-shipped module, so a kernel upgrade can never leave the pool unimportable
  • A first-boot service that generates the verification token and the demo database password uniquely per virtual machine
  • A shipped self-test that proves copy-on-write cloning actually works, end to end

DBLab services and ZFS pool The five appliance services, the ZFS pool that clones are branched from, and the only two ports reachable from off the machine.

Security model

A DBLab engine can clone entire databases, so an unauthenticated engine is a data-disclosure hole rather than a mere misconfiguration. This image treats the API and the clones as privileged:

  • Only TCP 80 (the web UI) and TCP 22 (SSH) are reachable from the network.
  • The engine API (2345), the UI container (2346), the bundled source database (5432) and the entire thin-clone port range (6000-6099) bind 127.0.0.1 only. The clone range keeps upstream's cloneAccessAddresses: 127.0.0.1 default deliberately — a thin clone is a complete, writable copy of the source database, so exposing it on a public interface would publish the data it holds. You reach clones over an SSH tunnel (Step 8).
  • No default credential ships. The engine's verification token and the demo database password are generated per virtual machine at first boot and written to a 0600 root:root file. Neither the engine nor the source database can even start until first boot has minted them, because both units are gated on a bootstrap marker that first boot writes last.
  • The engine runs as Community Edition and never consults a billing path. The DLE_COMPUTING_INFRASTRUCTURE variable is deliberately never set on this image; setting it switches the engine to Standard Edition, where snapshotting and cloning silently stop working without an active postgres.ai billing subscription.

Prerequisites

  • An Azure subscription with permission to create virtual machines
  • An SSH key pair for administrative access
  • A network security group allowing inbound TCP 22 (SSH) and 80 (DBLab web UI) from your own address ranges
  • Recommended size: Standard_B2ms (2 vCPU, 8 GiB) or larger. The engine, the source database and every running clone are all PostgreSQL processes on one machine, so memory is the resource that matters as clone count grows.

Step 1: Deploy from the Azure Portal

  1. In the Azure Portal, choose Create a resource and search for DBLab Engine on Ubuntu 24.04 LTS by cloudimg.
  2. Select the offer and click Create.
  3. Choose your subscription, resource group and region.
  4. Set the virtual machine size to Standard_B2ms or larger.
  5. Under Administrator account, select SSH public key and supply your key. The admin username is azureuser.
  6. Under Inbound port rules, allow SSH (22) and HTTP (80).
  7. Review and create.

The image carries a dedicated data disk for the ZFS pool. It is provisioned automatically with the virtual machine — you do not need to add or format a disk.

Step 2: Deploy from the Azure CLI

# Create a resource group
az group create --name dblab-rg --location eastus

# Create the virtual machine from the cloudimg offer
az vm create \
  --resource-group dblab-rg \
  --name dblab-vm \
  --image cloudimg:dblab-engine:default:latest \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

# Open the web UI port
az vm open-port --resource-group dblab-rg --name dblab-vm --port 80 --priority 1010

# Retrieve the public IP address of the new virtual machine
az vm show -d --resource-group dblab-rg --name dblab-vm --query publicIps -o tsv

Step 3: Connect via SSH

ssh azureuser@<vm-ip>

First boot creates the ZFS pool, seeds the demo database, takes the first snapshot and starts the engine. That takes two to three minutes on a Standard_B2ms; the rest of this guide assumes it has finished.

Step 4: Verify the Services

Five services make up the appliance. All five should report active:

systemctl is-active docker nginx dblab-pool dblab-source-db dblab-server

Expected output:

active
active
active
active
active

Confirm the ZFS pool that clones are branched from is online and backed by the dedicated data disk:

zpool list
zfs list -t all

Expected output — the pool is ONLINE and its mount point is /var/lib/dblab/dblab_pool:

NAME         SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
dblab_pool  31.5G  36.6M  31.5G        -         -     0%     0%  1.00x    ONLINE  -

Step 5: Confirm Only the Web UI Is Exposed

Check what is actually listening. Only 22 and 80 hold a wildcard socket; everything else is bound to loopback:

ss -tlnH | awk '{print $1, $4}' | grep -vE '127.0.0.5[34]' | sort -u

Expected output:

LISTEN 0.0.0.0:22
LISTEN 0.0.0.0:80
LISTEN 127.0.0.1:2345
LISTEN 127.0.0.1:2346
LISTEN 127.0.0.1:5432
LISTEN [::]:22
LISTEN [::]:80

Prove that the loopback binding is a deliberate choice and not a dead service — nginx answers on the machine's own routable address while the engine port is refused there, and the engine answers happily on loopback:

IP=$(hostname -I | awk '{print $1}')
curl -s -o /dev/null -w "http://$IP/ -> HTTP %{http_code}\n" -m 8 "http://$IP/"
timeout 4 bash -c "exec 3<>/dev/tcp/$IP/2345" 2>/dev/null && echo "2345 REACHABLE" || echo "$IP:2345 -> connection refused"
curl -s -o /dev/null -w "127.0.0.1:2345 (no token) -> HTTP %{http_code}\n" http://127.0.0.1:2345/status

DBLab security posture and patch state The web UI answers on the routable address, every other port refuses there, the API rejects an unauthenticated caller, and the image ships fully patched with no swap.

Step 6: Retrieve the Verification Token

Every virtual machine generates its own credentials on first boot:

sudo cat /stage/scripts/dblab-engine-credentials.log

The file is 0600 root:root. It holds the web UI address, the verification token (which grants full control of the engine, including cloning entire databases — treat it as a secret), the API base URL, and the demo source database credentials.

Confirm the engine is healthy and running as Community Edition:

curl -s -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' http://127.0.0.1:2345/status \
  | jq '{status: .status.code, version: .engine.version, edition: .engine.edition, pool: .pools[0].name, mode: .pools[0].mode, retrieval: .retrieving.status}'

Expected output:

{
  "status": "OK",
  "version": "v4.1.3-20260508-1125",
  "edition": "community",
  "pool": "dblab_pool",
  "mode": "zfs",
  "retrieval": "finished"
}

DBLab credentials and engine status Per-VM credentials in a root-only file, and an engine reporting Community Edition on a ZFS pool.

Step 7: Run the Shipped Self-Test

The image ships an end-to-end self-test. It does not merely check that the API returns 200 — it takes the engine's snapshot, creates a real thin clone, confirms the clone carries the source data, writes 1,000 rows into the clone, then re-counts the source and asserts it is unchanged. That copy-on-write independence is the product:

sudo /usr/local/sbin/dblab-selftest.sh

DBLab self-test output A thin clone is created, written to, proven not to affect the source, and destroyed — with the unauthenticated API call correctly refused.

Step 8: Create Your First Thin Clone

Clones are created through the REST API. Ask the engine which snapshots exist, then branch a clone from the newest one:

SNAP=$(curl -s -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' \
  http://127.0.0.1:2345/snapshots | jq -r '.[0].id')
echo "cloning from snapshot: $SNAP"

curl -s -X POST -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d "{\"id\":\"my-clone\",\"snapshot\":{\"id\":\"$SNAP\"},\"protected\":false,\"db\":{\"username\":\"engineer\",\"password\":\"choose-a-password\"}}" \
  http://127.0.0.1:2345/clone | jq -c '{id: .id, status: .status.code}'

Poll until the clone reports OK and note the port it was given:

for i in $(seq 1 40); do
  CJ=$(curl -s -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' http://127.0.0.1:2345/clone/my-clone)
  ST=$(echo "$CJ" | jq -r '.status.code'); [ "$ST" = "OK" ] && break; sleep 3
done
echo "$CJ" | jq -c '{id: .id, status: .status.code, port: .db.port, user: .db.username}'

Connect to the clone on the port the engine assigned it. It is a complete, writable PostgreSQL instance carrying every row the source had:

PORT=$(curl -s -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' \
  http://127.0.0.1:2345/clone/my-clone | jq -r '.db.port')
PGPASSWORD='choose-a-password' psql -h 127.0.0.1 -p "$PORT" -U engineer -d demo \
  -c "SELECT count(*) AS rows_in_clone FROM orders"

Write to it, then confirm the source is untouched:

PORT=$(curl -s -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' \
  http://127.0.0.1:2345/clone/my-clone | jq -r '.db.port')
PGPASSWORD='choose-a-password' psql -h 127.0.0.1 -p "$PORT" -U engineer -d demo \
  -c "DELETE FROM orders WHERE id % 2 = 0" -c "SELECT count(*) AS rows_after_delete FROM orders"
PGPASSWORD='<DEMO_PASSWORD>' psql -h 127.0.0.1 -p 5432 -U postgres -d demo \
  -c "SELECT count(*) AS rows_in_source FROM orders"

The clone loses half its rows; the source still reports 50,000. Throw the clone away when you are done:

curl -s -X DELETE -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' \
  http://127.0.0.1:2345/clone/my-clone
sleep 5
curl -s -o /dev/null -w 'clone after delete -> HTTP %{http_code} (404 = reaped)\n' \
  -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' http://127.0.0.1:2345/clone/my-clone

Reaching a clone from your laptop

Clone ports are bound to loopback on purpose, because a clone holds a full copy of the source data. Open an SSH tunnel from your own machine and connect through it:

ssh -L 6000:127.0.0.1:6000 azureuser@<vm-ip>

Then point any PostgreSQL client at localhost:6000. Substitute the port the engine reported for your clone if it is not the first one on the machine.

Step 9: Use the Web UI

Open http://<vm-ip>/ in a browser. The Community Edition UI asks for the verification token from Step 6:

DBLab verification token prompt The CE UI authenticates with the per-VM verification token. There is no default login to guess.

After authenticating you land on the instance overview — cloning summary, engine version, retrieval status, and the ZFS pool with its free space:

DBLab instance overview Average cloning time, live clone count, engine version and pool health, with the clone list underneath.

The Snapshots tab shows what makes thin cloning cheap. The snapshot below carries 55.476 MiB of logical data but occupies 64 KiB of physical space, and one clone is already branched from it:

DBLab snapshots Logical size versus physical size — the copy-on-write saving, made visible.

The Clones tab lists every live clone with the port and database user needed to connect, plus the diff size each one has accumulated since it was branched:

DBLab clones A live thin clone on port 6000, with the data it has diverged by since branching.

You can also create and destroy clones from this screen with Create clone, which is the same API call as Step 8.

Step 10: How Snapshots Are Refreshed

The engine re-dumps and re-restores the source database on a schedule, then takes a fresh snapshot. On this image the schedule is weekly:

sudo grep -A3 '^retrieval:' /etc/dblab/server.yml

Older snapshots are retained so that clones branched from them keep working. To take a snapshot immediately rather than waiting for the schedule, use Full refresh on the instance overview, or the /full-refresh API endpoint.

Step 11: Clone Your Own Database

The bundled demo database exists so the appliance can demonstrate itself. To clone your real data, repoint the engine's retrieval configuration at your own PostgreSQL server.

Look at the logicalDump job in /etc/dblab/server.yml — this is the part you change:

sudo sed -n '/logicalDump:/,/logicalRestore:/p' /etc/dblab/server.yml

Open that file in your editor of choice (sudo vi /etc/dblab/server.yml) and replace the bundled source with your server's connection details:

    - name: logicalDump
      options:
        source:
          type: remote
          connection:
            host: your-postgres-host.example.com
            port: 5432
            dbname: your_database
            username: your_readonly_user
            password: your_password

Then reload the engine and trigger a refresh:

sudo systemctl restart dblab-server

A few practical notes:

  • Use a read-only role for the dump connection. DBLab only ever reads from your source.
  • The pool must hold the logical size of your database plus the divergence of every live clone. Resize the data disk before pointing the engine at a database larger than the 32 GiB pool this image ships with.
  • Larger databases take proportionally longer for the first dump and restore. Watch progress with journalctl -u dblab-server -f.
  • For very large databases, postgres.ai documents a physical (physicalRestore) retrieval mode that attaches to a replica instead of dumping. See the upstream configuration reference.

Step 12: Grow the Pool

The ZFS pool lives on a dedicated Azure data disk. To enlarge it, expand the disk in Azure and then tell ZFS to use the extra space:

sudo zpool set autoexpand=on dblab_pool
sudo zpool online -e dblab_pool /dev/disk/azure/scsi1/lun0
zpool list

The device is always addressed through /dev/disk/azure/scsi1/lun0 rather than /dev/sdX, because Linux device letters can shift between boots on Azure.

Step 13: Managing the Services

# Status
systemctl status dblab-server --no-pager

# Engine logs
sudo journalctl -u dblab-server -n 50 --no-pager

# Container logs from the engine itself
sudo docker logs --tail 50 dblab_server

# The bundled demo source database
systemctl status dblab-source-db --no-pager

The engine, the UI and the source database all run as containers pinned to immutable image digests, pre-pulled into the image, so a customer virtual machine never needs to reach a container registry to start.

Step 14: Troubleshooting

First boot did not complete. The engine and the source database are deliberately gated behind a marker that first boot writes only after minting credentials, so neither can start with a default secret:

systemctl status dblab-engine-firstboot.service --no-pager
sudo journalctl -u dblab-engine-firstboot.service -n 40 --no-pager

The engine reports no pools. The pool is created at first boot on the dedicated data disk. Confirm the disk is present and the pool imported:

ls -l /dev/disk/azure/scsi1/lun0
zpool status
findmnt /var/lib/dblab/dblab_pool

A clone will not start. Clones are PostgreSQL containers; if the machine is out of memory they fail to come up. Check what the engine recorded:

curl -s -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' http://127.0.0.1:2345/status \
  | jq '{numClones: .cloning.numClones, clones: [.cloning.clones[] | {id, status: .status.code}]}'
free -h

Each running clone is a full PostgreSQL process. Move to a larger virtual machine size before running many clones concurrently.

Cloning or snapshotting silently stops working. Check that the engine still reports community:

curl -s -H 'Verification-Token: <DBLAB_VERIFICATION_TOKEN>' http://127.0.0.1:2345/status | jq -r '.engine.edition'

If it reports standard, the DLE_COMPUTING_INFRASTRUCTURE environment variable has been set somewhere. Standard Edition requires an active postgres.ai billing subscription and refuses to snapshot or clone without one. This image never sets that variable; leave it unset.

Step 15: Security Recommendations

  • Restrict inbound 80 and 22 to your own address ranges in the network security group.
  • Put TLS in front of the web UI before exposing it broadly — terminate at an Azure Application Gateway, or add a certificate to the nginx vhost at /etc/nginx/sites-available/dblab.
  • Treat the verification token as a root-equivalent secret. It can clone any database the engine can reach. Rotate it by editing server.verificationToken in /etc/dblab/server.yml and restarting dblab-server.
  • Leave the clone port range bound to loopback and reach clones over an SSH tunnel. A clone is a complete copy of your source data.
  • Use a read-only role for the engine's connection to your source database.
  • Keep the machine patched: sudo apt update && sudo apt upgrade.

Step 16: Support and Licensing

DBLab Engine and its Community Edition UI are distributed under the Apache Licence 2.0. PostgreSQL is distributed under the PostgreSQL Licence. There are no subscription or licence fees for the software in this image.

postgres.ai sells DBLab Standard Edition, which adds the hosted postgres.ai console, organisation-level access control and vendor support. It does not unlock engine functionality — snapshotting, branching and thin cloning are all Community Edition features and all work on this image.

cloudimg provides 24/7 support for the image itself, its first-boot provisioning and its configuration.

Deploy on Azure

Find DBLab Engine on Ubuntu 24.04 LTS in the Azure Marketplace.

Need Help?

Contact cloudimg support at support@cloudimg.co.uk.