Self-hosted Docker Compose stack for an Aegis email server, fronted by
Cloudflare Tunnel + Workers. One docker compose up brings the relay
and gateway online; the admin / web SPAs are deployed separately as
Cloudflare Workers from their own repos.
v0.3-alpha — reference deployment. Production hardening (HSM-backed tokens, secrets management, multi-instance HA) is out of scope for this repo. Treat it as a reproducible starting point, not a turnkey appliance.
| Service | Container | Public access | Purpose |
|---|---|---|---|
| relay | aegis-relay |
via cloudflared at relay.<your-domain> |
Encrypted-envelope store + identity directory + admin API |
| gateway | aegis-gateway |
host ports 25, 993 (direct) | Legacy SMTP/IMAP boundary for interop with classic email |
| cloudflared | aegis-cloudflared |
outbound only | Tunnel daemon — connects this host to Cloudflare's edge |
| (separate) aegis-admin SPA | CF Worker | admin.<your-domain> |
Operator console (deploy via aegis-admin/wrangler.jsonc) |
| (separate) aegis-web SPA | CF Worker | auth.<your-domain> |
End-user identity / inbox (deploy via aegis-web/wrangler.jsonc) |
Why the SPAs aren't in this stack: they're already publicly-trusted CF
Workers via wrangler deploy, with edge caching, CF Access policies,
and zero-config TLS. Bundling them into a host-side container would
just be re-adding the nginx-and-certbot pain we left behind.
- Docker 24+ with Compose v2
- A Cloudflare account with the domain you want to host on (free tier is fine)
- A Cloudflare Tunnel created in
Zero Trust → Networks → Tunnels, with the connector token in hand - The sibling repos cloned next to this one:
parent/ ├── aegis-relay/ ├── aegis-gateway/ ├── aegis-admin/ ← deployed separately as a CF Worker ├── aegis-web/ ← deployed separately as a CF Worker └── aegis-deploy/ ← you are here - Public IPv4 with port 25 (and 993) open only if you want legacy email federation. CF Tunnel can't proxy SMTP — port 25 has to be reachable from other mail servers directly. PQ-native-only deployments can leave the gateway disabled.
In Cloudflare → Zero Trust → Networks → Tunnels:
-
Create a tunnel named
aegis-<your-org>(Cloudflared connector) -
On "Install and run a connector", copy the token (e.g.
eyJhIjoiOWY...). Don't run the install command —docker compose upruns the connector for you. -
On the Public Hostnames tab, add:
Subdomain Domain Service relayyour-domain.comhttp://relay:8787gateway-admin(optional)your-domain.comhttp://gateway:8788These hostnames refer to the docker-compose service names, not host IPs — the cloudflared container can dial them on the internal compose network.
cd aegis-deploy
./scripts/bootstrap.sh
docker compose up -dThe bootstrap script prompts for hostname, primary domain, and your
tunnel token, then writes .env with fresh admin tokens (mode 600).
Confirm the tunnel registered:
docker compose logs cloudflared | grep "Registered tunnel connection"cd ../aegis-admin
npm install
wrangler login
npm run deployIn the CF dashboard → Workers & Pages → aegis-admin → Settings →
Custom Domains, attach admin.<your-domain>.
(Repeat for aegis-web if you want the end-user identity SPA at
auth.<your-domain>.)
Open https://admin.<your-domain> (sign in via CF Access if you've
enabled it). Connect using:
- Relay admin URL:
https://relay.<your-domain> - Relay admin token: the value from
.env'sAEGIS_RELAY_ADMIN_TOKEN
Go to Domains → Claim domain and enter your domain. The UI shows
the DNS TXT record to add (_aegis-verify.<domain>). Add it at your DNS
provider, then click Verify.
CLI alternative for a headless workflow:
./scripts/claim-domain.sh example.comOnce the domain is verified, clients can auto-discover this relay if
you publish a DNS SRV record. See docs/dns-discovery.md
for the SRV + .well-known walkthrough.
In the admin UI: Users → Provision a user → enter a local part
(alice) under your verified domain.
All configuration lives in .env. See .env.example for the full list.
| Variable | Purpose |
|---|---|
AEGIS_PUBLIC_HOSTNAME |
Hostname this relay answers on (matches the CF Tunnel ingress) |
AEGIS_PRIMARY_DOMAIN |
Domain you'll claim (informational only — the actual claim happens via the admin UI) |
AEGIS_RELAY_PUBLIC_URL |
Advertised in /.well-known/aegis-config |
CLOUDFLARED_TOKEN |
Tunnel token from CF dashboard |
AEGIS_RELAY_ADMIN_TOKEN |
Bearer token gating /admin/* on the relay |
AEGIS_GATEWAY_ADMIN_TOKEN |
Bearer token gating /admin/* on the gateway |
AEGIS_GATEWAY_PASSPHRASE |
Long-term secret deriving the gateway demo identity |
SMTP_PORT / IMAP_PORT |
Public ports for legacy email federation |
git -C ../aegis-relay pull
git -C ../aegis-gateway pull
git pull
docker compose build --pull
docker compose up -dThe aegis-admin and aegis-web SPAs update via their own
npm run deploy flows.
Storage lives in the relay-data named volume; pulling a new image
does not touch your envelopes, identities, or domain claims.
The relay's complete state is in the relay-data volume:
aegis-relay.db— SQLite database (envelopes, identities, domains, users)aegis-relay-runtime.json— runtime config (tokens, retention policy)aegis-relay-audit.jsonl— append-only audit log
docker run --rm -v aegis-deploy_relay-data:/data -v "$PWD":/backup alpine \
tar czf /backup/aegis-relay-$(date +%F).tar.gz -C /data .- Tunnel won't register —
docker compose logs cloudflaredwill show "Unauthorized" if the token is wrong. Pull a fresh token from the CF dashboard and update.env. The token is regenerable. curl https://relay.<your-domain>/healthzreturns 502 — tunnel ingress likely points at the wrong service name or port. The Service field must behttp://relay:8787(lowercase, the docker-compose service name, not localhost).curl /.well-known/aegis-configreturns 404 — domain not verified yet. Complete the Claim → Verify flow first.- Domain verification fails — DNS TTL: wait a few minutes after
publishing the TXT record. Check from anywhere:
dig TXT _aegis-verify.<domain> @1.1.1.1 docker compose buildis slow — first build compiles Rust from scratch; subsequent builds are cached. If you changeCargo.toml, expect a full rebuild.
- The admin token is a bearer token over HTTPS. Treat it like a root password. Rotate it via the admin UI ("Add token" → revoke the old).
- The CF Tunnel token is also a long-term credential. Rotate it from the CF dashboard if it's ever exposed.
- The gateway admin route (
gateway-admin.<your-domain>) sits behind the same tunnel as the relay. Add a CF Access policy restricting it to your operator email — the gateway's bearer-token check is the second line of defense, not the first. - The relay enforces self-signed identity documents (Ed25519 + ML-DSA-65). It does not implement key continuity yet — a key rotation appears to clients as a fresh identity (RFC-0002 §11).
- Federation is not yet implemented — this relay only delivers to identities published on itself or via the gateway's SMTP path.