Running a mail server
A SithBit deployment is at most six services, all built from this workspace:
| Service | Binary | Role | Needed when |
|---|---|---|---|
sithbitd | mail-spooler | SMTP MX + submission, IMAP, POP, and the spooler workers in one process | always |
account-api | account-api | wallet-challenge login → JWT; mail passwords, timezone, DND schedules | users manage their accounts |
mail-grpc | mail-grpc | gRPC gateway to the Solana programs (postage checks, SendMail, aliases) | mail should reach the chain |
domain-sithbit | domain-sithbit | DNS-based domain verification and on-chain domain authorization | you operate the domain registry |
sithbit-ipfsd | sithbit-ipfsd | the self-hosted IPFS node as a standalone daemon: HTTP pin API + optional public swarm | a fleet shares one node via [ipfs] kind = "remote" (a single sithbitd can embed the node instead) |
sithbit-gateway | sithbit-gateway | read-only IPFS HTTP path gateway over the same block/pin bucket (deserialized, raw, CAR) | pinned mail blobs should be fetchable/verifiable over plain HTTP |
Everything else is an external dependency you point config at: a Solana
RPC endpoint, TLS certificates, and DNS records (see
DNS setup). Mail bodies pin to IPFS through the embedded
node, the shared sithbit-ipfsd, or a third-party service
(Filebase/Pinata) — the [ipfs] reference
covers the selection.
The sections below climb from a zero-config dev run to the production compose topology. Each rung is runnable on its own; pick the highest one you need.
Note: if you notice
smtp-server,imap-server, orpop-serverbinaries elsewhere in the workspace, see Appendix: Development and pilot servers — they’re dev/pilot artifacts, not part of this deployment.
Bare binaries (zero config)
Every binary runs with no config file at all and lands on loopback dev ports — see the configuration reference for the defaults and how to override them:
cargo run -p mail-spooler --bin sithbitd # SMTP :2525, IMAP :2143, POP :2110
cargo run -p account-api # HTTP :8180
cargo run -p domain-sithbit # HTTP :8181
cargo run -p mail-grpc # gRPC :50051 (reads .env)
cargo run -p ipfs-daemon # HTTP :8182 (pin API)
cargo run -p ipfs-gateway # HTTP :8183 (read-only gateway)
Without a [grpc] + [ipfs] section, sithbitd disables the
chain pipeline: mail is accepted, delivered to mailboxes, and readable
over IMAP/POP, but delivered copies stay in chain state received.
That is the expected dev shape, not an error.
For long-running processes, build with the max-performance profile
instead of cargo run’s dev profile:
cargo build --profile server -p mail-spooler -p account-api -p mail-grpc -p domain-sithbit -p ipfs-daemon -p ipfs-gateway
ls target/server/ # sithbitd, account-api, mail-grpc, domain-sithbit, sithbit-ipfsd, sithbit-gateway
Container images
One multi-stage Dockerfile (docker/Dockerfile) builds all six
services as separate targets:
docker build -f docker/Dockerfile --target sithbitd -t sithbit/sithbitd .
docker build -f docker/Dockerfile --target account-api -t sithbit/account-api .
docker build -f docker/Dockerfile --target mail-grpc -t sithbit/mail-grpc .
docker build -f docker/Dockerfile --target domain-sithbit -t sithbit/domain-sithbit .
docker build -f docker/Dockerfile --target sithbit-ipfsd -t sithbit/sithbit-ipfsd .
docker build -f docker/Dockerfile --target sithbit-gateway -t sithbit/sithbit-gateway .
The images carry no configuration — TOML files and environment come
from the compose layer or your orchestrator. Two properties of the
runtime image (distroless cc-debian12) matter to an operator:
- There is no shell in the image.
docker execinto a running service is impossible; usedocker logs,docker cp, and the monitoring surfaces instead. When copying a live SQLite database out withdocker cp, take the-waland-shmsidecar files too, or the copy will read as empty. - CA certificates are baked in, so outbound TLS (RPC providers, Filebase, smarthosts) works without extra mounts.
The compose dev stack
docker-compose.yml at the workspace root boots sithbitd,
account-api, domain-sithbit, sithbit-ipfsd, and sithbit-gateway
(sharing the ipfsd block volume) with empty (all-default) configs,
publishing the dev ports on loopback only:
docker compose up -d --build
docker/smoke.sh # or: probe by hand; KEEP=1 leaves the stack up
docker/smoke.sh proves each service actually answers its protocol —
SMTP/IMAP/POP banners, HTTP from the two web services, a pin→fetch
byte-for-byte roundtrip through sithbit-ipfsd’s pin API, and the same
CID re-fetched through sithbit-gateway’s read-only surface — and
tears the stack down unless KEEP=1. State lives in named volumes and
survives down; docker compose down -v resets it.
The chain pipeline is disabled in this stack, exactly like the bare zero-config run.
Adding the chain: the chain profile
docker compose --profile chain up -d
The profile adds mail-grpc pointed at a surfpool validator running
on the host (boot one by running the mail_client integration suite,
which deploys and seeds the programs). Two things to know:
- The service uses
network_mode: hostdeliberately: a loopback-bound surfpool is not reachable through Docker’shost-gatewayfrom a bridge network, somail-grpcshares the host network and serves on127.0.0.1:50051exactly like a native run. - Its signing keypair comes from
DEFAULT_KEYPAIRin./.env— the keypair JSON array itself, not a path to a file.
Set SITHBIT_CHAIN_RPC to point the profile at a remote cluster
instead of the host surfpool.
Cloud-store overlays
Two overlay files swap the SQLite store for the cloud backends, backed by local emulators — the same code paths a scaled-out production deployment uses:
docker compose -f docker-compose.yml -f docker-compose.aws.yml up -d # DynamoDB Local + ElasticMQ
docker compose -f docker-compose.yml -f docker-compose.azure.yml up -d # Azurite (tables, queues, blobs)
The emulators publish no host ports (the stack reaches them over the
compose network) and their state is ephemeral. Store-backed services
fail fast if their backend isn’t accepting connections yet; compose’s
restart: on-failure brings them up as soon as it is.
IPFS cluster demo
docker-compose.cluster.yml is a standalone file (not an overlay):
two sithbit-ipfsd nodes with [cluster] enabled over one minio
bucket — the shared-bucket cluster
shape. docker/cluster-smoke.sh is its chaos probe: pin through node
1, stop node 1, fetch the same CID through node 2.
Outbound mail and port 25
Many hosting providers — most cloud VPS platforms, and virtually all
consumer ISPs — block outbound connections on port 25 by default to
curb spam relayed from compromised or careless hosts. If sithbitd’s
relay worker sees connection timeouts or refusals handing mail to a
recipient’s MX, this is almost always the cause rather than a bug in
the relay logic; confirm with a manual connection test from the box
sithbitd runs on (nc -zv <mx-host> 25).
Two ways to unblock it, in order of preference:
-
Ask the provider to lift the block. Most cloud providers (AWS, GCP, Azure, DigitalOcean, …) will do this for a verified account in good standing on request. It’s the only path that keeps outbound delivery under your own PTR/DKIM identity end to end.
-
Route through a third-party gateway as a smarthost. If the block can’t be lifted — shared hosting, some residential/VPS plans, or while a request is pending — point
[spooler.smarthost]at a provider like SendGrid or Amazon SES. These accept mail over authenticated submission on 587/465, so an outbound port 25 block doesn’t affect them, and the gateway does the actual MX delivery from IPs with established sending reputation:[spooler.smarthost] host = "smtp.sendgrid.net" # or email-smtp.<region>.amazonaws.com for SES port = 587 user = "apikey" # SendGrid: literal string "apikey"; SES: your SMTP username password = "<api-key-or-smtp-password>" require_tls = true
Either way, DNS setup still applies: publish SPF that
include:s the gateway (SendGrid: include:sendgrid.net; SES:
include:amazonses.com), and DKIM — most gateways can sign on your
behalf too, but [spooler.dkim] keeps signing under your control if
you’d rather sign locally before handing off to the smarthost.
This only affects outbound relay. Inbound MX on port 25 (the rest of the world delivering mail to you) is rarely blocked by hosting providers; if it is, that’s a harder blocker to work around and usually means the provider isn’t suited to running a mail server at all.
Production
docker-compose.prod.example.yml is the annotated production shape:
copy it, search for CHANGE, and fill in your registry, config files,
and keys. Sanity-check with docker compose -f <file> config before
up -d. The structural decisions it encodes:
- Real config lives in mounted TOML files, not a wall of env vars.
Start from
mail_spooler/sithbitd.example.tomlandaccount_api/account_api.toml; containers find the file viaSITHBITD_CONFIG/ACCOUNT_API_CONFIG. - Standard mail ports map onto the unprivileged in-container binds:
host 25→2525 (MX), 587→2587 (submission), 143→2143 (IMAP),
110→2110 (POP). The binds move to
0.0.0.0in the TOML; the images never need root or privileged ports. - TLS for the mail protocols comes from the
[smtp.tls]/[imap.tls]/[pop.tls]sections over a mounted/certsdirectory. The two HTTP services (account-api,domain-sithbit) stay loopback-published and belong behind a TLS-terminating reverse proxy. - SQLite allows exactly one
sithbitd. Never scale the service while[store] kind = "sqlite". For replicas, switch the TOML to theaws/azurestore and work through the Scaling out checklist. - The
storevolume is precious — it holdssithbit.db,credential.key,jwt.key, and the blob directory. Back it up; see Monitoring and backups for what is unrecoverable. Thealias-indexvolume is not precious: it re-syncs from chain history. - Secrets stay out of the compose file.
DEFAULT_KEYPAIR(the mail-grpc fee-payer/signing keypair JSON) comes from./.envnext to the compose file or your secret manager. Thedomain-sithbitpostmaster keypair authorizes domains on-chain — guard it like a wallet.
After the stack is up, work through DNS setup so the world can find your MX, then Monitoring and backups for day-2 operation.