From 509bbebe103900c1a0c15e9fe0801429b3007448 Mon Sep 17 00:00:00 2001 From: CTO Agent Date: Sat, 4 Jul 2026 10:17:13 +0000 Subject: [PATCH] feat(stalwart): Fresh deployment with basic auth - Deployed Stalwart v0.16.11 in stalwart namespace - Configured encrypted hcloud storage (20Gi PVC) - Set up LoadBalancer services for SMTP/IMAP - Configured Ingress with TLS for mail.basicstack.de - Daily backup CronJob with S3/restic - Bootstrap mode requires web UI setup completion Refs: DEV-206 Co-Authored-By: Paperclip --- apps/stalwart/stalwart-fresh-deployment.yaml | 324 +++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 apps/stalwart/stalwart-fresh-deployment.yaml diff --git a/apps/stalwart/stalwart-fresh-deployment.yaml b/apps/stalwart/stalwart-fresh-deployment.yaml new file mode 100644 index 0000000..8919c9a --- /dev/null +++ b/apps/stalwart/stalwart-fresh-deployment.yaml @@ -0,0 +1,324 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: stalwart +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: stalwart-data + namespace: stalwart +spec: + accessModes: + - ReadWriteOnce + storageClassName: hcloud-volumes-encrypted + resources: + requests: + storage: 20Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: stalwart-config + namespace: stalwart +data: + config.toml: | + [server] + hostname = "mail.basicstack.de" + + [server.listener.smtp] + bind = ["0.0.0.0:25"] + protocol = "smtp" + + [server.listener.submission] + bind = ["0.0.0.0:587"] + protocol = "smtp" + tls.implicit = false + + [server.listener.submissions] + bind = ["0.0.0.0:465"] + protocol = "smtp" + tls.implicit = true + + [server.listener.imap] + bind = ["0.0.0.0:143"] + protocol = "imap" + tls.implicit = false + + [server.listener.imaps] + bind = ["0.0.0.0:993"] + protocol = "imap" + tls.implicit = true + + [server.listener.http] + bind = ["0.0.0.0:8080"] + protocol = "http" + + [storage] + data = "rocksdb" + blob = "rocksdb" + fts = "rocksdb" + lookup = "rocksdb" + + [store.rocksdb] + type = "rocksdb" + path = "/var/lib/stalwart" + compression = "lz4" + + [directory.internal] + type = "internal" + store = "rocksdb" + + [session.auth] + directory = "internal" + + [jmap.protocol] + set.max-objects = 100000 +--- +apiVersion: v1 +kind: Service +metadata: + name: stalwart-smtp + namespace: stalwart +spec: + type: LoadBalancer + selector: + app: stalwart + ports: + - name: smtp + port: 25 + targetPort: 25 + protocol: TCP + - name: submission + port: 587 + targetPort: 587 + protocol: TCP + - name: submissions + port: 465 + targetPort: 465 + protocol: TCP +--- +apiVersion: v1 +kind: Service +metadata: + name: stalwart-imap + namespace: stalwart +spec: + type: LoadBalancer + selector: + app: stalwart + ports: + - name: imap + port: 143 + targetPort: 143 + protocol: TCP + - name: imaps + port: 993 + targetPort: 993 + protocol: TCP +--- +apiVersion: v1 +kind: Service +metadata: + name: stalwart-http + namespace: stalwart +spec: + type: ClusterIP + selector: + app: stalwart + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: stalwart + namespace: stalwart +spec: + serviceName: stalwart-http + replicas: 1 + selector: + matchLabels: + app: stalwart + template: + metadata: + labels: + app: stalwart + spec: + initContainers: + - name: fix-permissions + image: busybox:latest + command: ["sh", "-c", "chown -R 1000:1000 /var/lib/stalwart && chmod -R 755 /var/lib/stalwart"] + volumeMounts: + - name: data + mountPath: /var/lib/stalwart + containers: + - name: stalwart + image: stalwartlabs/stalwart:v0.16.11 + ports: + - containerPort: 25 + name: smtp + - containerPort: 587 + name: submission + - containerPort: 465 + name: submissions + - containerPort: 143 + name: imap + - containerPort: 993 + name: imaps + - containerPort: 8080 + name: http + env: + - name: STALWART_RECOVERY_ADMIN + valueFrom: + secretKeyRef: + name: stalwart-admin-credentials + key: admin-password + optional: false + volumeMounts: + - name: data + mountPath: /var/lib/stalwart + - name: config + mountPath: /opt/stalwart-mail/etc/config.toml + subPath: config.toml + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "2Gi" + cpu: "2000m" + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumes: + - name: data + persistentVolumeClaim: + claimName: stalwart-data + - name: config + configMap: + name: stalwart-config +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: stalwart-web + namespace: stalwart + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + tls: + - hosts: + - mail.basicstack.de + secretName: stalwart-tls + rules: + - host: mail.basicstack.de + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: stalwart-http + port: + number: 8080 +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: stalwart-backup + namespace: stalwart +spec: + schedule: "0 3 * * *" # 3 AM daily + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + template: + metadata: + labels: + app: stalwart-backup + spec: + restartPolicy: OnFailure + containers: + - name: backup + image: restic/restic:latest + command: + - /bin/sh + - -c + - | + set -e + echo "Starting Stalwart backup at $(date)" + + # Initialize restic repo if needed + restic snapshots || restic init + + # Backup the data directory + restic backup /var/lib/stalwart \ + --tag stalwart \ + --tag daily \ + --host stalwart-k8s + + # Prune old backups (keep 7 daily, 4 weekly, 6 monthly) + restic forget \ + --keep-daily 7 \ + --keep-weekly 4 \ + --keep-monthly 6 \ + --prune + + echo "Backup completed successfully at $(date)" + env: + - name: RESTIC_REPOSITORY + value: "s3:$(S3_ENDPOINT)/$(S3_BUCKET)/stalwart" + - name: RESTIC_PASSWORD + valueFrom: + secretKeyRef: + name: stalwart-s3-backup + key: restic-password + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: stalwart-s3-backup + key: access-key + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: stalwart-s3-backup + key: secret-key + - name: S3_ENDPOINT + valueFrom: + secretKeyRef: + name: stalwart-s3-backup + key: endpoint + - name: S3_BUCKET + valueFrom: + secretKeyRef: + name: stalwart-s3-backup + key: bucket + volumeMounts: + - name: data + mountPath: /var/lib/stalwart + readOnly: true + volumes: + - name: data + persistentVolumeClaim: + claimName: stalwart-data