TerminusDB on Ubuntu 24.04 on Azure User Guide
Overview
TerminusDB is an open source document graph database that treats your data the way git treats code. You store JSON documents against a schema that describes how they relate, and the database keeps the full history: every change is a commit, you can branch a database, diff two branches, reset to any earlier state, and merge work back together.
Queries are written in WOQL, TerminusDB's own query language, and a complete HTTP API covers documents, schemas, branches, diffs and history. Client libraries are published for Python and JavaScript, and an Elixir client is maintained by the community.
The cloudimg image delivers TerminusDB 12.0.7 on Ubuntu 24.04, served over TLS, with a unique administrator password generated on the first boot of your VM. Backed by 24/7 cloudimg support.
What is included:
- TerminusDB 12.0.7, run by systemd from the official container image pinned by tag and by manifest digest, as the unprivileged
terminusdbservice account - The complete HTTP API behind TLS on port 443, with a certificate generated for your VM at first boot
- A durable store at
/var/lib/terminusdb/storage, owned by the service account, which survives reboots - A per-VM administrator password written to
/root/terminusdb-credentials.txt, readable only by root - On-VM self-tests that prove the security posture at any time
A note on the dashboard
Upstream discontinued the TerminusDB Dashboard component. The server still serves a page at /, but in 12.0.7 that page is a notice to that effect with a link to upstream's migration documentation — it is not a working console. This image is therefore documented and supported as what it actually is: a database you drive from the HTTP API, the command line and the client libraries. Everything in this guide is done with curl, so it works the same from any language.
Prerequisites
- An Azure subscription
- SSH access to the VM (port 22), plus HTTPS (port 443) from wherever you will call the API
curlandpython3on your workstation for the examples below
Step 1: Deploy from the Azure Marketplace
Search the Azure Marketplace for TerminusDB on Ubuntu 24.04 LTS by cloudimg, choose Standard_B2s or larger, and allow inbound 22 and 443.
Step 2: Deploy from the Azure CLI
az vm create \
--resource-group my-resource-group \
--name my-terminusdb \
--image cloudimg:terminusdb-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
Open the ports the appliance actually uses:
az vm open-port --resource-group my-resource-group --name my-terminusdb --port 22 --priority 1001
az vm open-port --resource-group my-resource-group --name my-terminusdb --port 443 --priority 1002
Step 3: Connect to your VM
ssh azureuser@<public-ip>
Step 4: Confirm TerminusDB is running
Three units make up the appliance: docker runs the database container, terminusdb is the database itself, and nginx terminates TLS in front of it.
systemctl is-active docker terminusdb nginx
All three report active.
Confirm the exact image the appliance is running, pinned by digest:
sudo docker images terminusdb/terminusdb-server --digests --format 'table {{.Repository}}\t{{.Tag}}\t{{.Digest}}'
The health endpoint answers without a credential, which is what makes it usable as a load-balancer or uptime probe:
curl -sk https://localhost/api/ok -w '\n'

Step 5: Check what the network can reach
sudo ss -Hltn | awk '{print $4}' | sort -u
Only 22, 80 and 443 are bound on an address reachable from outside the VM. Port 80 does nothing but redirect to HTTPS. TerminusDB itself listens on 127.0.0.1:6363 and is reachable only through the TLS proxy, so the administrator password is never sent over a plaintext connection.
The appliance can prove that for you:
sudo /usr/local/sbin/terminusdb-port-check.sh
Step 6: Retrieve the first-boot administrator password
TerminusDB's published default login is the user admin with the password root. This image never creates it. No database is initialised while the image is built, and the entire store is removed before the image is captured — so the image you launched contains no accounts at all. On the first boot of your VM a password is generated for that VM alone.
sudo cat /root/terminusdb-credentials.txt
The file is 0600 root:root and contains the user, the password, and the URLs for this VM.

Put the password in a shell variable for the rest of this guide:
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
Step 7: Confirm the API refuses everything but your password
Left alone, TerminusDB answers a caller who presents no credential at all as a built-in anonymous identity, and will happily report its version and build hash to them. This image refuses unauthenticated requests at the edge instead. A request with no credential is rejected:
curl -sk -o /dev/null -w 'no credential: %{http_code}\n' https://localhost/api/info
So is the upstream default, and so is anything weak:
curl -sk -o /dev/null -w 'admin/root: %{http_code}\n' -u 'admin:root' https://localhost/api/info
curl -sk -o /dev/null -w 'blank: %{http_code}\n' -u 'admin:' https://localhost/api/info
Each returns 401. Your own password is accepted, and the response names the admin authority:
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" https://localhost/api/info | python3 -m json.tool
The whole posture, including a check that each of these tests is itself proven against a known-bad input:
sudo /usr/local/sbin/terminusdb-verify-auth.sh
sudo /usr/local/sbin/terminusdb-gate-selftest.sh

