Deployment guide

TerminusDB 12.0.8

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 query language. A complete HTTP API covers documents, schemas, branches, diffs and history, and client libraries are published for Python and JavaScript. TerminusDB is licensed under the Apache License, Version 2.0.

This guide covers launching and using the TerminusDB 12.0.8 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud Hardened Neo4j & Graph and RDF Databases AMIs listing on AWS Marketplace. You pick it as the software version when you subscribe and launch. Every product in the listing is described at /guides/graph-databases-aws/.

What SecureAnyCloud adds to the upstream build:

  • No default login, ever. TerminusDB's published default is the user admin with the password root. This image never creates it: no database is initialised while the image is built and the whole store is removed before capture, so the image contains no accounts at all. On first boot the store is created and a unique 32-character admin password is set for your instance alone, through standard input only (never a command line or a container environment variable).
  • Nothing answers without a login. Left alone, TerminusDB serves callers who send no credential as a built-in anonymous identity and reports its version and build hash to them. This image refuses any request without credentials at the HTTPS front door. Only the information-free health check /api/ok stays open, for load balancers and uptime monitors.
  • HTTPS from the first boot. TerminusDB has no TLS listener of its own and uses HTTP Basic authentication, so nginx serves the API on port 443 with a certificate generated on the instance for its own address. Port 80 only redirects to HTTPS, and requests for any other hostname are dropped.
  • The API is loopback only. TerminusDB listens on 127.0.0.1:6363. Only SSH, 80 and 443 are reachable from the network.
  • A locked-down container. TerminusDB runs from the official terminusdb/terminusdb-server:v12.0.8 image, pinned by tag and manifest digest, as an unprivileged account, with a read-only root filesystem, every Linux capability dropped and no-new-privileges.
  • Instance metadata is blocked. TerminusDB can fetch remote URLs (a WOQL get of a CSV by URL, cloning a remote database). The container can never reach the instance metadata service. A firewall rule drops its traffic to 169.254.169.254, and the nginx, Docker and TerminusDB services are denied it too.
  • Fully patched Ubuntu 24.04 at build time, the SecureAnyCloud security defaults and on-instance self-tests.

The image contains Ubuntu 24.04 LTS, Docker CE, the pinned TerminusDB 12.0.8 server image, nginx and the SecureAnyCloud first-boot service. The TerminusDB store lives on a dedicated 20 GiB data volume mounted at /var/lib/terminusdb.

A note on the dashboard

Upstream discontinued the TerminusDB Dashboard component. The server still serves a page at /dashboard/, but in 12.0.8 that page is only a notice to that effect, not a working console.

You drive this image from the HTTP API, the command line and the client libraries. Everything in this guide uses curl.

Connecting to your instance

Version SSH user API

TerminusDB 12.0.8 on Ubuntu 24.04 ubuntu https://<public-dns>/api/

Prerequisites

  • An AWS account and permission to launch EC2 instances
  • An EC2 key pair in the Region you launch in
  • A security group that allows TCP 22 from your own address, and TCP 443 (and optionally 80, which only redirects) from the clients that will use the API. TerminusDB's own port 6363 is never exposed, because it listens on loopback only.

Recommended instance type: m5.large (2 vCPU, 8 GiB). Choose a larger instance for big databases or heavy WOQL queries.

1

Launch the instance

Option A: AWS Marketplace console

  • Open the SecureAnyCloud Hardened Neo4j & Graph and RDF Databases AMIs listing on AWS Marketplace. Choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  • Choose Launch your software. In Version, choose TerminusDB 12.0.8 on Ubuntu 24.04 - YYYY-MM-DD, then select your Region.
  • Set the instance type. m5.large is recommended.
  • Choose your EC2 key pair.
  • Set the security group to allow TCP 22 from your own address and TCP 443 (and 80) from your clients.
  • Launch the instance. First boot takes under a minute after the instance is reachable. During that time it generates this instance's certificate and admin password and creates the store.

Option B: AWS CLI

Run this command on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page. Replace the key pair, security group and subnet with your own:

aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids sg-0123456789abcdef0 \
  --subnet-id subnet-0123456789abcdef0 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=terminusdb}]'

