Rq
Databases Azure

rqlite on Ubuntu 24.04 LTS on Azure

| Product: rqlite on Ubuntu 24.04 LTS by cloudimg

This image ships rqlite 10.2.7 on Ubuntu 24.04 LTS, configured and ready to answer queries from the first boot of every deployed virtual machine.

rqlite is a lightweight, distributed relational database that uses SQLite as its storage engine and the Raft consensus protocol to replicate writes across nodes. You get real relational SQL with transactions and the full SQLite type system, reached over a simple HTTP API that returns JSON, plus an interactive command line client. It runs perfectly well as a single node, which is how this image ships, and scales out to a fault tolerant cluster when you need one.

What makes this image different

The image contains no database at all. Most database images ship a database that was created on the build machine, which means build time rows, a schema and, worse, a build time credential ride into every customer virtual machine. This image does not. The entire data directory, holding the SQLite database, the Raft log, the Raft snapshots and the node identity, is destroyed before capture. A first boot service creates a pristine data directory on your virtual machine and generates a credential unique to it. There is no default login anywhere in the image and nothing to inherit.

Authentication is on, from the first packet. Out of the box, rqlite ships with no authentication at all: if no authentication file is configured, every request is permitted. This image never runs that way. First boot generates a random 32 character credential unique to your virtual machine and rqlite is started with authentication always enabled. An unauthenticated request is refused with 401.

rqlite refuses to start until that has happened. Two independent mechanisms enforce it, so a failure to generate a credential can never leave a database running without one.

Loopback only by default. The HTTP API and the Raft port are bound to 127.0.0.1 and the Marketplace plan opens port 22 only. A database reachable from the network the moment it boots is a materially larger attack surface than a documented opt in. Section 8 shows exactly how to open it to your own virtual network when you need to.

1. Deploy the virtual machine

Deploy from the Azure Marketplace listing, or from the gallery image with the Azure CLI. The recommended size is Standard_B2s or larger.

az vm create \
  --resource-group my-resource-group \
  --name my-rqlite-vm \
  --image "rqlite on Ubuntu 24.04 LTS by cloudimg" \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

The image declares a dedicated 32 GiB data disk. It is provisioned automatically and mounted at /var/lib/rqlite, so your database, your Raft log and your snapshots live on their own volume rather than on the operating system disk.

2. Connect to the virtual machine

ssh azureuser@<vm-ip>

3. Confirm rqlite is running

systemctl status rqlited.service --no-pager

The service is a native systemd unit and starts automatically on boot. The drop in shown in the output is what enforces the first boot credential generation described above.

rqlite service status on a freshly booted virtual machine, showing the native systemd unit active and the cloudimg first boot drop in

Confirm the version, the storage engine and the exact upstream commit this binary was built from:

rqlited -version
rqlited v10.2.7 linux amd64 go1.26.5 sqlite3.53.2 (commit a66a6f36d3b5a7f0da0bad3768014052a74ce4bc, compiler gc)

4. Retrieve your credential

First boot generates a random credential and writes it to a file readable only by root.

sudo cat /root/rqlite-credentials.txt

Expected output, with the password value unique to your virtual machine:

# rqlite per-VM credential -- generated on first boot, unique to this VM.
# The HTTP API is bound to loopback (127.0.0.1:4001) and requires this credential.
RQLITE_USERNAME=cloudimg
RQLITE_PASSWORD=<RQLITE_PASSWORD>

The same credential is held in /etc/rqlite/auth.json, which is readable only by root and the rqlite service account.

5. Run your first query

rqlite exposes an HTTP API that takes SQL and returns JSON. Every request must authenticate.

curl -sS -u 'cloudimg:<RQLITE_PASSWORD>' -G 'http://localhost:4001/db/query' \
  --data-urlencode 'q=SELECT sqlite_version()' | jq .

Confirm that an unauthenticated request is genuinely refused:

curl -sS -o /dev/null -w 'anonymous request returns HTTP %{http_code}\n' \
  -G 'http://localhost:4001/db/query' --data-urlencode 'q=SELECT 1'
anonymous request returns HTTP 401

The interactive client is also installed. It reads from standard input, so when you use it inside a script redirect from /dev/null or pass -c:

rqlite -u 'cloudimg:<RQLITE_PASSWORD>' -H localhost -p 4001 </dev/null <<'SQL'
.tables
SQL

An authenticated rqlite query returning real rows over the HTTP API, and an unauthenticated request being refused with HTTP 401

