vaguely official, slightly unfortunate
Fixes the root cause that was silently dropping Stalwart's cert/setting writes, completes the public HTTPS endpoints, and captures the debugging knowledge. - docker-compose.yml: gate the ts-stalwart healthcheck on Postgres reachability (nc -z the-record-prod:5432) in addition to tailscaled health. Stalwart's depends_on: service_healthy can no longer release it into the window where the tailnet route to Postgres isn't up yet — which was failing table init and losing in-flight cert writes (-> rcgen). - caddy/caddy.json + README: add the :443 SNI fan-out. mta-sts / autoconfig / autodiscover pass through to stalwart:443 (Stalwart terminates TLS with its wildcard cert; no proxy_protocol on :443). All other SNIs go to the box's web Caddy on :8443 (https_port 8443). L7 reverse_proxy is impossible here: CAA pins issuance to Stalwart's ACME account, so Caddy can't obtain its own cert for these names. - acl-snippet.hujson: grant tcp:443 on reverse-proxy -> stalwart for the SNI pass-through. - config/config.json: track the v0.16 bootstrap (commit-safe; the DB secret is an EnvironmentVariable reference, not inline). - LESSONS.md: symptom -> cause -> fix notes (PG race, DNS-01/Spaceship dead key, auto-ban vs PROXY protocol, wildcard-requires-DNS-01, SNI pass-through, ephemeral sidecar IP, LE rate-limit checks). - .gitignore: exclude _backup/ and _validate/ (DB dumps + an inline-secret config) and editor swap files. NEVER commit those. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| caddy | ||
| config | ||
| .env.example | ||
| .gitignore | ||
| acl-snippet.hujson | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| LESSONS.md | ||
| README.md | ||
tailwart
Tailscale × Stalwart. A mailbox with no WAN presence, fronted by a layer-4 proxy that can live on another machine entirely.
A deliberately over-engineered playground: Stalwart
mail server wired into Postgres + Redis + Garage S3 at once, deployed as a
Tailscale sidecar, with a separate caddy-l4 edge that pipes the raw mail ports
over the tailnet. For infinidim.net.
See CLAUDE.md for the architecture and the gotchas.
Layout
tailwart/
├── docker-compose.yml # the mailbox: ts-stalwart sidecar + stalwart
├── config/config.toml # Stalwart config — PG + Redis + S3 wiring (strawman)
├── caddy/ # the edge: custom Caddy (caddy-l4) layer-4 mail proxy
│ ├── Dockerfile # pulls prebuilt caddy-l4 binary (caddyserver.com, no local build)
│ ├── caddy.json # :25/465/587/143/993 → stalwart over the tailnet
│ ├── docker-compose.yml # deploy on any public-IP, tailnet, tag:reverse-proxy host
│ └── README.md
├── acl-snippet.hujson # tag:stalwart owner + grants to merge into your policy
├── .env.example # operator surface — copy to .env
└── .gitignore
Quickstart
cp .env.example .env && $EDITOR .env # fill secrets (see CLAUDE.md prereqs)
# 1. create the stalwart role/db in shared Postgres + the Garage bucket
# (one-off; see CLAUDE.md "Prerequisites")
# 2. admin console: assign tag:stalwart to the OAuth client + paste acl-snippet
# 3. bring up the mailbox (tailnet-only)
docker compose up -d
# 4. bring up the edge (binds public mail ports; can be a different host)
cd caddy && docker compose up -d --build
Then point infinidim.net's MX at the edge host, add SPF/DKIM/DMARC, and finish
configuration in Stalwart's web admin (mail.infinidim.net).
Status
Scaffold / strawman. The Stalwart config.toml keys need verifying against a
pinned image version before first real boot — treat it as a starting shape, not
a turnkey config.