stack.basicstack.de/apps/stalwart
CTO Agent c4551495e0 fix(stalwart): co-locate stalwart-backup pod with stalwart-0 (DEV-468)
The stalwart-backup CronJob has failed nightly since 2026-08-13, all with
FailureTarget=DeadlineExceeded. Root cause: the backup pod had no
scheduling constraint and got placed on a node different from stalwart-0.
The hcloud CSI block volume is RWO and can only be attached to one node,
so the backup pod stayed in ContainerCreating with FailedAttachVolume /
Multi-Attach until the 600s active deadline killed it.

- Add podAffinity requiredDuringScheduling on
  app=stalwart,statefulset.kubernetes.io/pod-name=stalwart-0 with topology
  key kubernetes.io/hostname so the backup pod always lands on the same
  node. Same-node co-location lets both pods share the already-attached
  block volume; the in-container script then scales stalwart-0 down,
  backs up, and scales it back up as before.
- Raise activeDeadlineSeconds from 600s to 1800s as safety headroom
  (successful runs are ~86s; the extra budget covers prune growth).

Verified: manual run of the patched CronJob completed in 86s and wrote
restic snapshot f76f534e (2026-08-15 12:07:30) to
s3://basicstack-backup/stalwart. stalwart-0 is back to Ready.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-15 12:11:26 +00:00
..
CERTIFICATE-RENEWAL.md Implement automatic TLS certificate renewal for Stalwart 2026-07-04 16:48:48 +00:00
CONFIGMAP-HARDENING-TEST-2026-08-01.md Document ConfigMap hardening test results (DEV-431) 2026-08-01 10:17:04 +00:00
ISSUE-2026-07-13-smtp-imap-external-access.md docs(stalwart): Clarify LoadBalancer was working all along via hostPort 2026-07-13 20:12:11 +00:00
manual_config_steps.md apps/stalwart/manual_config_steps.md aktualisiert 2026-07-04 16:39:11 +00:00
README.md Document Hetzner Load Balancer architecture for Stalwart 2026-08-02 11:32:30 +00:00
SETUP_COMPLETE.md Move Application manifests to apps root for proper self-management 2026-07-12 09:27:28 +00:00
STABILITY-CHECK.md Update Stalwart stability check: SMTP transient issue during LoadBalancer transition 2026-08-09 16:51:30 +00:00
STABLE-ROUTING-SOLUTION.md Document stable routing solution for Stalwart Mail 2026-07-22 18:07:38 +00:00
stalwart-admin-credentials-sealed.yaml Fix Stalwart ArgoCD degraded status by ignoring unsealed secrets 2026-07-25 15:15:42 +00:00
stalwart-allow-cluster-ips-job.yaml Complete Stalwart ConfigMap mount hardening: add stalwart.toml copy 2026-08-01 09:52:26 +00:00
stalwart-bootstrap-config.yaml Restore Stalwart bootstrap ConfigMap (non-destructive recovery) 2026-07-26 13:09:20 +00:00
stalwart-config.yaml apps/stalwart/stalwart-config.yaml aktualisiert 2026-08-01 10:45:38 +00:00
stalwart-fresh-deployment.yaml fix(stalwart): co-locate stalwart-backup pod with stalwart-0 (DEV-468) 2026-08-15 12:11:26 +00:00
stalwart-monitoring.yaml Stalwart reliability hardening: fix k3s service networking issues 2026-07-11 11:06:36 +00:00
stalwart-postgres-backup.yaml feat(stalwart): deploy stalwart-postgres StatefulSet + backup (DEV-469) 2026-08-15 12:05:25 +00:00
stalwart-postgres-credentials-sealed.yaml feat(stalwart): deploy stalwart-postgres StatefulSet + backup (DEV-469) 2026-08-15 12:05:25 +00:00
stalwart-postgres.yaml feat(stalwart): deploy stalwart-postgres StatefulSet + backup (DEV-469) 2026-08-15 12:05:25 +00:00
stalwart-s3-backup-sealed.yaml Fix Stalwart ArgoCD degraded status by ignoring unsealed secrets 2026-07-25 15:15:42 +00:00

Stalwart Mail Server v0.16.11

Clean deployment of Stalwart mail server with username/password authentication only.

Architecture

  • Version: v0.16.11
  • Authentication: Username/password only (NO OAuth/OIDC)
  • Configuration: API-based (stored in RocksDB)
  • Storage: Encrypted hcloud-volumes (20Gi)
  • Backup: Daily restic backup to S3 at 3 AM
  • Web UI: https://mail.basicstack.de

Network Architecture

Stalwart uses a two-tier load balancing setup:

  1. Kubernetes LoadBalancer Services: Four separate LoadBalancer services expose SMTP (25, 587, 465) and IMAP (143, 993) ports. 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
  • stalwart-admin-credentials-sealed.yaml - Sealed secret for admin password
  • stalwart-s3-backup-sealed.yaml - Sealed secret for S3 backup credentials

Deployment

# Apply sealed secrets first
kubectl apply -f stalwart-admin-credentials-sealed.yaml
kubectl apply -f stalwart-s3-backup-sealed.yaml

# Create bootstrap config
kubectl create configmap stalwart-bootstrap-config \
  --from-literal=config.json='{"@type":"RocksDb","path":"/var/lib/stalwart"}' \
  -n stalwart

# 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: The configuration is stored in the RocksDB and cannot be overwritten by a configuration file!

All configuration is done via the web UI or API. The bootstrap config only points to the RocksDB database location. NO config.toml files are used.

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

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

Data is stored in /var/lib/stalwart using the RocksDB database format. This includes:

  • Email messages
  • User accounts
  • Server configuration
  • TLS certificates configuration

Certificate Renewal

Refer to Automatic Renewal TLS Certificate