6. Create a schema and write real data

Writes go to /db/execute as a JSON array of statements. Parameterised statements are supported and are the right way to pass values.

curl -sS -u 'cloudimg:<RQLITE_PASSWORD>' -H 'Content-Type: application/json' \
  -d '[
        ["CREATE TABLE IF NOT EXISTS station (id INTEGER PRIMARY KEY, name TEXT NOT NULL, region TEXT NOT NULL, opened INTEGER)"],
        ["CREATE INDEX IF NOT EXISTS idx_station_region ON station(region)"],
        ["INSERT INTO station(name, region, opened) VALUES(?, ?, ?)", "Kings Cross", "London", 1852],
        ["INSERT INTO station(name, region, opened) VALUES(?, ?, ?)", "Waverley", "Edinburgh", 1846],
        ["INSERT INTO station(name, region, opened) VALUES(?, ?, ?)", "Piccadilly", "Manchester", 1842],
        ["INSERT INTO station(name, region, opened) VALUES(?, ?, ?)", "Temple Meads", "Bristol", 1840]
      ]' \
  'http://localhost:4001/db/execute?pretty' | jq '.results[] | {rows_affected: .rows_affected, last_insert_id: .last_insert_id}'

Read the rows back with a real query:

curl -sS -u 'cloudimg:<RQLITE_PASSWORD>' -G 'http://localhost:4001/db/query?pretty' \
  --data-urlencode 'q=SELECT region, name, opened FROM station ORDER BY opened' | jq .

An aggregate over the same table:

curl -sS -u 'cloudimg:<RQLITE_PASSWORD>' -G 'http://localhost:4001/db/query?pretty' \
  --data-urlencode 'q=SELECT COUNT(*) AS stations, MIN(opened) AS earliest, MAX(opened) AS latest FROM station' | jq .

rqlite supports real transactions. Pass transaction and the whole batch either commits or rolls back together:

curl -sS -u 'cloudimg:<RQLITE_PASSWORD>' -H 'Content-Type: application/json' \
  -d '[
        ["UPDATE station SET region = ? WHERE name = ?", "Greater London", "Kings Cross"],
        ["INSERT INTO station(name, region, opened) VALUES(?, ?, ?)", "Haymarket", "Edinburgh", 1842]
      ]' \
  'http://localhost:4001/db/execute?transaction&pretty' | jq '.results[] | {rows_affected: .rows_affected}'

A real schema with a secondary index, parameterised inserts and a query returning real rows from the rqlite HTTP API

7. Inspect Raft consensus and node status

Even as a single node, rqlite runs a real Raft state machine: the node elects itself leader and every write is committed through the Raft log before it is applied to SQLite. A node that never achieves leadership can accept connections but cannot serve a write, so this is the check that matters.

curl -sS -u 'cloudimg:<RQLITE_PASSWORD>' 'http://localhost:4001/status' \
  | jq '{raft_state: .store.raft.state, raft_term: .store.raft.term, leader: .store.leader.addr, http_bind: .http.bind_addr, auth: .http.auth}'
{
  "raft_state": "Leader",
  "raft_term": 4,
  "leader": "localhost:4002",
  "http_bind": "127.0.0.1:4001",
  "auth": "enabled"
}

Note "auth": "enabled": rqlite itself confirms authentication is active.

List the nodes in the cluster and their reachability:

curl -sS -u 'cloudimg:<RQLITE_PASSWORD>' 'http://localhost:4001/nodes?pretty' | jq .

A readiness probe suitable for a load balancer or a health check:

curl -sS -o /dev/null -w 'readyz returns HTTP %{http_code}\n' \
  -u 'cloudimg:<RQLITE_PASSWORD>' 'http://localhost:4001/readyz'

Real Raft consensus state from a single rqlite node, showing leadership, the current term, the node listing and a passing readiness probe

8. Network posture, and opening rqlite to your virtual network

By default both ports are bound to loopback only:

ss -tln | grep -E '400[12]'
LISTEN 0      4096       127.0.0.1:4001      0.0.0.0:*
LISTEN 0      4096       127.0.0.1:4002      0.0.0.0:*

Port 4001 is the HTTP API and port 4002 is Raft. The Marketplace plan opens port 22 only. Neither database port is reachable from outside the virtual machine as shipped.

Reaching rqlite from your workstation without opening anything

The simplest safe path is an SSH tunnel. Nothing is exposed to the network:

ssh -L 4001:localhost:4001 azureuser@<vm-ip>

