Skip to content

Container Deployment

Spurline ships with a production-oriented container build for AMD64 and ARM64. It runs as an unprivileged user, stores its SQLite database in /data, and serves Nostr WebSocket traffic and HTTP probes on port 8080.

Start with Docker Compose

Build and start the relay:

cp .env.example .env
sudo install -d -o 10001 -g 10001 /mnt/bitcoin/spurline
docker compose up --build --detach
docker compose ps

Docker Compose reads .env automatically. SPURLINE_DATA_DIR is required so the database location cannot silently change between deployments. The supplied example sets it to /mnt/bitcoin/spurline.

The supplied deployment example binds port 8780 on all host interfaces so a separate reverse proxy server can reach it:

ws://SPURLINE_SERVER_IP:8780/
http://SPURLINE_SERVER_IP:8780/health
http://SPURLINE_SERVER_IP:8780/info

Restrict inbound TCP port 8780 to the reverse proxy server at the host firewall. Set SPURLINE_PUBLIC_URL to the external wss:// URL served by that proxy.

Follow runtime logs with:

docker compose logs --follow spurline

The fixed-name spurline-data Docker volume uses the local driver to bind the host directory /mnt/bitcoin/spurline at /data inside the container. This gives Docker a named volume while keeping the database at an explicit host location. Create the directory with ownership matching Spurline's unprivileged container user before the first start:

sudo install -d -o 10001 -g 10001 /mnt/bitcoin/spurline

The database therefore lives at:

/mnt/bitcoin/spurline/spurline.sqlite3

This directory survives container replacement. Normal updates do not erase relay events:

docker compose up --build --detach --force-recreate

docker compose down leaves the named volume, host directory, and database untouched. Removing the Docker volume registration does not replace a proper backup policy for the host directory.

To back the named volume with another host location, edit SPURLINE_DATA_DIR in .env before Compose first creates the volume:

SPURLINE_DATA_DIR=/srv/spurline

Docker records the selected device path in the volume definition. If you later change SPURLINE_DATA_DIR, remove and recreate the Docker volume registration before starting Spurline against the new directory. Do not delete the underlying host data.

Publish on another interface

The example .env supports a reverse proxy running on a different server:

SPURLINE_BIND_ADDRESS=0.0.0.0
SPURLINE_PORT=8780
SPURLINE_PUBLIC_URL=wss://relay.example.com

If the reverse proxy runs on the Spurline host itself, bind to 127.0.0.1 instead. In either topology, the proxy must support WebSocket upgrades and publish wss:// to clients rather than exposing the plain WebSocket port directly.

Configuration

Compose reads these deployment settings from .env:

Variable Example value Purpose
SPURLINE_DATA_DIR /mnt/bitcoin/spurline Host directory backing spurline-data
SPURLINE_BIND_ADDRESS 0.0.0.0 Host interface publishing the relay port
SPURLINE_PORT 8780 Published host port
SPURLINE_PUBLIC_URL unset External wss:// relay URL advertised in metadata
SPURLINE_VERIFY_SIGNATURES true Verify Nostr event signatures

The Compose container target remains port 8080 even when a different host port is selected.

The image recognizes these environment variables:

Variable Container default Purpose
SPURLINE_HOST 0.0.0.0 Internal bind address
SPURLINE_PORT 8080 Internal HTTP and WebSocket port
SPURLINE_DATABASE /data/spurline.sqlite3 SQLite database path
SPURLINE_VERIFY_SIGNATURES true Verify Nostr event signatures
SPURLINE_PUBLIC_URL unset External relay URL used by /info

Signature verification should remain enabled outside disposable test fixtures.

Published image

Pushes to main publish a multi-architecture image to GitHub Container Registry:

docker pull ghcr.io/trbouma/spurline:latest
docker run --detach \
  --name spurline \
  --publish 127.0.0.1:8780:8080 \
  --volume /mnt/bitcoin/spurline:/data \
  ghcr.io/trbouma/spurline:latest

Tagged releases also receive their Git tag as an image tag. The build targets linux/amd64 and linux/arm64, including Raspberry Pi 4 installations using a 64-bit operating system.

Operational boundaries

Run one Spurline process against each SQLite database. Live WebSocket subscriptions are held in process memory, so adding Uvicorn workers would partition connected clients and event fan-out. Future horizontal scaling requires an explicit shared coordination design rather than a worker-count change.