SurrealDB is a multi-model database: the same engine stores schemaless documents, strictly typed tables and graph relations, and you query all of them with SurrealQL, a SQL-like language. It ships as a single binary that serves an HTTP and WebSocket API on port 8000. In this tutorial you will install SurrealDB on Ubuntu 24.04, run it as a hardened systemd service with persistent RocksDB storage, create a schema, run document and graph queries, add a database user and, optionally, publish the API over HTTPS with Nginx.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS with at least 1 GB of RAM, for example a CubePath VPS.
  • A non-root user with sudo privileges and UFW enabled with OpenSSH allowed.
  • For the optional HTTPS step: a domain name such as db.your_domain with an A record pointing to your_server_ip.

Step 1 - Installing the SurrealDB binary

SurrealDB's supported installation method on Linux is its install script, which detects your CPU architecture and downloads the latest stable release. Download the script first and read it before running it:

curl -sSf https://install.surrealdb.com -o surreal-install.sh
less surreal-install.sh

Run it:

sh surreal-install.sh

The script prints where it placed the binary. When run as a regular user it installs to ~/.surrealdb/surreal. Copy it to a system-wide path so the service user can run it:

sudo install -m 0755 ~/.surrealdb/surreal /usr/local/bin/surreal

If the script already reported /usr/local/bin/surreal, skip that command. Confirm the binary works:

surreal version
2.x.x for linux on x86_64

Step 2 - Creating a service user and a data directory

SurrealDB should not run as root. Create a system user with no login shell and a data directory it owns:

sudo useradd --system --home-dir /var/lib/surrealdb --shell /usr/sbin/nologin surrealdb
sudo install -d -o surrealdb -g surrealdb -m 0750 /var/lib/surrealdb

Store the root credentials in an environment file readable only by root, instead of putting them on the command line where any local user could see them with ps. SURREAL_USER and SURREAL_PASS are the environment variables that surreal start reads for its initial root user:

sudo install -d -m 0755 /etc/surrealdb
sudo nano /etc/surrealdb/surrealdb.env
SURREAL_USER=root
SURREAL_PASS=your_strong_password

Replace your_strong_password with a long random password, then lock the file down:

sudo chmod 0600 /etc/surrealdb/surrealdb.env

Step 3 - Running SurrealDB as a systemd service

Create the unit file:

sudo nano /etc/systemd/system/surrealdb.service
[Unit]
Description=SurrealDB
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=surrealdb
Group=surrealdb
EnvironmentFile=/etc/surrealdb/surrealdb.env
ExecStart=/usr/local/bin/surreal start --log info --bind 127.0.0.1:8000 rocksdb:///var/lib/surrealdb/data
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/surrealdb

[Install]
WantedBy=multi-user.target

The important choices:

  • rocksdb:///var/lib/surrealdb/data stores data on disk with the RocksDB engine. The memory backend you may see in examples loses everything on restart.
  • --bind 127.0.0.1:8000 keeps the API off the public internet. Step 7 publishes it through Nginx with TLS when you need remote access.
  • ProtectSystem=strict makes the whole file system read-only for the process except the data directory.

Load and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now surrealdb
sudo systemctl status surrealdb --no-pager
● surrealdb.service - SurrealDB
     Loaded: loaded (/etc/systemd/system/surrealdb.service; enabled; preset: enabled)
     Active: active (running) since Fri 2026-09-25 10:02:11 UTC; 4s ago

Check the health and version endpoints:

curl -i http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/version
HTTP/1.1 200 OK
...
surrealdb-2.x.x

Step 4 - Defining a namespace, a database and a schema

SurrealDB organizes data as namespaces that contain databases, which contain tables. Open an interactive SurrealQL shell as root against the local server, using the password from Step 2:

surreal sql --endpoint http://127.0.0.1:8000 --username root --password your_strong_password --pretty

Create a namespace and a database, and switch to them:

DEFINE NAMESPACE shop;
USE NS shop;
DEFINE DATABASE main;
USE DB main;

Define a strict table for users and a flexible one for events:

DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD name ON TABLE user TYPE string;
DEFINE FIELD email ON TABLE user TYPE string;
DEFINE FIELD created_at ON TABLE user TYPE datetime DEFAULT time::now();
DEFINE INDEX user_email ON TABLE user FIELDS email UNIQUE;

DEFINE TABLE event SCHEMALESS;

SCHEMAFULL rejects fields that are not defined and enforces the declared types; SCHEMALESS accepts any document. The unique index prevents two users with the same email.

List what you created:

INFO FOR DB;

The output lists the event and user tables with their definitions.

Step 5 - Running document queries

Every record has an ID in the form table:id. You can let SurrealDB generate it or set it yourself:

CREATE user:alice SET name = 'Alice Smith', email = '[email protected]';
CREATE user CONTENT { name: 'Bob Jones', email: '[email protected]' };

Try to insert a duplicate email to see the index at work:

CREATE user SET name = 'Fake Alice', email = '[email protected]';
Database index `user_email` already contains '[email protected]', with record `user:alice`

Query, update and aggregate:

SELECT name, email FROM user ORDER BY name;
UPDATE user:alice SET name = 'Alice Doe';
SELECT count() AS total FROM user GROUP ALL;
[{ total: 2 }]

The schemaless event table stores nested documents as they come:

CREATE event CONTENT { type: 'login', user: user:alice, meta: { ip: '203.0.113.10', agent: 'curl' } };
SELECT type, meta.ip FROM event WHERE user = user:alice;

Step 6 - Modeling relations as a graph

Instead of join tables, SurrealDB connects records with RELATE, which creates an edge record in its own table. The edge can carry data, such as the quantity purchased:

CREATE product:laptop SET name = 'Laptop Pro', price = 1299.99;
CREATE product:mouse SET name = 'Wireless Mouse', price = 49.99;

RELATE user:alice->purchased->product:laptop SET quantity = 1, at = time::now();
RELATE user:alice->purchased->product:mouse SET quantity = 2, at = time::now();
RELATE user:bob->purchased->product:mouse SET quantity = 1, at = time::now();

Traverse the graph with arrows. Products Alice bought:

SELECT ->purchased->product.name AS products FROM user:alice;
[{ products: ['Laptop Pro', 'Wireless Mouse'] }]

Reverse direction, everyone who bought the mouse:

SELECT <-purchased<-user.name AS buyers FROM product:mouse;
[{ buyers: ['Alice Doe', 'Bob Jones'] }]

And a two-hop query, "customers who bought something Bob also bought":

SELECT ->purchased->product<-purchased<-user.name AS related FROM user:bob;

Type exit to leave the shell.

Step 7 - Creating a database user and using the HTTP API

Applications should not use the root account. Open the shell again with the same command as in Step 4, run USE NS shop DB main; and create a user limited to the main database of the shop namespace:

DEFINE USER app ON DATABASE PASSWORD 'your_app_password' ROLES EDITOR;

EDITOR can read and write data in that database but cannot manage users. Use VIEWER for read-only access.

Every SurrealQL statement can also be sent over HTTP to the /sql endpoint. The target namespace and database go in the surreal-ns and surreal-db headers:

curl -s -X POST http://127.0.0.1:8000/sql \
  -u "app:your_app_password" \
  -H "surreal-ns: shop" \
  -H "surreal-db: main" \
  -H "Accept: application/json" \
  -d "SELECT name, email FROM user;"
[{"result":[{"email":"[email protected]","name":"Alice Doe"},{"email":"[email protected]","name":"Bob Jones"}],"status":"OK","time":"312.4µs"}]

Official SDKs for JavaScript, Python, Go, Rust and other languages connect over the WebSocket endpoint /rpc on the same port.

Step 8 - Publishing the API over HTTPS (optional)

If applications on other servers need to reach SurrealDB, put Nginx in front of it with a Let's Encrypt certificate rather than binding SurrealDB to a public IP. Install Nginx and Certbot and open HTTP and HTTPS:

sudo apt install -y nginx certbot python3-certbot-nginx
sudo ufw allow 'Nginx Full'

Create a server block. The Upgrade headers are needed for the WebSocket endpoint used by the SDKs:

sudo nano /etc/nginx/sites-available/surrealdb
server {
    listen 80;
    server_name db.your_domain;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 3600s;
    }
}

Enable it, test the configuration and request a certificate. Certbot adds the HTTPS server block and the redirect:

sudo ln -s /etc/nginx/sites-available/surrealdb /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d db.your_domain

Verify from your own computer:

curl -i https://db.your_domain/health
HTTP/2 200

Troubleshooting

The service keeps restarting. Read the error with sudo journalctl -u surrealdb -n 50 --no-pager. A permissions error on the data path means /var/lib/surrealdb is not owned by surrealdb; fix it with sudo chown -R surrealdb:surrealdb /var/lib/surrealdb.

Changing SURREAL_PASS has no effect. The initial root user is only created when the database has no root users yet. To change the password of an existing root user, connect as root and run DEFINE USER OVERWRITE root ON ROOT PASSWORD 'new_password' ROLES OWNER;, then update the environment file to match.

Queries return nothing after a restart. Check ExecStart in the unit: if it uses memory instead of rocksdb://..., data only lives in RAM.

Conclusion

SurrealDB is now running on Ubuntu 24.04 as an unprivileged systemd service with RocksDB storage, a typed schema, graph relations and a non-root application user, optionally exposed over HTTPS. Next, back up the database regularly with surreal export to a file stored off the server, explore record-level PERMISSIONS on tables for multi-tenant apps, and connect your application with the official SDK for its language.