The AMI already requires IMDSv2, so you do not need --metadata-options. The data volume is created from the AMI automatically.

2

Connect over SSH

ssh -i my-key-pair.pem ubuntu@<vm-ip>

The login banner points to the credentials file, the self-test and this guide.

3

Check the services and health

Four units make up the appliance: docker runs the database container, sac-docker-imds-deny installs the metadata firewall rule for containers, terminusdb is the database, and nginx terminates TLS in front of it.

systemctl is-active docker sac-docker-imds-deny terminusdb nginx test -f /var/lib/sac/data/terminusdb-firstboot.done && echo first-boot-done curl -sk -o /dev/null -w 'health check /api/ok: HTTP %{http_code}\n' https://localhost/api/ok
sudo docker images terminusdb/terminusdb-server --digests --format '{{.Repository}}:{{.Tag}} {{.Digest}}'

All four units report active, the first-boot marker is present, /api/ok returns HTTP 200 without a credential, and the image is terminusdb/terminusdb-server:v12.0.8 with digest sha256:43061044d1857bb6f1a2f41d7a59ad06ba5f91c03cbc90128d7c0415f32a0916.

TerminusDB listens on loopback only; nginx owns the public ports:

sudo ss -Hltnp | grep -E ':(6363|80|443) ' | awk '{print $4, $6}' | sed -E 's/users:\(\("([a-z-]+)".*/\1/' | sort -u

You see docker-proxy on 127.0.0.1:6363 and nginx on ports 80 and 443. The appliance can check its own listening set for you:

sudo /usr/local/sbin/terminusdb-port-check.sh
4

Read the per-instance credentials

The admin password is unique to this instance and is written to a root-only file at first boot. Print only the field names, never the password itself:

sudo grep -oE '^[A-Z_]+=' /root/terminusdb-credentials.txt

This lists TERMINUSDB_ADMIN_USER (admin), TERMINUSDB_ADMIN_PASSWORD, TERMINUSDB_HOST, TERMINUSDB_PUBLIC_IP, TERMINUSDB_URL and TERMINUSDB_API. Read the password with sudo sed -n 's/^TERMINUSDB_ADMIN_PASSWORD=//p' /root/terminusdb-credentials.txt when you need it, and keep it safe.

The examples below use a small shell function, tdb, that hands the password to curl on standard input, so it never appears on a command line or in your shell history. Each block defines it again so you can paste any block on its own.

5

The authentication gate

A request with no credential, the upstream default admin/root, and a blank password are all refused:

curl -sk -o /dev/null -w 'no credential: HTTP %{http_code}\n' https://localhost/api/info
curl -sk -o /dev/null -w 'admin/root: HTTP %{http_code}\n' -u 'admin:root' https://localhost/api/info
curl -sk -o /dev/null -w 'blank password: HTTP %{http_code}\n' -u 'admin:' https://localhost/api/info

Each returns HTTP 401, with a WWW-Authenticate: Basic challenge so browsers and HTTP clients know to ask for a login. Your own password is accepted, and the response names the admin user and the version:

