This repository manages automated deployment of the Strfry Nostr Relay for Bitcoin District. Deployment is fully automated via GitHub Actions, with submodule updates, build, configuration, and systemd service management.
┌───────────────────────────┐
│ Upstream strfry Repo │
│ (new commits/releases) │
└─────────────┬────────────┘
│
▼
┌───────────────────────────┐
│ auto-bump-strfry.yml │
│ GitHub Actions Workflow │
│ - Checks for upstream │
│ commits on strfry/main │
│ - Builds submodule │
│ - Runs runtime sanity │
│ check (--version/--help)│
│ - Opens a PR if successful│
└─────────────┬────────────┘
│ PR created
▼
┌───────────────────────────┐
│ Manual Review & Merge PR │
│ - Human confirms build │
│ + sanity checks pass │
└─────────────┬────────────┘
│ Merge to main
▼
┌───────────────────────────┐
│ deploy.yml │
│ GitHub Actions Workflow │
│ - Checkout repo & submodules
│ - SSH to server │
│ - Run deploy.sh │
│ • Build strfry │
│ • Copy runtime config │
│ • Restart systemd │
│ - Smoke test with config │
└─────────────┬────────────┘
│ Success → Production
▼
┌───────────────────────────┐
│ Live strfry Relay │
│ - Running with deployed │
│ repo-controlled config │
│ - Systemd ensures restart │
│ on failure │
└───────────────────────────┘
-
Strfry (relay) uses
plugins/nip05_gate.pyto accept/reject incoming events.- What it does: Supports two gate modes:
open(accepts all events) andnip05(builds an allowlist from.well-known/nostr.jsondocuments and accepts events whosepubkeyis present innames,verified_names, or both). - Behavior:
- Open mode: Accepts all valid events after basic validation (event ID and pubkey format).
- NIP05 mode: Non-blocking event path; background refresh with per-URL backoff and conditional HTTP (ETag/Last-Modified; 304 treated as success).
- Optional startup fail-open for regular kinds via
--startup-grace-seconds(ephemeral kinds remain rate-limited). - Ephemeral kinds
20000–29999are governed by per-pubkey token buckets (EPHEMERAL_*), independent of the allowlist. - Optional bypass for
Import/Sync/Streamsources when--allow-importis enabled.
- Key flags/env:
STRFRY_GATE_MODE(open|nip05),NIP05_JSON_URLS/NIP05_JSON_URL,NIP05_FIELD(names|verified_names|both),ALLOW_IMPORT,STARTUP_GRACE_SECONDS,EPHEMERAL_RATE,EPHEMERAL_BURST,EPHEMERAL_MAX_BUCKETS,EPHEMERAL_TTL_SECONDS. - Scope: Only affects the relay's event ingestion; does not interact with the auth proxy.
- What it does: Supports two gate modes:
-
Blossom (media uploads) uses
scripts/nostr-auth-proxy/, not the strfry plugin.- What it does: Validates upload requests in front of Blossom using NIP‑98 (or legacy 24242) and NIP‑05 mapping, with modes
nip05(default),allowlist, oropen. - Key env:
GATE_MODE,ALLOWLIST_FILE,REQUIRED_NIP05_DOMAIN, plus timing/cache knobs likeCACHE_TTLandSKEW_SECONDS. - Scope: Only affects Blossom upload routes (via nginx
auth_request); does not affect relay events.
- What it does: Validates upload requests in front of Blossom using NIP‑98 (or legacy 24242) and NIP‑05 mapping, with modes
In short: the relay is gated by nip05_gate.py, and the media server is gated by nostr-auth-proxy. They are independent and can run side‑by‑side when both services are enabled.
nostr-stack-deploy/
├─ .github/workflows/
│ ├─ deploy.yml # CI/CD deploy workflow
│ └─ auto-bump-strfry.yml # PR workflow for upstream updates
├─ scripts/
│ └─ deploy.sh # Server-side deploy script
│ └─ dashboard/ # Stats generation scripts
│ └─ generate_stats.sh
├─ configs/
│ ├─ strfry.conf # Repo-controlled Strfry config
│ ├─ strfry.service # Systemd service unit
│ └─ nginx/ # Nginx configuration files
│ └─ relay.bitcoindistrict.org.conf
│ └─ dashboard/ # Dashboard units and env
│ ├─ dashboard.env
│ ├─ relay-dashboard.service
│ ├─ relay-dashboard-stats.service
│ └─ relay-dashboard-stats.timer
├─ web/
│ └─ relay-dashboard/ # Static dashboard frontend
│ └─ index.html
└─ strfry/ # Submodule pointing to Strfry upstream
This repo now uses modular scripts and environment files.
- Copy
configs/default.envto.env(optional for local/dev) and adjust. - For environment-specific overrides, create
configs/production.env,configs/staging.env, etc. - Secrets (API tokens, SSH keys) should be provided via CI/CD secrets or host environment and not committed.
Priority when loading variables:
configs/default.env.envin repo rootconfigs/${DEPLOY_ENV}.env- Variables injected by the environment (CI secrets) override all
scripts/deploy.sh: Orchestrator. Loads config, then calls modules.scripts/setup-system.sh: Base packages, swap, firewall.scripts/setup-nginx.sh: Nginx and certificates for the relay${DOMAIN}.scripts/build-strfry.sh: Builds strfry with sensible parallelism.scripts/setup-strfry.sh: Installs runtime config and systemd service for strfry.scripts/setup-dashboard.sh: Optional dashboard (gated byDASHBOARD_ENABLED).scripts/setup-blossom.sh: Optional Blossom + nostr-auth-proxy (gated byBLOSSOM_ENABLED).scripts/deploy_legacy.sh: Previous monolithic script retained for fallback.
Templates live in configs/nginx/*.template and are rendered with envsubst at deploy time.
The GitHub Actions workflow deploy.yml invokes the orchestrator and passes variables/secrets from repository settings. Set vars.DEPLOY_ENV to select configs/${DEPLOY_ENV}.env.
-
Ubuntu 24.04 server with a
deployuser. -
SSH access from GitHub Actions:
- Add private key as
DEPLOY_SSH_KEYsecret.
- Add private key as
-
Install dependencies (automatically handled by
deploy.sh):
sudo apt-get update
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev pkg-config \
liblmdb-dev libflatbuffers-dev libsecp256k1-dev libzstd-dev zlib1g-devMinimum Requirements:
- RAM: 1GB (with automatic swap space configuration)
- Storage: 20GB+ (for OS, dependencies, and strfry database)
- CPU: 1 vCPU (compilation will be single-threaded)
Recommended Requirements:
- RAM: 2GB+ (enables parallel compilation)
- Storage: 40GB+ (for larger databases and future growth)
- CPU: 2+ vCPUs (faster compilation and better performance)
Performance Notes:
- 1GB RAM droplets will use single-threaded compilation (slower but reliable)
- 2GB+ RAM droplets will use parallel compilation (faster build)
- The deploy script automatically optimizes for your system's capabilities
| Secret Name | Description |
|---|---|
| DEPLOY_HOST | Server hostname or IP |
| DEPLOY_USER | Deploy username |
| DEPLOY_SSH_KEY | Private SSH key for deploy user |
| GITHUB_TOKEN | Default GitHub Actions token |
| CLOUDFLARE_API_TOKEN | Cloudflare API token for DNS-01 (optional) |
configs/strfry.confcontains runtime configuration.configs/strfry.servicedefines the systemd service.- Both are version-controlled and copied to runtime locations by
deploy.sh.
- Installs all required build dependencies.
- Automatically configures swap space for memory-constrained systems.
- Adaptive compilation - uses single-threaded compilation on low-memory systems (<2GB RAM).
- Installs and configures nginx with reverse proxy to strfry.
- Obtains SSL certificates with Let's Encrypt using either:
- HTTP-01 via the Nginx plugin (default), or
- DNS-01 via Cloudflare (when enabled), which works with proxied orange-cloud records.
- Configures UFW firewall (SSH + port 7777 + HTTP/HTTPS).
- Builds Strfry from submodule with optimal settings for your system.
- Copies config to
$HOME/.strfry/strfry.conf. - Ensures data directory exists.
- Restarts systemd service.
- Performs a smoke test to verify binary runs with config.
- Optionally deploys a modular dashboard (NIP-11 + lightweight stats) served as static files behind nginx. Controlled via env.
You can customize the deployment via environment variables found in configs/default.env
Guidance:
-
Put SECRETS (e.g.,
CLOUDFLARE_API_TOKEN, SSH keys) in your CI/CD secrets or host environment. Do not commit them. -
Put non-secret SETTINGS in a
.envfile on the server or export before runningscripts/deploy.sh. You can base this onconfigs/env.example. -
Repo-controlled configs live in
configs/and are safe to commit (e.g., nginx vhost templates, systemd unit files, blossom config template). Runtime copies are written to/etc/...by the deploy script. -
DOMAIN: Relay FQDN. Must resolve to the server. -
CERTBOT_EMAIL: Email used for Let's Encrypt registration/alerts. -
CLOUDFLARE_ENABLED: Settrueto use Certbot's Cloudflare DNS plugin (DNS-01). -
CLOUDFLARE_API_TOKEN: Cloudflare API token with Zone DNS Edit permissions. -
DASHBOARD_ENABLED: Whentrue, installs static dashboard and timer to generate stats JSON. -
DASHBOARD_DOMAIN: FQDN for the dashboard vhost.
When the dashboard is enabled, the following are installed without touching the main relay service:
- Static files to
/var/www/relay-dashboard. - Separate Nginx vhost for
DASHBOARD_DOMAIN(HTTP → HTTPS redirect; serves only static files). - Systemd units:
relay-dashboard.service(provisions webroot) andrelay-dashboard-stats.timer/.service(refreshesstats.jsonand cachednip11.json). - Env file at
configs/dashboard/dashboard.envallows overridingSTRFRY_BIN,STRFRY_CONFIG,DASHBOARD_ROOT,NIP11_URL.
When Cloudflare is enabled, certificates are issued via DNS-01 and work with proxied (orange-cloud) DNS records. Otherwise, HTTP-01 is used via the Nginx plugin.
-
auto-bump-strfry.yml
- Checks upstream
strfryrepo for updates. - Runs build and runtime checks.
- Opens a PR if the submodule can be safely updated.
- Checks upstream
-
deploy.yml
- Triggered on
mainpush. - Syncs repo to server and runs
deploy.sh. - Smoke test confirms deployment success.
- Triggered on
# On your server:
adduser deploy
usermod -aG sudo deploy
mkdir -p ~/nostr-stack-deploy
# Clone the repo manually for first deploy (optional if using GitHub Actions)
git clone --recurse-submodules git@github.com:BitcoinDistrict/nostr-stack-deploy.git ~/nostr-stack-deploy
# Run the automated deployment script
# Optionally set environment variables inline
cd ~/nostr-stack-deploy
DOMAIN=relay.bitcoindistrict.org \
CERTBOT_EMAIL=you@example.com \
CLOUDFLARE_ENABLED=true \
CLOUDFLARE_API_TOKEN=cf_XXXXXXXXXXXXXXXXXXXXXXXXXXXX \
DASHBOARD_ENABLED=true \
DASHBOARD_DOMAIN=dashboard.relay.bitcoindistrict.org \
bash scripts/deploy.sh
# Verify deployment
sudo systemctl status strfry
sudo journalctl -u strfry -fWe will add a Blossom media server to the stack to handle blob storage (images/files) using the upstream implementation. Reference: hzrd149/blossom-server.
- Service
blossom-serveron the relay host (containerized by default for reproducibility). - Repo-controlled config at
configs/blossom/config.yml. - Systemd unit
configs/blossom/blossom.serviceto manage the service. - Nginx vhost for
BLOSSOM_DOMAINwith TLS and reverse proxy to localhost. - Upload gating: Only NIP‑05 verified pubkeys may upload; reads remain public.
BLOSSOM_ENABLED=true
BLOSSOM_DOMAIN=media.relay.bitcoindistrict.org
BLOSSOM_CONTAINER_IMAGE=ghcr.io/hzrd149/blossom-server:master
BLOSSOM_PORT=3300
BLOSSOM_MAX_UPLOAD_MB=16
BLOSSOM_GATE_MODE=nip05 # nip05 | allowlist | open
BLOSSOM_ALLOWLIST_FILE=/etc/blossom/allowlist.txt
Notes:
- Certificates are issued the same way as the relay (HTTP‑01 or Cloudflare DNS‑01).
BLOSSOM_PORTlistens on127.0.0.1; nginx handles public TLS onBLOSSOM_DOMAIN.- A non-container option (Node under systemd using npx) will be supported via a flag; container is default.
We will enforce uploads from Nostr verified pubkeys via an auth proxy in front of Blossom:
- Require NIP‑98 signed requests to authenticate the uploader's pubkey.
- Require header
X-NIP05: <name@domain>; resolve and verify.well-known/nostr.jsonmaps to the authenticated pubkey. - Implemented as a minimal auth service; nginx uses
auth_requeston upload routes. Modes:nip05(default): full NIP‑98 + NIP‑05 validation.allowlist: only pubkeys inBLOSSOM_ALLOWLIST_FILEmay upload.open: no gate (not recommended).
Downloads remain public.
configs/blossom/config.yml: Base server config (data dir/var/lib/blossom, base URLhttps://${BLOSSOM_DOMAIN}, limits from env).configs/blossom/blossom.service: Systemd unit (container or node mode) with persistent data volume.configs/nginx/${BLOSSOM_DOMAIN}.conf: Nginx vhost with TLS, caching headers, gzip, proxy to127.0.0.1:${BLOSSOM_PORT};auth_requeston upload endpoints when gated.scripts/nostr-auth-proxy/: Minimal service validating NIP‑98 and NIP‑05; returns 2xx/4xx for nginxauth_request.scripts/deploy.sh: Add gated provisioning behindBLOSSOM_ENABLED.
curl -X POST \
-H "Authorization: Nostr <nip98-signed-event>" \
-H "X-NIP05: alice@example.com" \
-F "file=@/path/to/image.jpg" \
https://$BLOSSOM_DOMAIN/uploadThe auth proxy supports three gate modes:
nip05(default): Requires NIP-98 signed requests and validates NIP-05 mappingallowlist: Only allows pubkeys listed inNOSTR_AUTH_ALLOWLIST_FILEopen: No authentication required (not recommended for production)
The deploy script automatically configures volume mounts only when using allowlist mode, avoiding invalid Docker mount specifications.
Consult upstream docs for exact endpoints and config: hzrd149/blossom-server.
NIP‑98 reference: kind 27235 events with empty content, u (absolute URL) and method tags must be signed, and created_at must be within a short window (default 60s). The proxy enforces these checks and validates NIP‑05 mapping before allowing uploads. See spec: NIP‑98.
- Secrets:
CLOUDFLARE_API_TOKEN, SSH keys: store as GitHub Actions secrets or server env vars. Never commit.
- Non-secrets (safe in
.envon the server):DOMAIN,CERTBOT_EMAIL,DASHBOARD_*,BLOSSOM_*,NOSTR_AUTH_*.
- Files managed by deploy script:
/etc/blossom/config.ymland/etc/default/*are generated fromconfigs/and env values.
- Commit new config, systemd unit, nginx vhost, and auth proxy; guard with
BLOSSOM_ENABLED. - Extend
deploy.shto provision Docker (if needed), install files, obtain certs, start services, and smoke‑test. - Stage with
BLOSSOM_GATE_MODE=allowlistand verify uploads/reads end‑to‑end. - Switch to
BLOSSOM_GATE_MODE=nip05and validate NIP‑98/NIP‑05 flow. - Enable in production.
- Always update
<YOUR_ADMIN_NOSTR_PUBKEY>instrfry.confbefore deploying. - Changes to
configs/are version-controlled and deployed automatically. - Submodule updates go through PR review for safety.