Nostri Relay Admin Guide

A self-hosted Nostr relay supporting kinds 0, 3, 4, 13, 1059, and 10002.

Nostri Relay is part of the Nostri service. Learn more and get started at https://nostri.me

Overview

Nostri Relay is a standalone Nostr relay written in Rust. It handles WebSocket connections from Nostr clients, stores events in PostgreSQL, and fans out new events to all active subscribers in real time.

It is designed to run as a Docker container alongside the Nostri NIP-05 service, sharing a PostgreSQL server but using a separate database (nostri_relay).

Prerequisites

RequirementNotes
PostgreSQL 14+Running as a Docker container or externally
DockerRequired for container deployment
Rust 1.95+Only needed to build from source
DNS recordrelay.nostri.me pointing to your server
Reverse proxyWith WebSocket support enabled (see Synology)

Configuration

Configuration is entirely through environment variables. Copy .env.example to .env and fill in your values. In Docker, the env_file directive in docker-compose.yml loads this file automatically.

Environment Variables

VariableDefaultDescription
DATABASE_URL required PostgreSQL connection string, e.g. postgres://user:pass@host:5432/nostri_relay. Use the container name as the host when running in Docker.
PORT 3001 TCP port the relay listens on.
RELAY_NAME Nostri Relay Display name shown in the NIP-11 relay info document.
RELAY_DESCRIPTION empty Short description shown in NIP-11.
RELAY_URL wss://localhost:3001 Full public WebSocket URL (e.g. wss://relay.nostri.me). Used to validate NIP-42 auth events — must match exactly what clients connect to.
RELAY_PUBKEY optional Operator's hex public key, shown in NIP-11.
RELAY_CONTACT optional Operator contact, e.g. mailto:you@example.com, shown in NIP-11.
RELAY_ICON optional URL to the relay's icon image, shown in NIP-11.
APP_ENV development Deployment environment label (e.g. production, development). Printed in the startup log line for quick verification.
RUST_LOG nostri_relay=info Log level. Options: trace, debug, info, warn, error.

Database Setup

Nostri Relay uses its own PostgreSQL database, separate from the Nostri NIP-05 database.

Create the database

Connect to your PostgreSQL server and run:

CREATE DATABASE nostri_relay;

Migrations

Table creation is handled automatically by the application on startup via sqlx::migrate!. The migration files are in the migrations/ folder. You do not need to run them manually.

The following tables are created:

