Post-cutover 14-day observation window on DEV-474 closed today with 14/14 healthy days. This removes every RocksDB-era artefact from the manifest and updates the README to reflect the PostgreSQL backend as the default: - Delete PVC `stalwart-data` (was 20Gi hcloud-volumes-encrypted, RWO). - Drop the `fix-permissions` init-container (existed only to chown the RocksDB PVC mount). - Replace the StatefulSet's `data` volume with an `emptyDir` and keep the `/var/lib/stalwart` mount as a plain scratch path (Stalwart's state is now in the `stalwart-postgres` StatefulSet). - Delete the `stalwart-backup` CronJob and its `stalwart-backup` ServiceAccount / Role / RoleBinding — RocksDB snapshotting is obsolete now that state lives in Postgres. Daily backup is via `stalwart-postgres-backup` (restic tag `stalwart-postgres`). - README: update version, backend, storage, backup, files, and deployment sections; correct exposed-ports language to match SMTP 25/587 + IMAPS 993. Executed alongside the merge: - `restic forget --prune` removed all 11 snapshots on tag=stalwart (Job `stalwart-restic-forget-rocksdb` on 2026-08-28 15:09 UTC, log preserved on DEV-474). 16.165 MiB pruned. `restic snapshots --tag stalwart` now returns empty; only the 14 tag=`stalwart-postgres` snapshots remain. - `stalwart-backup` CronJob was pre-suspended in-cluster to prevent tonight's 03:00 UTC fire from re-creating a stalwart-tagged snapshot before this PR merges. Rollback (if needed before merge): revert this commit; the old CronJob would then re-create RocksDB snapshots from live PVC contents. After merge the rollback path is the Postgres `pg_dump` restic chain only. Refs: DEV-467, DEV-473 (cutover), DEV-474 (this observation + teardown), DEV-548 (day-14 execution issue). Co-Authored-By: Paperclip <noreply@paperclip.ing>
150 lines
6.2 KiB
Markdown
150 lines
6.2 KiB
Markdown
# Stalwart Mail Server v0.16.18
|
|
|
|
Deployment of Stalwart mail server on a PostgreSQL backend (RocksDB was retired in DEV-467/DEV-473; teardown completed in DEV-474).
|
|
|
|
## Architecture
|
|
|
|
- **Version**: v0.16.18
|
|
- **Authentication**: OIDC via Pocket-ID (backed by Postgres accounts store)
|
|
- **Configuration + state**: PostgreSQL (`stalwart-postgres` StatefulSet in-namespace)
|
|
- **Storage**: Encrypted hcloud-volumes attached to `stalwart-postgres-0` (10Gi). Stalwart itself uses an emptyDir at `/var/lib/stalwart` — no persistent block volume on `stalwart-0`
|
|
- **Backup**: Daily Postgres logical dump to S3 via `stalwart-postgres-backup` CronJob (restic tag `stalwart-postgres`) at 02:30 UTC
|
|
- **Web UI**: https://mail.basicstack.de
|
|
|
|
## Network Architecture
|
|
|
|
Stalwart uses a two-tier load balancing setup:
|
|
|
|
1. **Kubernetes LoadBalancer Services**: Two LoadBalancer services expose SMTP (25, 587) and IMAPS (993) ports (SMTPS/465 removed per DEV-359 approval, plaintext IMAP/143 disabled). The service type `LoadBalancer` is required because the Hetzner Cloud Controller Manager (CCM) automatically provisions and manages the Hetzner Load Balancer when it detects this service type.
|
|
|
|
2. **Hetzner Load Balancer**: Automatically managed by the Hetzner CCM. The load balancer distributes traffic across:
|
|
- k3s-cp-1 (control plane node)
|
|
- k3s-worker-1 through k3s-worker-5 (worker nodes)
|
|
|
|
### Traffic Flow
|
|
|
|
```
|
|
External Client → Hetzner Load Balancer → NodePort → K8s Service → Stalwart Pod
|
|
```
|
|
|
|
The Hetzner CCM watches for LoadBalancer-type services and automatically:
|
|
- Creates/updates the Hetzner LB configuration
|
|
- Configures health checks
|
|
- Maps service ports to node ports
|
|
- Manages target nodes
|
|
|
|
**Important**: Changing the service type from `LoadBalancer` to `NodePort` would break the automatic Hetzner LB management. The current setup is the correct configuration for our infrastructure.
|
|
|
|
## Files
|
|
|
|
- `stalwart-fresh-deployment.yaml` - Main deployment manifest (StatefulSet, services, ingress)
|
|
- `stalwart-postgres.yaml` - Postgres backend StatefulSet + service (state store)
|
|
- `stalwart-postgres-backup.yaml` - Daily `pg_dump` → restic CronJob (tag `stalwart-postgres`)
|
|
- `stalwart-config.yaml` - Stalwart configmap (`stalwart.toml`)
|
|
- `stalwart-bootstrap-config.yaml` - Bootstrap configmap pointing Stalwart at Postgres
|
|
- `stalwart-admin-credentials-sealed.yaml` - Sealed secret for admin password
|
|
- `stalwart-postgres-credentials-sealed.yaml` - Sealed secret for Postgres role
|
|
- `stalwart-s3-backup-sealed.yaml` - Sealed secret for S3 backup credentials
|
|
|
|
## Deployment
|
|
|
|
```bash
|
|
# Apply sealed secrets first
|
|
kubectl apply -f stalwart-admin-credentials-sealed.yaml
|
|
kubectl apply -f stalwart-postgres-credentials-sealed.yaml
|
|
kubectl apply -f stalwart-s3-backup-sealed.yaml
|
|
|
|
# Configmaps: bootstrap points Stalwart at Postgres; stalwart-config carries stalwart.toml
|
|
kubectl apply -f stalwart-bootstrap-config.yaml
|
|
kubectl apply -f stalwart-config.yaml
|
|
|
|
# Postgres backend + backup
|
|
kubectl apply -f stalwart-postgres.yaml
|
|
kubectl apply -f stalwart-postgres-backup.yaml
|
|
|
|
# Deploy Stalwart
|
|
kubectl apply -f stalwart-fresh-deployment.yaml
|
|
```
|
|
|
|
## Initial Admin Login
|
|
|
|
After deployment, log in at https://mail.basicstack.de with:
|
|
- Username: `admin`
|
|
- Password: (from stalwart-admin-credentials secret)
|
|
|
|
## Configuration
|
|
|
|
> **IMPORTANT:** Runtime configuration and account/queue state now live in **PostgreSQL** (`stalwart-postgres-0`), not RocksDB.
|
|
|
|
The `stalwart-bootstrap-config` configmap points Stalwart at the Postgres store; runtime edits made in the web UI or API are persisted to Postgres. The `stalwart-config` configmap carries the boot-time `stalwart.toml`.
|
|
|
|
The historical RocksDB backend (PVC `stalwart-data`, restic tag `stalwart`, `stalwart-backup` CronJob, and the `fix-permissions` init-container) was retired on 2026-08-28 after 14 healthy post-cutover days (DEV-474). No RocksDB restic chain remains; rollback is now via the Postgres `pg_dump` restic chain (`tag=stalwart-postgres`).
|
|
|
|
Check https://stalw.art/docs/ref/ for configuration possibilities.
|
|
The API access via /jmap seems to be too complex for the agent and the configruation has been done manually.
|
|
|
|
Refer to [manual configuration steps](manual_config_steps.md)
|
|
|
|
## Network Architecture
|
|
|
|
### Load Balancing Setup
|
|
|
|
Stalwart uses a two-tier load balancing architecture:
|
|
|
|
1. **Hetzner Cloud Load Balancer** (External Layer)
|
|
- Provides the public-facing IP for mail services
|
|
- Configured with k3s-cp-1 and all worker nodes as targets
|
|
- Forwards traffic to Kubernetes NodePorts on the cluster nodes
|
|
|
|
2. **Kubernetes LoadBalancer Services** (Internal Layer)
|
|
- Service type: LoadBalancer (managed by k3s ServiceLB)
|
|
- Automatically configured by Hetzner CCM (Cloud Controller Manager)
|
|
- Creates NodePorts that the Hetzner LB targets
|
|
|
|
### Traffic Flow
|
|
|
|
```
|
|
External Mail Client
|
|
↓
|
|
Hetzner Load Balancer (public IP)
|
|
↓
|
|
k3s Node NodePort (automatically assigned)
|
|
↓
|
|
Kubernetes LoadBalancer Service (stalwart-smtp / stalwart-imap)
|
|
↓
|
|
Stalwart Pod
|
|
```
|
|
|
|
### Why LoadBalancer Service Type is Required
|
|
|
|
The Kubernetes services MUST remain type `LoadBalancer` because:
|
|
- The Hetzner CCM automatically manages the Hetzner Load Balancer configuration
|
|
- When a service is type LoadBalancer, the CCM creates/updates the Hetzner LB targets
|
|
- Changing to NodePort would break this automatic management
|
|
- Manual Hetzner LB configuration would be required and error-prone
|
|
|
|
### Services
|
|
|
|
- `stalwart-smtp` (LoadBalancer): Ports 25, 587
|
|
- `stalwart-imap` (LoadBalancer): Port 993
|
|
- `stalwart-http` (ClusterIP): Port 8080 (web UI via Traefik Ingress)
|
|
|
|
## Ports
|
|
|
|
- SMTP: 25, 587
|
|
- IMAPS: 993 (secure only, port 143 disabled per DEV-359)
|
|
- HTTP: 8080 (web UI)
|
|
|
|
## Storage
|
|
|
|
All persistent state lives in **PostgreSQL** on `stalwart-postgres-0` (PVC `postgres-data-stalwart-postgres-0`, 10Gi, `hcloud-volumes-encrypted`). Stalwart connects to Postgres via the bootstrap config and stores:
|
|
- Email messages (blob + metadata)
|
|
- User accounts
|
|
- Server configuration
|
|
- Queue / spool state
|
|
|
|
`/var/lib/stalwart` on `stalwart-0` is an emptyDir mount kept only as a writable path if Stalwart needs one at runtime; no data of value lives there.
|
|
|
|
## Certificate Renewal
|
|
|
|
Refer to [Automatic Renewal TLS Certificate](CERTIFICATE-RENEWAL.md)
|