Nostri Relay Admin Guide
A self-hosted Nostr relay supporting kinds 0, 3, 4, 13, 1059, and 10002.
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
| Requirement | Notes |
|---|---|
| PostgreSQL 14+ | Running as a Docker container or externally |
| Docker | Required for container deployment |
| Rust 1.95+ | Only needed to build from source |
| DNS record | relay.nostri.me pointing to your server |
| Reverse proxy | With 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
| Variable | Default | Description |
|---|---|---|
| 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:
| Table | Purpose |
|---|---|
| events | All stored Nostr events |
| event_tags | Index 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
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:
- Reads the version from
Cargo.toml. - Checks that Docker is running and the Synology volume is mounted.
- Builds a
linux/amd64image tagged:<version>and:latest. - Pushes both tags to
gitea.markwagner.me/maverick/nostri-relay. - Copies
deploy/docker-compose.ymlto/Volumes/docker/NostriRelay/on the mounted Synology share.
/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)
- Open DSM → Control Panel → Application Portal → Reverse Proxy.
- Create a new rule:
- Source: HTTPS, hostname
relay.nostri.me, port 443 - Destination: HTTP, localhost, port 3001
- Source: HTTPS, hostname
- Open the Custom Header tab.
- Click Create → WebSocket. This adds the two required headers:
Upgrade: $http_upgradeConnection: $connection_upgrade
- Save.
Updating to a new version
- Bump the version in
Cargo.toml. - Mount the Synology share, then run
./deploy.sh— this builds, pushes to the registry, and copies the compose file. - SSH into Synology and run
docker compose pull && docker compose up -dfrom/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.
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
| NIP | Description |
|---|---|
| NIP-01 | Basic protocol — EVENT, REQ, CLOSE, EOSE, OK, NOTICE |
| NIP-04 | Encrypted direct messages (kind 4) |
| NIP-11 | Relay information document (GET / with Accept: application/nostr+json) |
| NIP-17 | Private direct messages — uses kinds 13 and 1059 |
| NIP-42 | Authentication — required to read kind 4, 13, and 1059 events |
| NIP-44 | Versioned encryption (used by NIP-17 messages) |
| NIP-59 | Gift Wrap — kinds 13 (Seal) and 1059 (Gift Wrap) for private messaging |
| Kind | Name | Storage | Access |
|---|---|---|---|
| 0 | User Metadata (profiles) | Replaceable — only latest per pubkey kept | Public |
| 3 | Contact List (follows) | Replaceable — only latest per pubkey kept | Public |
| 4 | Encrypted Direct Messages (NIP-04) | Append-only | NIP-42 auth — sender & recipient only |
| 13 | Seal (NIP-59) | Append-only | NIP-42 auth — p-tag recipient only |
| 1059 | Gift Wrap (NIP-59) | Append-only | NIP-42 auth — p-tag recipient only |
| 10002 | Relay List Metadata | Replaceable — only latest per pubkey kept | Public |
Troubleshooting
Container fails to start
Check logs for the error:
docker logs nostri-relay
Common causes:
DATABASE_URLis wrong or the database doesn't exist yet.- The
nostri_relay_dbnetwork doesn't exist — rundocker network create nostri_relay_db. - The PostgreSQL container is not connected to
nostri_relay_db.
Clients connect but receive no events
- Verify the reverse proxy WebSocket headers are set (see Step 5 above).
- Confirm
RELAY_URLmatches the URL clients are using exactly (wss://vsws://).
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