← nostrmail.org

Run your own bridge.

Connect legacy email and Nostr on infrastructure you control. Pick your setup below. You'll get one .env and one docker-compose.yml to copy, tailored to your choices.

Configure my setup ↓
What do you want to do?
Outbound SMTP
Policy & identity Advanced

Optional. A whole domain (example.com) or a subdomain (nostr.example.com) both work. Whatever you enter is used for the mail host, DNS records, addresses and tests below.

Setup

Everything runs from published images with Docker Compose. The files below are generated for your selection. Internal wiring (service hostnames, the shared signing key) is already filled in.

First: your SMTP server

postfix-rspamd-docker ships its own config, so it runs as a separate stack. Repo →

Prerequisites: a public IP whose reverse DNS (PTR) matches MAIL_HOSTNAME, your sending domain(s), and port 587 open.

postfix .env
MAIL_HOSTNAME=mail.example.com
MAIL_DOMAINS=example.com
SMTP_USERS=postmaster@example.com:change-me-strong-password
SUBMISSION_PORT=587
Run it, then generate DKIM keys
git clone https://github.com/nogringo/postfix-rspamd-docker.git
cd postfix-rspamd-docker
cp .env.example .env
# edit .env: set MAIL_HOSTNAME, MAIL_DOMAINS, SMTP_USERS
docker compose up -d
docker compose exec rspamd rspamadm dkim_keygen -d example.com -s dkim -b 2048
cd ..

It publishes 587 on the host, so the bridge reaches it at mail.example.com on port 587 (its MAIL_HOSTNAME). Use the SMTP_USERS account as SMTP_USERNAME / SMTP_PASSWORD in the .env below, and publish the DNS records further down before sending.

Create .env

Make a new folder, save this as .env, and fill in the placeholder values.

.env

Generate WEBHOOK_SIGNING_KEY with openssl rand -hex 32. It is defined once here and every service that needs it reads it from this file.

Generate each decision token with openssl rand -hex 32 too.

Set NIP05_PRIVATE_READERS to the npub of NOSTR_PRIVATE_KEY, otherwise mail to private aliases is rejected.

Create docker-compose.yml

Save this next to your .env, in the same folder.

docker-compose.yml

Launch

Bring the whole stack up.

docker compose up -d

nmail-api sets up and upgrades its database at startup. NIP-05 is then served at /.well-known/nostr.json?name=<local-part>, and the admin console at /admin when you set ADMIN_PASSWORD.

Upgrading an install made with an earlier version of this guide? nmail-api refuses to start until its database reaches migration 011. Catch it up once, then pull:

One-time catch-up for older installs
git clone --depth 1 https://github.com/nogringo/nmail-api.git
for f in nmail-api/migrations/0{07,08,09,10,11}_*.sql; do docker compose exec -T postgres psql -U nmail -d nmail < "$f"; done
docker compose pull nmail-api && docker compose up -d

DNS records

Add these at your DNS provider for the domain set above. Names are shown in full: if your provider adds the domain itself, enter only the part before it.

Inbound

Route legacy mail to your bridge server.

Type Name Value
MX Priority 10
A Your server's IPv4
AAAA Your server's IPv6 Only if port 25 is reachable over IPv6

Self-hosted SMTP

Lets receiving servers trust the mail you send.

Type Name Value
A Your server's IPv4
AAAA Your server's IPv6 Only if it has one
TXT SPF
TXT The record printed by rspamadm dkim_keygen DKIM
TXT DMARC

Reverse DNS (PTR) Hosting provider

This one is not set at your DNS provider. In your server host's panel (often under "Reverse DNS" or "rDNS"), point your server's IP to mail.example.com, the same value as MAIL_HOSTNAME.

Provider SMTP

Your SMTP provider (Mailgun, SendGrid, Amazon SES, etc.) documents the exact SPF, DKIM and tracking-CNAME records to add for your sending domain. Follow their dashboard. No mail server to run.

NIP-05 identity

To serve NIP-05 from your domain, expose nmail-api's /.well-known/nostr.json at https://example.com/.well-known/nostr.json (reverse-proxy it). Keep the original Host header (Caddy does by default, nginx needs proxy_set_header Host $host): private aliases are resolved with NIP-98 requests signed for that host.

Test it

Inbound

Send a normal email from any mail client to a bridged address, then watch it flow and confirm the Nostr event arrives in your client / relays.

docker compose logs -f haraka-webhook webhook

Outbound

From a Nostr Mail client, send a message to a legacy email address and confirm delivery (check spam too). Watch the bridge:

docker compose logs -f bridge

Self-hosted SMTP

Verify SMTP auth + TLS, then score deliverability against a service like mail-tester.com before going live.

swaks --to test@example.com --from postmaster@example.com \
  --server mail.example.com:587 --tls \
  --auth LOGIN --auth-user postmaster@example.com --auth-password 'change-me-strong-password' \
  --body "Test from my bridge"

Policy & identity

Hit the NIP-05 endpoint and confirm a decision call appears in the nmail-api logs when you send or receive.

curl 'http://localhost:3000/.well-known/nostr.json?name=alice'