stack.basicstack.de/apps/stalwart/stalwart-fresh-deployment.yaml

234 lines
6 KiB
YAML
Raw Normal View History

---
apiVersion: v1
kind: Namespace
metadata:
name: stalwart
---
apiVersion: v1
kind: Service
metadata:
name: stalwart-smtp
namespace: stalwart
spec:
type: LoadBalancer
externalTrafficPolicy: Cluster
selector:
app: stalwart
ports:
- name: smtp
port: 25
targetPort: 25
protocol: TCP
- name: submission
port: 587
targetPort: 587
protocol: TCP
# Port 465 (SMTPS) removed per DEV-359 approval - enforce secure protocols
---
apiVersion: v1
kind: Service
metadata:
name: stalwart-imap
namespace: stalwart
spec:
type: LoadBalancer
externalTrafficPolicy: Cluster
selector:
app: stalwart
ports:
# Port 143 (IMAP) removed per DEV-359 approval - enforce secure protocols
- 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
annotations:
# Automatically restart this StatefulSet when the TLS certificate secret is updated
# This ensures the pod reloads new certificates after cert-manager renews them
secret.reloader.stakater.com/reload: "stalwart-tls"
spec:
serviceName: stalwart-http
replicas: 1
selector:
matchLabels:
app: stalwart
template:
metadata:
labels:
app: stalwart
spec:
initContainers:
# Copy ConfigMap files to emptyDir to avoid subPath mounting race
- name: copy-config
image: busybox:latest
command:
- sh
- -c
- |
set -e
echo "Copying bootstrap config..."
cp /tmp/bootstrap-config/* /etc/stalwart/
echo "Copying stalwart.toml..."
cp /tmp/stalwart-config/stalwart.toml /etc/stalwart/stalwart.toml
echo "Config files copied successfully:"
ls -la /etc/stalwart/
volumeMounts:
- name: bootstrap-config-source
mountPath: /tmp/bootstrap-config
readOnly: true
- name: stalwart-config-source
mountPath: /tmp/stalwart-config
readOnly: true
- name: config
mountPath: /etc/stalwart
containers:
- name: stalwart
image: stalwartlabs/stalwart:v0.16.18
ports:
- containerPort: 25
name: smtp
- containerPort: 587
name: submission
# Port 465 (SMTPS) removed - enforce STARTTLS on 587
# Port 143 (IMAP) removed - enforce TLS on 993
- containerPort: 993
name: imaps
- containerPort: 8080
name: http
env:
#- name: STALWART_RECOVERY_ADMIN
# valueFrom:
# secretKeyRef:
# name: stalwart-admin-credentials
# key: admin-password
# optional: false
- name: TLS_CERTIFICATE
value: "/etc/stalwart/certs/tls.crt"
- name: TLS_PRIVATE_KEY
value: "/etc/stalwart/certs/tls.key"
# DEV-476: bootstrap config.json resolves authSecret via this env var.
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: stalwart-postgres-credentials
key: POSTGRES_PASSWORD
volumeMounts:
- name: data
mountPath: /var/lib/stalwart
- name: config
mountPath: /etc/stalwart
- name: tls-certs
mountPath: /etc/stalwart/certs
readOnly: true
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
# DEV-474: /var/lib/stalwart is no longer RocksDB storage (state now lives in
# the stalwart-postgres StatefulSet). Kept as emptyDir so stalwart still has
# a writable path if it needs one at runtime.
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "2000m"
startupProbe:
exec:
command:
- curl
- -f
- --max-time
- "3"
- http://localhost:8080/healthz/live
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
exec:
command:
- curl
- -f
- --max-time
- "3"
- http://localhost:8080/healthz/live
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- curl
- -f
- --max-time
- "3"
- http://localhost:8080/healthz/ready
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumes:
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
# DEV-474: was persistentVolumeClaim.claimName=stalwart-data (RocksDB).
# Postgres-backed now; replaced with emptyDir.
- name: data
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
emptyDir: {}
# emptyDir for config files (populated by copy-config init-container)
- name: config
emptyDir: {}
# ConfigMap sources for init-container (no longer mounted directly via subPath)
- name: bootstrap-config-source
configMap:
name: stalwart-bootstrap-config
- name: stalwart-config-source
configMap:
name: stalwart-config
- name: tls-certs
secret:
secretName: stalwart-tls
defaultMode: 0444
---
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