Deployment
Running ScaiSend in production. Three services, three datastores, and a handful of DNS records. This page covers the moving parts; see the standalone deployment guide shipped with the repository (docs/deployment-standalone.md) for copy-pasteable install steps.
What you're running#
| Service | Command | Role |
|---|---|---|
| API | scaisend serve |
FastAPI; handles /v3/*, /api/admin/*, /t/*, /i/*, /webhooks/scaikey |
| Worker | scaisend worker |
arq; renders templates, delivers webhooks, aggregates stats |
| SMTP | scaisend smtp |
aiosmtpd; direct outbound delivery + inbound DSN/FBL |
All three can run on a single host for small deployments, or split across hosts for scale. They're stateless — all state is in MySQL, Redis, or S3 — so you can add replicas freely.
Dependencies#
| System | Purpose | Version |
|---|---|---|
| Python | Runtime | 3.11+ |
| MySQL | Durable state (27 tables) | 8.0+ |
| Redis | Queues, cache, rate-limit counters | 7+ |
| S3-compatible | Attachments, images | Garage, MinIO, or real S3 |
| ScaiKey | Identity, OAuth, webhook-based sync | Accessible over HTTPS |
A production deployment also needs:
- Public HTTPS for the API. Typically via a reverse proxy (Nginx, Caddy, Traefik) terminating TLS.
- Public IP on port 25 for the SMTP inbound server (DSN and FBL reception). Reachable from the internet.
- Outbound port 25 unrestricted (or restricted to your SMTP service's egress IPs). Many cloud providers block outbound 25 by default; contact support to unblock.
- Publishing DNS for each sender domain (DKIM, SPF, DMARC) and for the ScaiSend instance itself (A/AAAA, reverse DNS).
Configuration#
All configuration is via environment variables. Minimal set:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | |
See .env.example in the repository for every variable.
Self-registration with ScaiKey#
If your ScaiKey deployment allows self-registration, let ScaiSend bootstrap itself:
1 2 3 | |
This:
- Calls ScaiKey's
/v1/clients/registerwith ScaiSend's metadata. - Waits for an admin to approve the registration (or auto-approves if ScaiKey is configured for that).
- Writes the returned client credentials to
.env.
Check status with:
1 | |
For scoped registration (one tenant or partner only):
1 2 | |
Initial sync#
After registration, mirror the current state from ScaiKey into your ScaiSend DB:
1 | |
--create-roles creates the default admin, developer, and viewer roles for every tenant. --full mirrors every partner, tenant, user, and group.
Re-run after prolonged ScaiKey outages, but it's unnecessary during normal operation — webhooks keep everything in sync in real time.
Database migrations#
Alembic handles schema. On first install:
1 | |
On upgrade:
1 2 3 4 5 | |
Migrations are forward-only. There's no automated downgrade path; if you need to roll back, restore from backup.
Running the services#
Systemd units#
A typical setup runs each service as a separate systemd unit:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Replicate for scaisend-worker and scaisend-smtp. Put the API behind a reverse proxy terminating TLS on 443.
Docker / Kubernetes#
ScaiSend runs cleanly in containers. Run three separate deployments (one per service), each with its own image tag or process flag. Scale independently.
Key things to remember:
- The SMTP service needs
hostNetwork: true(Kubernetes) ornetwork_mode: host(Docker Compose) if it's listening on port 25 — running it in a NAT-ed container means recipient MXs see a private IP in the reverse DNS check. - All three services share the
.env— the same MySQL, Redis, S3 credentials. - Only the API service needs inbound HTTP exposure.
DNS for the ScaiSend instance itself#
Separate from per-sender-domain DNS, the ScaiSend instance hosts some records:
| Record | Purpose |
|---|---|
A / AAAA for the API hostname |
HTTPS traffic |
A / AAAA for the SMTP inbound hostname |
DSN / FBL reception (can be same as API host) |
PTR for the SMTP outbound IPs |
Reverse DNS; required by most receivers for deliverability |
MX for the abuse address domain |
If you're receiving FBLs, the MX must point at the SMTP inbound server |
PTR records are usually managed by your IP provider. Make sure every outbound IP has a PTR pointing at a hostname that resolves back to that IP (forward-confirmed reverse DNS, FCrDNS).
Health checks#
| Endpoint | Purpose |
|---|---|
GET /health |
Liveness — returns 200 if the API process is up |
GET /ready |
Readiness — returns 200 if DB is reachable, 503 otherwise |
Point your load-balancer health checks at /ready. Kubernetes livenessProbe at /health; readinessProbe at /ready.
The worker and SMTP services don't expose HTTP; rely on systemd / Kubernetes to restart them if they die.
Backup and recovery#
State in order of criticality:
| Store | Backup strategy |
|---|---|
| MySQL | Regular mysqldump or managed-DB snapshots. Daily minimum. |
| S3 | Versioning + replication. Attachments aren't regenerable from anywhere else. |
| Redis | No backup. Queued-but-unsent messages are lost on wipe; persistently stored messages survive. |
On restore:
- Restore MySQL to the desired point in time.
- Restore S3 attachments (or rely on durability).
- Let Redis rebuild naturally. Messages in the
QUEUEDstate in MySQL will be picked up by the worker on next poll.
Scaling#
| Bottleneck | How to scale |
|---|---|
| API throughput | Add more API replicas behind the load balancer |
| Template rendering | Add more worker replicas |
| SMTP delivery concurrency | Increase SMTP_POOL_SIZE_PER_DOMAIN; add more SMTP replicas |
| Inbound DSN/FBL volume | One SMTP replica is usually plenty; only add if you're processing millions of DSNs/day |
| MySQL write load | Vertical scale MySQL; tune indexes; consider a read-replica for /v3/messages reads |
| Redis memory | Vertical scale; queue depth is usually the bottleneck, not key count |
Related#
- Health and Monitoring — what to observe.
- Troubleshooting — common failure modes.
- Sender Domains — per-domain DNS setup.