chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
# Stalwart Mail Server v0.16.18
2026-07-01 18:08:31 +00:00
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
Deployment of Stalwart mail server on a PostgreSQL backend (RocksDB was retired in DEV-467/DEV-473; teardown completed in DEV-474).
2026-07-01 18:08:31 +00:00
2026-07-04 13:22:36 +00:00
## Architecture
2026-07-01 18:08:31 +00:00
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
- **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
2026-07-04 13:22:36 +00:00
- **Web UI**: https://mail.basicstack.de
2026-07-01 18:08:31 +00:00
2026-08-02 11:25:41 +00:00
## Network Architecture
Stalwart uses a two-tier load balancing setup:
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
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.
2026-08-02 11:25:41 +00:00
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.
2026-07-04 13:22:36 +00:00
## Files
2026-07-01 18:08:31 +00:00
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
- `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
2026-07-04 13:22:36 +00:00
- `stalwart-admin-credentials-sealed.yaml` - Sealed secret for admin password
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
- `stalwart-postgres-credentials-sealed.yaml` - Sealed secret for Postgres role
2026-07-04 13:22:36 +00:00
- `stalwart-s3-backup-sealed.yaml` - Sealed secret for S3 backup credentials
2026-07-01 18:08:31 +00:00
2026-07-04 13:22:36 +00:00
## Deployment
2026-07-01 18:08:31 +00:00
2026-07-04 13:22:36 +00:00
```bash
# Apply sealed secrets first
kubectl apply -f stalwart-admin-credentials-sealed.yaml
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
kubectl apply -f stalwart-postgres-credentials-sealed.yaml
2026-07-04 13:22:36 +00:00
kubectl apply -f stalwart-s3-backup-sealed.yaml
2026-07-01 18:08:31 +00:00
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
# 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
2026-07-01 18:08:31 +00:00
2026-07-04 13:22:36 +00:00
# Deploy Stalwart
kubectl apply -f stalwart-fresh-deployment.yaml
```
2026-07-01 18:08:31 +00:00
2026-07-04 13:22:36 +00:00
## Initial Admin Login
2026-07-01 18:08:31 +00:00
2026-07-04 13:22:36 +00:00
After deployment, log in at https://mail.basicstack.de with:
- Username: `admin`
- Password: (from stalwart-admin-credentials secret)
2026-07-01 18:08:31 +00:00
2026-07-04 13:22:36 +00:00
## Configuration
2026-07-01 18:08:31 +00:00
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
> **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` .
2026-08-01 09:52:26 +00:00
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
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` ).
2026-07-04 13:46:20 +00:00
2026-07-04 13:41:06 +00:00
Check https://stalw.art/docs/ref/ for configuration possibilities.
2026-07-04 16:11:57 +00:00
The API access via /jmap seems to be too complex for the agent and the configruation has been done manually.
2026-07-04 16:31:09 +00:00
Refer to [manual configuration steps ](manual_config_steps.md )
2026-07-01 18:08:31 +00:00
2026-08-02 11:16:01 +00:00
## 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)
2026-07-04 13:22:36 +00:00
## Ports
2026-08-02 11:16:01 +00:00
- SMTP: 25, 587
- IMAPS: 993 (secure only, port 143 disabled per DEV-359)
2026-07-04 13:22:36 +00:00
- HTTP: 8080 (web UI)
## Storage
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
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)
2026-07-04 13:22:36 +00:00
- User accounts
- Server configuration
chore(stalwart): retire RocksDB PVC + backup — Postgres backend is authoritative (DEV-474)
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>
2026-08-28 15:12:25 +00:00
- 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.
2026-07-04 17:19:55 +00:00
## Certificate Renewal
Refer to [Automatic Renewal TLS Certificate ](CERTIFICATE-RENEWAL.md )