Then point any client at http://localhost:4001 on your own machine.

Opening the API to your own virtual network

When an application elsewhere in your virtual network needs to reach rqlite, change the bind address and open the port to that network only. Never open a database port to 0.0.0.0/0.

Edit the service unit:

sudo systemctl edit rqlited.service

Add an override that binds the API to all interfaces and advertises the address other hosts should use. Substitute your virtual machine's private address:

[Service]
ExecStart=
ExecStart=/usr/local/bin/rqlited \
  -http-addr 0.0.0.0:4001 \
  -http-adv-addr <private-ip>:4001 \
  -raft-addr localhost:4002 \
  -auth /etc/rqlite/auth.json \
  /var/lib/rqlite/data

Then reload and restart, and open port 4001 in the network security group to your virtual network's address range only:

sudo systemctl daemon-reload && sudo systemctl restart rqlited
az network nsg rule create --resource-group my-resource-group --nsg-name my-nsg \
  --name allow-rqlite-vnet --priority 1001 --access Allow --protocol Tcp \
  --destination-port-ranges 4001 --source-address-prefixes 10.0.0.0/16

Because authentication is already enabled, the API is authenticated from the moment you expose it. Consider also enabling HTTPS with -http-cert and -http-key before carrying traffic across a network.

Forming a real Raft cluster

This image ships a single node, which is a complete working database. Raft replication needs at least three nodes to tolerate the loss of one. To grow a cluster, deploy further virtual machines from this same image, then on each additional node set a unique node identity, advertise a routable Raft address and join the first node:

-node-id <unique-node-id> \
-http-addr 0.0.0.0:4001 -http-adv-addr <this-node-private-ip>:4001 \
-raft-addr 0.0.0.0:4002 -raft-adv-addr <this-node-private-ip>:4002 \
-join http://<first-node-private-ip>:4001

Every node must carry the same auth.json, and the joining node needs a credential with the join permission. Open ports 4001 and 4002 between the cluster members only, inside your virtual network, never to the internet.

9. Back up your database

rqlite serves a consistent backup of the underlying SQLite database over the API:

sudo curl -sS -u 'cloudimg:<RQLITE_PASSWORD>' 'http://localhost:4001/db/backup' -o /var/lib/rqlite/backup.sqlite
ls -lh /var/lib/rqlite/backup.sqlite

The result is an ordinary SQLite file. Any SQLite tool can read it, and you can restore it into a fresh node with the /db/load endpoint. Copy it to Azure Blob Storage or another durable location on a schedule that suits you.

10. Keeping rqlite patched

The operating system and every apt managed component on this image patch normally with apt-get update && apt-get upgrade, and unattended upgrades run exactly as they do on stock Ubuntu.

rqlite itself is not an apt package: the project does not publish an apt repository, so apt-get upgrade will not move it. This image documents the update path on the machine itself:

cat /usr/share/doc/cloudimg/RQLITE-UPDATE-PATH.txt

In short, you have three supported routes:

  1. Drop in the upstream binary. rqlite publishes a tarball for every release. Verify its checksum, then install rqlited and rqlite over /usr/local/bin and restart the service. Do not install the upstream .deb over this image: it would place a second rqlited at /usr/bin along with a conflicting systemd unit.
  2. Take the republished cloudimg image, which is rebuilt from source on a freshly patched base.
  3. Rebuild from source. rqlite is MIT licensed, so you have the right to do this, and the document above carries the exact commands.

The binary on this image was compiled from the upstream MIT source at commit a66a6f36d3b5a7f0da0bad3768014052a74ce4bc with Go 1.26.5, which carries fixes that upstream's own prebuilt 10.2.7 binary predates.

11. Licences and source availability

rqlite is MIT licensed. Its storage engine, SQLite, is in the public domain. Several components linked into rqlited, including the HashiCorp Raft implementation, are licensed under the Mozilla Public License 2.0.

The full licence text of every component linked into the binary, along with a notice naming each component, its version and where to obtain its source, is on the image:

cat /usr/share/doc/cloudimg/rqlite-third-party-licences/NOTICE.txt

Under section 3.2 of the Mozilla Public License 2.0, you may obtain the source of any MPL 2.0 component from its module path through any Go module proxy, or by contacting support@cloudimg.co.uk.

Support

This image is supported by cloudimg. If you hit a problem with the image, contact support@cloudimg.co.uk. For questions about rqlite itself, the upstream project documentation is at rqlite.io.