Databases AWS

rqlite Distributed SQLite Database on AWS User Guide

| Product: rqlite Distributed SQLite Database | Support by cloudimg

This image ships rqlite 10.2.7, configured and ready to answer queries from the first boot of every instance you launch.

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 instance. 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 instance 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 instance 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 instance security group 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 VPC when you need to.

1. Launch the instance

Subscribe to the listing in AWS Marketplace and launch it, or launch the AMI directly with the AWS CLI. The recommended instance type is m5.large or larger.

aws ec2 run-instances \
  --image-id <ami-id> \
  --instance-type m5.large \
  --key-name my-key \
  --security-group-ids <sg-id> \
  --metadata-options 'HttpTokens=required'

The image declares a dedicated 32 GiB EBS data volume. 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, and you can snapshot or resize it independently.

2. Connect to your instance

Connect over SSH as the default login user for your operating system variant.

OS variant SSH login user Connect
Ubuntu 24.04 LTS ubuntu ssh ubuntu@<instance-ip>
ssh ubuntu@<instance-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 instance, 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 instance:

# rqlite per-VM credential -- generated on first boot, unique to this instance.
# 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 VPC

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 instance security group opens port 22 only. Neither database port is reachable from outside the instance 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 ubuntu@<instance-ip>

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

Opening the API to your own VPC

When an application elsewhere in your VPC 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 instance'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 add an inbound rule to the instance security group allowing port 4001 from your VPC CIDR only:

sudo systemctl daemon-reload && sudo systemctl restart rqlited
aws ec2 authorize-security-group-ingress --group-id <sg-id> \
  --protocol tcp --port 4001 --cidr 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, launch further instances 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 VPC, 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 Amazon S3 or another durable location on a schedule that suits you, or take an EBS snapshot of the data volume.

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.