Step 8: Create a database
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" -X POST https://localhost/api/db/admin/people \
-H 'Content-Type: application/json' \
-d '{"label":"People","comment":"example database"}'
List what exists:
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" https://localhost/api/db | python3 -m json.tool
Step 9: Add a schema
TerminusDB is schema-checked: documents must satisfy a class you define.
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" -X POST \
'https://localhost/api/document/admin/people?graph_type=schema&author=guide&message=add+Employee+schema' \
-H 'Content-Type: application/json' \
-d '{"@type":"Class","@id":"Employee","name":"xsd:string","title":"xsd:string"}'
Step 10: Insert and read documents
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" -X POST \
'https://localhost/api/document/admin/people?author=guide&message=insert+Ada+Lovelace' \
-H 'Content-Type: application/json' \
-d '{"@type":"Employee","name":"Ada Lovelace","title":"Analyst"}'
Read it back:
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" 'https://localhost/api/document/admin/people?type=Employee'
The schema is enforced. A document with a property the class does not declare is rejected rather than quietly stored:
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -w 'schema violation: %{http_code}\n' -u "admin:$PW" -X POST \
'https://localhost/api/document/admin/people?author=guide&message=bad' \
-H 'Content-Type: application/json' \
-d '{"@type":"Employee","name":"Bad Record","nonexistent_property":"x"}'
Step 11: The commit history — what makes TerminusDB different
Every write above was a commit, with the author and message you supplied:
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" 'https://localhost/api/log/admin/people' | python3 -m json.tool | head -30

Step 12: Branch a database
Branching is cheap and works like git — take a copy of main, change it independently, and compare.
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" -X POST https://localhost/api/branch/admin/people/local/branch/trial \
-H 'Content-Type: application/json' \
-d '{"origin":"admin/people/local/branch/main"}'
Confirm it exists:
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" 'https://localhost/api/db/admin/people?branches=true' | python3 -m json.tool
Step 13: Query with WOQL
WOQL is TerminusDB's query language. This finds every document of type Employee:
PW=$(sudo grep '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt | cut -d= -f2-)
curl -sk -u "admin:$PW" -X POST https://localhost/api/woql/admin/people \
-H 'Content-Type: application/json' \
-d '{"query":{"@type":"Triple","subject":{"variable":"S"},"predicate":{"node":"rdf:type"},"object":{"node":"@schema:Employee"}}}' \
| python3 -m json.tool | head -20
Step 14: Call the API from your workstation
Everything above used localhost from inside the VM. From elsewhere, use the VM's address. The certificate generated at first boot names that address, so once you trust it you can verify normally:
curl -k -u "admin:<new-password>" https://<public-ip>/api/info
The Python client speaks the same API:
from terminusdb_client import Client
client = Client("https://<public-ip>")
client.connect(user="admin", key="<admin-password>", team="admin", db="people")
print(client.get_all_documents())
Step 15: Use your own TLS certificate
The image generates a self-signed certificate for your VM's address. To use a certificate for a real domain, replace the pair and reload nginx:
sudo install -m 0644 -o root -g root /path/to/fullchain.pem /etc/terminusdb/tls/server.crt
sudo install -m 0640 -o root -g root /path/to/privkey.pem /etc/terminusdb/tls/server.key
sudo systemctl reload nginx
Step 16: Back up and restore
The store is a directory, so a backup is a copy of it taken while the database is stopped:
sudo systemctl stop terminusdb
sudo tar -czf /root/terminusdb-backup.tar.gz -C /var/lib/terminusdb storage
sudo systemctl start terminusdb
Restore by putting the directory back, then correcting ownership:
sudo systemctl stop terminusdb
sudo tar -xzf /root/terminusdb-backup.tar.gz -C /var/lib/terminusdb
sudo chown -R terminusdb:terminusdb /var/lib/terminusdb/storage
sudo systemctl start terminusdb
Step 17: Keep the VM patched
Ubuntu security updates are applied automatically by unattended-upgrades. To apply them immediately:
sudo apt-get update && sudo apt-get -y upgrade
Step 18: Change the administrator password
The command below reads the new password from standard input, so it never appears in your shell history or the process list.
Do not pipe an empty value into it. TerminusDB will accept an empty password and your instance would then authenticate anyone, so the block refuses anything shorter than 12 characters rather than passing it on:
NEWPW='<new-password>'
if [ "${#NEWPW}" -lt 12 ]; then
echo "Refusing: the password must be at least 12 characters. Nothing was changed."
else
printf '%s\n' "$NEWPW" | sudo docker exec -i terminusdb /app/terminusdb/terminusdb user password admin
sudo sed -i "s|^TERMINUSDB_ADMIN_PASSWORD=.*|TERMINUSDB_ADMIN_PASSWORD=${NEWPW}|" /root/terminusdb-credentials.txt
echo "Updated, and /root/terminusdb-credentials.txt now matches."
fi
unset NEWPW
Confirm the change took and that the appliance is still refusing everything else:
sudo /usr/local/sbin/terminusdb-verify-auth.sh
Troubleshooting
The API returns 401 with the password from the credentials file. Confirm you are reading the current file — if the VM was re-provisioned, the password was rotated:
sudo grep '^TERMINUSDB_ADMIN_USER=' /root/terminusdb-credentials.txt
The service will not start. Check the database and the proxy separately:
sudo systemctl status terminusdb --no-pager -l | head -20
sudo journalctl -u terminusdb -n 15 --no-pager -p err --since '-10min'
Confirm the whole appliance is healthy:
sudo /usr/local/sbin/terminusdb-selftest.sh
Support
This image is published by cloudimg with 24/7 support for deployment, upgrades, TLS certificates and custom domains, client library integration, schema design and scaling. TerminusDB is a trademark of its respective owner. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.