Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Running a mail server

A SithBit deployment is at most six services, all built from this workspace:

ServiceBinaryRoleNeeded when
sithbitdmail-spoolerSMTP MX + submission, IMAP, POP, and the spooler workers in one processalways
account-apiaccount-apiwallet-challenge login → JWT; mail passwords, timezone, DND schedulesusers manage their accounts
mail-grpcmail-grpcgRPC gateway to the Solana programs (postage checks, SendMail, aliases)mail should reach the chain
domain-sithbitdomain-sithbitDNS-based domain verification and on-chain domain authorizationyou operate the domain registry
sithbit-ipfsdsithbit-ipfsdthe self-hosted IPFS node as a standalone daemon: HTTP pin API + optional public swarma fleet shares one node via [ipfs] kind = "remote" (a single sithbitd can embed the node instead)
sithbit-gatewaysithbit-gatewayread-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, or pop-server binaries 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 exec into a running service is impossible; use docker logs, docker cp, and the monitoring surfaces instead. When copying a live SQLite database out with docker cp, take the -wal and -shm sidecar 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: host deliberately: a loopback-bound surfpool is not reachable through Docker’s host-gateway from a bridge network, so mail-grpc shares the host network and serves on 127.0.0.1:50051 exactly like a native run.
  • Its signing keypair comes from DEFAULT_KEYPAIR in ./.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.toml and account_api/account_api.toml; containers find the file via SITHBITD_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.0 in 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 /certs directory. 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 the aws/azure store and work through the Scaling out checklist.
  • The store volume is precious — it holds sithbit.db, credential.key, jwt.key, and the blob directory. Back it up; see Monitoring and backups for what is unrecoverable. The alias-index volume 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 ./.env next to the compose file or your secret manager. The domain-sithbit postmaster 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.