TablePurpose
eventsAll stored Nostr events
event_tagsIndex of single-char tags (#e, #p) for fast filter queries

Running Locally

Requires a PostgreSQL instance and a populated .env file in the project root.

# Development build
cargo run

# Release build
cargo build --release
./target/release/nostri-relay

Common development commands

cargo test                  # run all tests
cargo clippy -- -D warnings # lint
cargo fmt                   # format code

Docker

Registry

Images are published to the Gitea container registry at gitea.markwagner.me/maverick/nostri-relay. Both a versioned tag (e.g. :0.1.3) and :latest are pushed on each deploy.

Docker Compose

The deploy/docker-compose.yml file is the production compose configuration:

services:
  nostri-relay:
    image: gitea.markwagner.me/maverick/nostri-relay:latest
    restart: unless-stopped
    ports:
      - "3001:3001"
    env_file:
      - .env
    networks:
      - nostri_relay_db

networks:
  nostri_relay_db:
    external: true
The nostri_relay_db network is an external network — it must be created on the Docker host before starting the container. See the Synology section below.

deploy.sh

deploy.sh in the project root handles the full build-and-deploy cycle in one step:

./deploy.sh

It performs these steps automatically:

  1. Reads the version from Cargo.toml.
  2. Checks that Docker is running and the Synology volume is mounted.
  3. Builds a linux/amd64 image tagged :<version> and :latest.
  4. Pushes both tags to gitea.markwagner.me/maverick/nostri-relay.
  5. Copies deploy/docker-compose.yml to /Volumes/docker/NostriRelay/ on the mounted Synology share.
The Synology share must be mounted at /Volumes/docker before running the script. The script exits with an error if the mount is not found.

Synology NAS Deployment

Network architecture

Nostri Relay shares a PostgreSQL server with the Nostri app but uses a dedicated Docker network to reach it. Neither app has access to the other's containers.

nostri_default              nostri_relay_db
──────────────              ───────────────
[nostri app]  ──────────────── [postgres]
                          ──── [nostri-relay]

Step 1 — One-time network and database setup

SSH into your Synology and run the following once:

# Create the shared network
docker network create nostri_relay_db

# Connect the PostgreSQL container to it
docker network connect nostri_relay_db <postgres-container-name>

# Create the relay database (connect to postgres first)
docker exec -it <postgres-container-name> psql -U <user> -c "CREATE DATABASE nostri_relay;"

Step 2 — Prepare the .env file

Copy .env.example to deploy/.env and fill in your values. The DATABASE_URL host must be the PostgreSQL container name (not localhost), since containers communicate by name on the shared network:

DATABASE_URL=postgres://admin:yourpassword@postgres:5432/nostri_relay
PORT=3001
APP_ENV=production
RELAY_NAME=Nostri Relay
RELAY_DESCRIPTION=Private Nostr relay for profiles, contacts, direct messages, and relay lists
RELAY_URL=wss://relay.nostri.me
RUST_LOG=nostri_relay=info

Step 3 — Log in to the registry (one time)

SSH into Synology and authenticate with the Gitea registry so it can pull images:

docker login gitea.markwagner.me

Step 4 — Build and push from Mac

Mount the Synology share, then run deploy.sh from the project root. It builds the image, pushes it to the registry, and copies the compose file to the NAS:

./deploy.sh

Step 5 — Pull and start on Synology

SSH into Synology and run:

cd /volume1/docker/NostriRelay
docker compose pull
docker compose up -d

On first start, migrations run automatically and create the events and event_tags tables in nostri_relay.

Step 6 — Reverse proxy (DSM Application Portal)

  1. Open DSM → Control Panel → Application Portal → Reverse Proxy.
  2. Create a new rule:
    • Source: HTTPS, hostname relay.nostri.me, port 443
    • Destination: HTTP, localhost, port 3001
  3. Open the Custom Header tab.
  4. Click Create → WebSocket. This adds the two required headers:
    • Upgrade: $http_upgrade
    • Connection: $connection_upgrade
  5. Save.
WebSocket support must be enabled in the reverse proxy. Without the two headers above, Nostr clients will fail to connect even though the container is running.

Updating to a new version

  1. Bump the version in Cargo.toml.
  2. Mount the Synology share, then run ./deploy.sh — this builds, pushes to the registry, and copies the compose file.
  3. SSH into Synology and run docker compose pull && docker compose up -d from /volume1/docker/NostriRelay, or restart the container in Container Manager.

Optional — Automatic updates with Watchtower

Watchtower is a separate Docker container that polls the registry on an interval and automatically pulls and restarts containers when a new image is available — eliminating the need to SSH in after each deploy.

A ready-to-use compose file is provided at deploy/docker-compose-watchtower.yml. It uses label-based monitoring — Watchtower only touches containers that have the label com.centurylinklabs.watchtower.enable=true, which is already set on the nostri-relay service. Add the same label to any other service you want monitored (e.g. the Nostri NIP-05 container). This approach works correctly with Container Manager Projects, where container names are assigned by Synology and may not match the service name in the compose file.

Before starting Watchtower, log in to the registry on the Synology so it can authenticate when pulling new images:
docker login gitea.markwagner.me

Deploy Watchtower as a standalone stack — separate from the relay compose file:

docker compose -f docker-compose-watchtower.yml up -d

Once running, step 3 of the update process above is no longer needed. After ./deploy.sh pushes a new :latest image, Watchtower will detect and apply it within 5 minutes automatically.

Supported NIPs & Event Kinds

NIPDescription
NIP-01Basic protocol — EVENT, REQ, CLOSE, EOSE, OK, NOTICE
NIP-04Encrypted direct messages (kind 4)
NIP-11Relay information document (GET / with Accept: application/nostr+json)
NIP-17Private direct messages — uses kinds 13 and 1059
NIP-42Authentication — required to read kind 4, 13, and 1059 events
NIP-44Versioned encryption (used by NIP-17 messages)
NIP-59Gift Wrap — kinds 13 (Seal) and 1059 (Gift Wrap) for private messaging
KindNameStorageAccess
0User Metadata (profiles)Replaceable — only latest per pubkey keptPublic
3Contact List (follows)Replaceable — only latest per pubkey keptPublic
4Encrypted Direct Messages (NIP-04)Append-onlyNIP-42 auth — sender & recipient only
13Seal (NIP-59)Append-onlyNIP-42 auth — p-tag recipient only
1059Gift Wrap (NIP-59)Append-onlyNIP-42 auth — p-tag recipient only
10002Relay List MetadataReplaceable — only latest per pubkey keptPublic

Troubleshooting

Container fails to start

Check logs for the error:

docker logs nostri-relay

Common causes:

Clients connect but receive no events

Private events not delivered (kinds 4, 13, 1059)

Kinds 4, 13, and 1059 all require NIP-42 authentication. The client must respond to the AUTH challenge sent by the relay on connect. Additionally, for kinds 13 and 1059, the relay only delivers events where the authenticated pubkey matches a p tag on the event. Check that your Nostr client supports NIP-42.

Verify the relay is reachable

# NIP-11 info document
curl -H "Accept: application/nostr+json" https://relay.nostri.me

# Quick WebSocket test (requires wscat: npm i -g wscat)
wscat -c wss://relay.nostri.me