tdb() { printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^TERMINUSDB_ADMIN_PASSWORD=//p' /root/terminusdb-credentials.txt)" | curl -sk -K - "$@"; } tdb https://localhost/api/info; echo

The reply is api:success with "authority":"terminusdb://system/data/User/admin" and "terminusdb":{"version":"12.0.8"}.

The whole gate, including a check that each test is itself proven against a known-bad input first:

sudo /usr/local/sbin/terminusdb-verify-auth.sh
sudo /usr/local/sbin/terminusdb-gate-selftest.sh
6

Create a database, a schema and a document

Create a database called people:

tdb() { printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^TERMINUSDB_ADMIN_PASSWORD=//p' /root/terminusdb-credentials.txt)" | curl -sk -K - "$@"; } tdb -X POST https://localhost/api/db/admin/people \
  -H 'Content-Type: application/json' \
  -d '{"label":"People","comment":"example database"}'; echo

TerminusDB is schema-checked: documents must satisfy a class you define. Add an Employee class, then insert a document:

tdb() { printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^TERMINUSDB_ADMIN_PASSWORD=//p' /root/terminusdb-credentials.txt)" | curl -sk -K - "$@"; } tdb -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"}'; echo tdb -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"}'; echo

Read the document back:

tdb() { printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^TERMINUSDB_ADMIN_PASSWORD=//p' /root/terminusdb-credentials.txt)" | curl -sk -K - "$@"; } tdb 'https://localhost/api/document/admin/people?type=Employee'; echo

The schema is enforced. A document with a property the class does not declare is rejected rather than quietly stored:

tdb() { printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^TERMINUSDB_ADMIN_PASSWORD=//p' /root/terminusdb-credentials.txt)" | curl -sk -K - "$@"; } tdb -o /dev/null -w 'schema violation: HTTP %{http_code}\n' -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"}'

The insert is refused with HTTP 400.

7

Commit history and branches

Every write above was a commit, with the author and message you supplied:

tdb() { printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^TERMINUSDB_ADMIN_PASSWORD=//p' /root/terminusdb-credentials.txt)" | curl -sk -K - "$@"; } tdb https://localhost/api/log/admin/people | python3 -c 'import json,sys; [print(c["author"], "|", c["message"]) for c in json.load(sys.stdin)]'

Branching works like git: take a copy of main, change it independently, and compare.

tdb() { printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^TERMINUSDB_ADMIN_PASSWORD=//p' /root/terminusdb-credentials.txt)" | curl -sk -K - "$@"; } tdb -X POST https://localhost/api/branch/admin/people/local/branch/trial \
  -H 'Content-Type: application/json' \
  -d '{"origin":"admin/people/local/branch/main"}'; echo tdb 'https://localhost/api/db/admin/people?branches=true' | python3 -c 'import json,sys; print(json.load(sys.stdin)["branches"])'

The second command lists both main and trial.

8

Query with WOQL

WOQL is TerminusDB's query language. This finds every document of type Employee:

tdb() { printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^TERMINUSDB_ADMIN_PASSWORD=//p' /root/terminusdb-credentials.txt)" | curl -sk -K - "$@"; } tdb -X POST https://localhost/api/woql/admin/people \
  -H 'Content-Type: application/json' \
  -d '{"query":{"@type":"Triple","subject":{"@type":"NodeValue","variable":"S"},"predicate":{"@type":"NodeValue","node":"rdf:type"},"object":{"@type":"Value","node":"@schema:Employee"}}}' \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["bindings"])'
9

Call the API from your workstation

From your own machine, use the instance's address. The certificate generated at first boot names it (public IP, private IP and the EC2 DNS names); fetch it once and trust it, or pass -k while testing:

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())

Browsers and the JavaScript client must call the API from the same origin. nginx refuses state-changing requests (POST, PUT, DELETE) that carry an Origin header for another site. Scripts and server-side clients send no Origin and are unaffected.

10

Serve under your own DNS name

Point a DNS record at the instance, then regenerate the certificate and pin the served hostname:

sudo terminusdb-set-hostname terminusdb.example.com

This issues a new self-signed certificate for that name (replace /etc/ssl/terminusdb/terminusdb.crt and terminusdb.key with your own CA- or ACM-issued pair for production, then sudo systemctl reload nginx) and makes nginx answer only for it.

For a public, browser-trusted certificate, you can also terminate TLS at an Application Load Balancer in front of the instance and keep the security group's 443 open only to it.

11

Back up and restore

The store is a directory on its own EBS volume. The simplest backup is an EBS snapshot of the /var/lib/terminusdb volume. For a file-level copy, stop the database first:

sudo systemctl stop terminusdb
sudo tar -czf /var/tmp/terminusdb-backup.tar.gz -C /var/lib/terminusdb storage
sudo systemctl start terminusdb

Restore by putting the directory back with the database stopped, then correcting ownership:

sudo systemctl stop terminusdb
sudo tar -xzf /var/tmp/terminusdb-backup.tar.gz -C /var/lib/terminusdb
sudo chown -R terminusdb:terminusdb /var/lib/terminusdb/storage
sudo systemctl start terminusdb
12

Change the administrator password

The command reads the new password from standard input, so it never appears in your shell history or the process list. It refuses anything shorter than 16 characters:

read -rsp 'New admin password: ' NEWPW; echo if [ "${#NEWPW}" -lt 16 ]; then echo "Refusing: use at least 16 characters. Nothing was changed." else printf '%s\n' "$NEWPW" | sudo docker exec -i terminusdb /app/terminusdb/terminusdb user password admin printf '%s\n' "$NEWPW" | sudo sh -c 'read -r P; sed -i "s|^TERMINUSDB_ADMIN_PASSWORD=.*|TERMINUSDB_ADMIN_PASSWORD=$P|" /root/terminusdb-credentials.txt' echo "Updated, and /root/terminusdb-credentials.txt now matches." fi unset NEWPW

Confirm the change took and that everything else is still refused:

sudo /usr/local/sbin/terminusdb-verify-auth.sh

Security notes

TerminusDB 12.0.8 is the latest release at the time of writing, and no security advisory has been published for it. On top of that, this image does the following:

  • No default login. It never creates the upstream default admin/root login. The store and its admin password are created on your instance at first boot, through standard input only.
  • No anonymous access. It refuses unauthenticated requests at nginx, so the server's built-in anonymous identity, version and build hash are not reachable from the network. It also clears the X-User-Forward header that TerminusDB's optional "insecure user header" mode would trust (that mode is off).
  • Loopback only. It keeps TerminusDB on 127.0.0.1:6363 and runs it as an unprivileged account in a read-only container with every capability dropped.
  • Instance metadata blocked. It blocks the instance metadata service for the container and for the nginx, Docker and TerminusDB services, so a WOQL get of a URL or a clone from a remote URL can never reach the instance's credentials.
  • Same-origin only. It refuses cross-origin state-changing requests at nginx and logs the real client address.

The firewall rule and the per-service metadata block are visible on the instance:

sudo iptables -S DOCKER-USER | grep DROP systemctl show -p IPAddressDeny nginx.service docker.service terminusdb.service

From inside the container's own network namespace, the internet is reachable but the metadata service is not:

CPID=$(sudo docker inspect terminusdb --format '{{.State.Pid}}')
CIP=$(getent ahostsv4 checkip.amazonaws.com | awk 'NR==1{print $1}')
sudo nsenter -t "$CPID" -n curl -s -m 10 -o /dev/null -w 'internet from the container: HTTP %{http_code}\n' --resolve "checkip.amazonaws.com:443:$CIP" https://checkip.amazonaws.com/
sudo nsenter -t "$CPID" -n curl -s -m 5 -o /dev/null -w 'metadata from the container: HTTP %{http_code}\n' http://169.254.169.254/latest/meta-data/ || true

The first line reports HTTP 200 and the second HTTP 000. The connection is dropped, so curl gives up after its 5 second limit.

Run the full appliance self-test at any time:

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

Keep the instance patched (sudo apt-get update && sudo apt-get upgrade) and keep the admin password safe. Snapshot the /var/lib/terminusdb volume regularly, and expose only ports 22 and 443 to the networks that need them.

Troubleshooting

The API returns 401 with the password from the credentials file. Check that the file is the current one, because a re-provisioned instance has a new password:

sudo grep -c '^TERMINUSDB_ADMIN_PASSWORD=' /root/terminusdb-credentials.txt

The service will not start. Check the database, the proxy and the first-boot unit:

systemctl status terminusdb nginx --no-pager -l | head -30 sudo journalctl -u terminusdb-firstboot -u terminusdb -n 30 --no-pager

Support

SecureAnyCloud provides 24/7 technical support for every version in this listing by email and live chat, 365 days a year, with a 24-hour response SLA: sales@secureanycloud.com.

Trademarks

TerminusDB is a trademark of its respective owner. Docker is a trademark of Docker, Inc. SecureAnyCloud is not affiliated with or endorsed by the TerminusDB project, its maintainers or Docker, Inc.

Deploy on AWS

Launch TerminusDB 12.0.8 on Ubuntu 24.04 with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS