Skip to content
v0.5.1GitHub

Operating

systemd and Docker

applies to v0.5.1

The Debian package installs LuraDB as the systemd service luradb. It runs as the luradb user and logs to journald. The same package runs in Docker with the compose file in the repository.

After apt install, these files are on the host:

Path What it is Owner and mode
/usr/bin/luradb The server binary. root, 755
/usr/lib/systemd/system/luradb.service The unit. root, 644
/etc/luradb/luradb.toml The configuration, a dpkg conffile that also holds the admin key. root:luradb, 640
/etc/luradb/tls/ Empty; the place for the certificate and key of the HTTPS listener. root:luradb, 750
/usr/share/doc/luradb/ README.md, INSTALL.md and the copyright file. root, 644
/var/lib/luradb/ The data, created by systemd when the service starts. luradb, 750
/run/luradb/ The runtime directory with the Unix socket, created by systemd. luradb, 750

The package creates the system user and group luradb without a login and without a home directory, enables the unit and does not start it. Install walks through the first start; this page covers running the service afterwards. The unit is this file:

[Unit]
Description=LuraDB multi-model database
After=network.target
StartLimitIntervalSec=300
StartLimitBurst=3
[Service]
Type=simple
User=luradb
Group=luradb
ExecStart=/usr/bin/luradb --config /etc/luradb/luradb.toml
WorkingDirectory=/var/lib/luradb
StateDirectory=luradb
StateDirectoryMode=0750
RuntimeDirectory=luradb
RuntimeDirectoryMode=0750
UMask=0027
LimitNOFILE=65536
LimitMEMLOCK=infinity
Restart=on-failure
RestartSec=2
RestartPreventExitStatus=4
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
[Install]
WantedBy=multi-user.target
Setting Effect
User=luradb, Group=luradb The server runs as the user the package created.
WorkingDirectory=/var/lib/luradb A relative path in luradb.toml resolves against this directory.
StateDirectory=luradb, StateDirectoryMode=0750 systemd creates /var/lib/luradb for the service.
RuntimeDirectory=luradb, RuntimeDirectoryMode=0750 systemd creates /run/luradb, where the Unix socket lives. Group members need the execute bit on the directory to reach the socket.
UMask=0027 Files the server creates are readable for the owner and the group, and not for others.
LimitNOFILE=65536 The most open files and connections.
LimitMEMLOCK=infinity The io_uring buffers the server registers need locked memory.
Restart=on-failure, RestartSec=2 systemd starts the service again two seconds after it ended with an error.
RestartPreventExitStatus=4 After exit code 4 it does not restart, see below.
StartLimitIntervalSec=300, StartLimitBurst=3 At most three starts in five minutes.
NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, ProtectKernelTunables, ProtectControlGroups, RestrictSUIDSGID Hardening. The unit sets no SystemCallFilter on purpose, because the io_uring calls must stay allowed; /dev/shm stays writable for the shared-memory path.

The service is enabled, so it also starts on the next boot. The commands are those of the package’s INSTALL.md:

Terminal window
sudo systemctl start luradb
systemctl status luradb
journalctl -u luradb
journalctl -u luradb -f

The server reads luradb.toml once, at start. After a change:

Terminal window
sudoedit /etc/luradb/luradb.toml
sudo systemctl restart luradb

systemctl stop sends SIGTERM, as does Ctrl+C for a server started by hand. The server stops accepting connections, finishes the requests in flight, drains the data shards and exits with code 0:

INFO luradb: Shutdown signal received. Starting graceful shutdown...
INFO luradb::server: [http] listener stopped
INFO luradb::server::control: Draining the data shards...
INFO luradb::server::control: Shutting down system store...
INFO luradb::server::control: Shutdown complete.

A write that was confirmed is in the write-ahead log, so even a stop with SIGKILL loses no confirmed write: in a test, a key written before kill -9 was there after the next start. An update restarts the service only if it is running, see Upgrading. Removing the package keeps the data and the user:

Terminal window
sudo apt remove luradb # binary and unit gone, the configuration stays
sudo apt purge luradb # also removes /etc/luradb/luradb.toml

/var/lib/luradb and the user luradb are never deleted by the package. Removing them is up to you (sudo rm -rf /var/lib/luradb, sudo deluser luradb).

Every data path in the packaged luradb.toml lies below /var/lib/luradb, the one directory the service may write to besides /run/luradb. Relative paths resolve there too, because of WorkingDirectory=:

Setting Packaged value Where it lands
storage.wal_path /var/lib/luradb/luradb.wal A directory of 64 MiB segment files.
storage.vlog_path /var/lib/luradb/luradb.vlog The value log; its files live in shard_<k>/ beside this path.
storage.sstable_dir /var/lib/luradb/sstables SSTables, in shard_<k>/ as well.
storage.system_dir /var/lib/luradb/luradb_sys Users, key hashes and permissions, and the lock file.
json.wal_path, json.vlog_path, json.sstable_dir luradb_json.wal and so on Below /var/lib/luradb, relative to the working directory.
rel.wal_path, rel.vlog_path, rel.sstable_dir luradb_rel.wal and so on The same.
backup.dir luradb_backups /var/lib/luradb/luradb_backups.
log.path not set No files: the log goes to the journal.
server.unix_socket_path /run/luradb/luradb.sock Mode 0660, group luradb.
server.tls_cert_path, server.tls_key_path /etc/luradb/tls/server.crt and server.key Read at start.

With ProtectSystem=strict, systemd mounts the file system read-only for the service, apart from the state and runtime directories. A data, log or backup path outside /var/lib/luradb, for example a disk mounted at /mnt/backup, is therefore not writable until you allow it in a drop-in, which systemctl edit creates and keeps across updates of the package:

Terminal window
sudo systemctl edit luradb
[Service]
ReadWritePaths=/mnt/backup

Restart the service after it. Nothing below /home is reachable (ProtectHome=true), and the service has a private /tmp. The same drop-in is where MemoryHigh= goes, which caps the memory of the service from the system side in addition to resources.memory_budget.

The socket directory has the mode 0750 and the socket 0660, so membership of the group luradb is what lets a local user connect. Files the server writes are group-readable (UMask=0027). Add users to the group only if they may use the socket, see Unix socket and trusted users.

The server ends with an exit code that tells systemd what to do:

Exit code Meaning systemd
0 A stop through SIGTERM or SIGINT. No restart, because the exit is not a failure.
1 The start was refused, for example for an invalid configuration. Restarts after two seconds, until the start limit is reached.
3 The disk reported an error while making data durable, or writes to a data file failed for good. Restarts; the start recovers every confirmed write from the write-ahead log.
4 At the start, a volume has less free space than the recovery needs. No restart (RestartPreventExitStatus=4). Free space, then start the service.

The unit allows three starts within five minutes, manual ones included. After that, systemd refuses to start the service until you reset the counter:

Terminal window
sudo systemctl reset-failed luradb

A refused start, such as a mistake in the configuration, therefore ends in a unit that systemd no longer starts after the third attempt. Read the message with journalctl -u luradb -b, fix it, run reset-failed and start again. Troubleshooting lists the messages, and Resource guard explains the codes 3 and 4.

The Docker setup is a way to try LuraDB on any machine with Docker; it is not a distribution image. It builds an image from a .deb you provide, so nothing is compiled, and nothing beyond the ubuntu:24.04 base image is pulled. Install has the steps. The container shares the kernel of its host (with Docker Desktop, of its virtual machine), which must be Linux 6.1 or newer.

compose.yaml is the whole setup:

name: luradb
services:
luradb:
build:
context: .
additional_contexts:
debdir: ../../target/debian
image: luradb:local
init: true
# Docker's default seccomp profile blocks io_uring — LuraDB requires it.
security_opt:
- seccomp=unconfined
# io_uring registered buffers need memlock (see systemd unit).
ulimits:
memlock: -1
ports:
- "127.0.0.1:${LURADB_PORT:-3000}:3000"
volumes:
- data:/var/lib/luradb
- etc:/etc/luradb
healthcheck:
test: ["CMD", "curl", "-fs", "http://127.0.0.1:3000/health"]
interval: 5s
timeout: 3s
retries: 5
start_period: 15s
volumes:
data:
etc:
Setting Effect
security_opt: seccomp=unconfined Docker’s default seccomp profile blocks the io_uring calls LuraDB needs. Without this, a plain docker run fails at start.
ulimits: memlock: -1 The counterpart of LimitMEMLOCK=infinity in the unit.
init: true Docker runs an init process as process 1 of the container, with the server as its child.
ports The container’s port 3000 is published on 127.0.0.1 of the host only. LURADB_PORT picks the host port.
volumes data holds /var/lib/luradb, etc holds /etc/luradb, so the data, the configuration and the admin key outlive the container.
healthcheck curl -fs on /health every five seconds. It fails on 503 and passes on a degraded server that answers 200.

The image installs the package like a target system and then changes three lines of the packaged configuration: the bind address becomes 0.0.0.0, because Docker’s port mapping reaches the container through its network interface and the host-side bind stays local, the Swagger UI is switched on, and the admin key goes back to the text @GENERATED_ON_INSTALL@, so that no key is baked into an image layer. Authentication stays on. The entrypoint stands in for what systemd does on the host: it creates /var/lib/luradb and /run/luradb, generates the admin key on the first start, and starts the server as luradb.

The admin key is printed once, on the first start, and kept in the etc volume. Set LURADB_ADMIN_KEY in the container’s environment before that first start to use a key of your own; the stock compose.yaml forwards no host variables, so this takes an environment: entry of your own or docker compose run -e.

Terminal window
docker compose up -d
docker compose logs luradb | grep "admin api key"
docker compose exec luradb grep api_key /etc/luradb/luradb.toml
curl http://localhost:3000/health

The stock compose.yaml sets no restart: policy, so a stopped container stays stopped. Docker cannot leave out an exit code from a restart policy, which matters because of the codes above: restart: on-failure:3 restarts after exit code 3 and after any failure at most three times, while restart: unless-stopped or restart: always would restart exit code 4 in a loop.

The configuration is the packaged one, in the etc volume, with the Unix socket at /run/luradb/luradb.sock inside the container. Removing the volumes removes the data, the configuration and the key together.