From 9b0c56f720162bf6a6d35a937ee4756ac334c409 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Sun, 15 Feb 2026 18:30:39 +0000 Subject: [PATCH] mailu app initial attempt --- mailu/configmap.yaml | 30 +++++++++ mailu/deployment-admin.yaml | 103 ++++++++++++++++++++++++++++ mailu/deployment-dovecot.yaml | 70 +++++++++++++++++++ mailu/deployment-front.yaml | 70 +++++++++++++++++++ mailu/deployment-postfix.yaml | 60 +++++++++++++++++ mailu/deployment-redis.yaml | 56 ++++++++++++++++ mailu/deployment-rspamd.yaml | 45 +++++++++++++ mailu/deployment-unbound.yaml | 49 ++++++++++++++ mailu/deployment-webmail.yaml | 61 +++++++++++++++++ mailu/ingress.yaml | 42 ++++++++++++ mailu/kustomization.yaml | 25 +++++++ mailu/manifest.yaml | 60 +++++++++++++++++ mailu/namespace.yaml | 4 ++ mailu/pvc.yaml | 11 +++ mailu/service-redis.yaml | 14 ++++ mailu/service-unbound.yaml | 19 ++++++ mailu/service.yaml | 123 ++++++++++++++++++++++++++++++++++ 17 files changed, 842 insertions(+) create mode 100644 mailu/configmap.yaml create mode 100644 mailu/deployment-admin.yaml create mode 100644 mailu/deployment-dovecot.yaml create mode 100644 mailu/deployment-front.yaml create mode 100644 mailu/deployment-postfix.yaml create mode 100644 mailu/deployment-redis.yaml create mode 100644 mailu/deployment-rspamd.yaml create mode 100644 mailu/deployment-unbound.yaml create mode 100644 mailu/deployment-webmail.yaml create mode 100644 mailu/ingress.yaml create mode 100644 mailu/kustomization.yaml create mode 100644 mailu/manifest.yaml create mode 100644 mailu/namespace.yaml create mode 100644 mailu/pvc.yaml create mode 100644 mailu/service-redis.yaml create mode 100644 mailu/service-unbound.yaml create mode 100644 mailu/service.yaml diff --git a/mailu/configmap.yaml b/mailu/configmap.yaml new file mode 100644 index 0000000..e413dfc --- /dev/null +++ b/mailu/configmap.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mailu-config + namespace: {{ .namespace }} +data: + DOMAIN: "{{ .domain }}" + HOSTNAMES: "{{ .hostname }}" + POSTMASTER: "admin" + TZ: "{{ .timezone }}" + TLS_FLAVOR: "cert" + MESSAGE_SIZE_LIMIT: "50000000" + MESSAGE_RATELIMIT: "200/day" + RELAYNETS: "" + RELAYHOST: "{{ .relayHost }}" + RELAYPORT: "{{ .relayPort }}" + FETCHMAIL_ENABLED: "false" + RECIPIENT_DELIMITER: "+" + DMARC_RUA: "admin" + DMARC_RUF: "admin" + WELCOME: "false" + WELCOME_SUBJECT: "Welcome to your new email account" + WELCOME_BODY: "Welcome! You can now use your email account." + ADMIN: "true" + WEB_ADMIN: "/admin" + WEB_WEBMAIL: "/webmail" + WEBMAIL: "roundcube" + SITENAME: "Mailu" + WEBSITE: "https://{{ .hostname }}" + LOG_LEVEL: "{{ .logLevel }}" diff --git a/mailu/deployment-admin.yaml b/mailu/deployment-admin.yaml new file mode 100644 index 0000000..5923db7 --- /dev/null +++ b/mailu/deployment-admin.yaml @@ -0,0 +1,103 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: admin + namespace: {{ .namespace }} +spec: + replicas: 1 + selector: + matchLabels: + component: admin + template: + metadata: + labels: + component: admin + spec: + dnsPolicy: "None" + dnsConfig: + nameservers: + - {{ .unbound.ip }} + searches: + - {{ .namespace }}.svc.cluster.local + - svc.cluster.local + - cluster.local + options: + - name: ndots + value: "5" + initContainers: + - name: fix-permissions + image: busybox:latest + command: ['sh', '-c', 'chown -R 999:999 /data /dkim'] + volumeMounts: + - name: data + subPath: admin + mountPath: /data + - name: data + subPath: dkim + mountPath: /dkim + containers: + - name: admin + image: {{ .images.admin }} + imagePullPolicy: IfNotPresent + securityContext: + capabilities: + add: + - SYS_CHROOT + - CHOWN + - SETGID + - SETUID + env: + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: mailu-secrets + key: secretKey + - name: REDIS_ADDRESS + value: "{{ .redis.host }}" + - name: I_KNOW_MY_SETUP_DOESNT_FIT_REQUIREMENTS_AND_WONT_FILE_ISSUES_WITHOUT_PATCHES + value: "true" + - name: INITIAL_ADMIN_ACCOUNT + value: "{{ .initialAccount.username }}" + - name: INITIAL_ADMIN_DOMAIN + value: "{{ .initialAccount.domain }}" + - name: INITIAL_ADMIN_PW + valueFrom: + secretKeyRef: + name: mailu-secrets + key: initialAccountPassword + envFrom: + - configMapRef: + name: mailu-config + ports: + - name: http + containerPort: 8080 + volumeMounts: + - name: data + subPath: admin + mountPath: /data + - name: data + subPath: dkim + mountPath: /dkim + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /ping + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /ping + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: data + persistentVolumeClaim: + claimName: mailu-storage diff --git a/mailu/deployment-dovecot.yaml b/mailu/deployment-dovecot.yaml new file mode 100644 index 0000000..bc7a29d --- /dev/null +++ b/mailu/deployment-dovecot.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dovecot + namespace: {{ .namespace }} +spec: + replicas: 1 + selector: + matchLabels: + component: dovecot + template: + metadata: + labels: + component: dovecot + spec: + initContainers: + - name: fix-permissions + image: busybox:latest + command: ['sh', '-c', 'chown -R 999:999 /data /mail'] + volumeMounts: + - name: data + subPath: mail + mountPath: /mail + - name: data + subPath: dovecot + mountPath: /data + containers: + - name: dovecot + image: {{ .images.dovecot }} + imagePullPolicy: IfNotPresent + securityContext: + capabilities: + add: + - SYS_CHROOT + - CHOWN + - SETGID + - SETUID + envFrom: + - configMapRef: + name: mailu-config + ports: + - name: imap + containerPort: 143 + - name: imaps + containerPort: 993 + - name: pop3 + containerPort: 110 + - name: pop3s + containerPort: 995 + - name: sieve + containerPort: 4190 + - name: auth + containerPort: 2102 + - name: lmtp + containerPort: 2525 + volumeMounts: + - name: data + subPath: mail + mountPath: /mail + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "2000m" + volumes: + - name: data + persistentVolumeClaim: + claimName: mailu-storage diff --git a/mailu/deployment-front.yaml b/mailu/deployment-front.yaml new file mode 100644 index 0000000..4745aa7 --- /dev/null +++ b/mailu/deployment-front.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: front + namespace: {{ .namespace }} +spec: + replicas: 1 + selector: + matchLabels: + component: front + template: + metadata: + labels: + component: front + spec: + containers: + - name: front + image: {{ .images.front }} + imagePullPolicy: IfNotPresent + securityContext: + capabilities: + add: + - SYS_CHROOT + - CHOWN + - SETGID + - SETUID + - NET_BIND_SERVICE + env: + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: mailu-secrets + key: secretKey + envFrom: + - configMapRef: + name: mailu-config + ports: + - name: http + containerPort: 80 + - name: https + containerPort: 443 + - name: smtp + containerPort: 25 + - name: smtps + containerPort: 465 + - name: submission + containerPort: 587 + - name: imap + containerPort: 143 + - name: imaps + containerPort: 993 + - name: pop3 + containerPort: 110 + - name: pop3s + containerPort: 995 + volumeMounts: + - name: certs + mountPath: /certs + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + volumes: + - name: certs + secret: + secretName: {{ .tlsSecretName }} + optional: true diff --git a/mailu/deployment-postfix.yaml b/mailu/deployment-postfix.yaml new file mode 100644 index 0000000..a390393 --- /dev/null +++ b/mailu/deployment-postfix.yaml @@ -0,0 +1,60 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postfix + namespace: {{ .namespace }} +spec: + replicas: 1 + selector: + matchLabels: + component: postfix + template: + metadata: + labels: + component: postfix + spec: + initContainers: + - name: fix-permissions + image: busybox:latest + command: ['sh', '-c', 'chown -R 999:999 /queue'] + volumeMounts: + - name: data + subPath: mailqueue + mountPath: /queue + containers: + - name: postfix + image: {{ .images.postfix }} + imagePullPolicy: IfNotPresent + securityContext: + capabilities: + add: + - SYS_CHROOT + - CHOWN + - SETGID + - SETUID + - NET_BIND_SERVICE + envFrom: + - configMapRef: + name: mailu-config + ports: + - name: smtp + containerPort: 25 + - name: smtps + containerPort: 465 + - name: submission + containerPort: 587 + volumeMounts: + - name: data + subPath: mailqueue + mountPath: /queue + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "2Gi" + cpu: "1000m" + volumes: + - name: data + persistentVolumeClaim: + claimName: mailu-storage diff --git a/mailu/deployment-redis.yaml b/mailu/deployment-redis.yaml new file mode 100644 index 0000000..5424b3f --- /dev/null +++ b/mailu/deployment-redis.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: {{ .namespace }} +spec: + replicas: 1 + selector: + matchLabels: + component: redis + template: + metadata: + labels: + component: redis + spec: + securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + seccompProfile: + type: RuntimeDefault + containers: + - name: redis + image: {{ .images.redis }} + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + readOnlyRootFilesystem: false + ports: + - name: redis + containerPort: 6379 + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 5 + periodSeconds: 5 + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + emptyDir: {} diff --git a/mailu/deployment-rspamd.yaml b/mailu/deployment-rspamd.yaml new file mode 100644 index 0000000..53e225b --- /dev/null +++ b/mailu/deployment-rspamd.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rspamd + namespace: {{ .namespace }} +spec: + replicas: 1 + selector: + matchLabels: + component: rspamd + template: + metadata: + labels: + component: rspamd + spec: + containers: + - name: rspamd + image: {{ .images.rspamd }} + imagePullPolicy: IfNotPresent + env: + - name: REDIS_ADDRESS + value: "{{ .redis.host }}:{{ .redis.port }}" + envFrom: + - configMapRef: + name: mailu-config + ports: + - name: rspamd + containerPort: 11332 + - name: http + containerPort: 11334 + volumeMounts: + - name: data + subPath: rspamd + mountPath: /var/lib/rspamd + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "2Gi" + cpu: "2000m" + volumes: + - name: data + persistentVolumeClaim: + claimName: mailu-storage diff --git a/mailu/deployment-unbound.yaml b/mailu/deployment-unbound.yaml new file mode 100644 index 0000000..0ccdd6f --- /dev/null +++ b/mailu/deployment-unbound.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: unbound + namespace: {{ .namespace }} +spec: + replicas: 1 + selector: + matchLabels: + component: unbound + template: + metadata: + labels: + component: unbound + spec: + containers: + - name: unbound + image: {{ .unbound.image }} + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: mailu-config + env: + - name: UNBOUND_TLS_NAME + value: "dns" + ports: + - name: dns + containerPort: 53 + protocol: UDP + - name: dns-tcp + containerPort: 53 + protocol: TCP + resources: + requests: + memory: "128Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" + livenessProbe: + tcpSocket: + port: 53 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 53 + initialDelaySeconds: 5 + periodSeconds: 5 diff --git a/mailu/deployment-webmail.yaml b/mailu/deployment-webmail.yaml new file mode 100644 index 0000000..c2c3eee --- /dev/null +++ b/mailu/deployment-webmail.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webmail + namespace: {{ .namespace }} +spec: + replicas: 1 + selector: + matchLabels: + component: webmail + template: + metadata: + labels: + component: webmail + spec: + initContainers: + - name: fix-permissions + image: busybox:latest + command: ['sh', '-c', 'chown -R 999:999 /data'] + volumeMounts: + - name: data + subPath: webmail + mountPath: /data + containers: + - name: webmail + image: {{ .images.webmail }} + imagePullPolicy: IfNotPresent + securityContext: + capabilities: + add: + - SYS_CHROOT + - CHOWN + - SETGID + - SETUID + env: + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: mailu-secrets + key: secretKey + envFrom: + - configMapRef: + name: mailu-config + ports: + - name: http + containerPort: 80 + volumeMounts: + - name: data + subPath: webmail + mountPath: /data + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "1000m" + volumes: + - name: data + persistentVolumeClaim: + claimName: mailu-storage diff --git a/mailu/ingress.yaml b/mailu/ingress.yaml new file mode 100644 index 0000000..4a5a5a1 --- /dev/null +++ b/mailu/ingress.yaml @@ -0,0 +1,42 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: mailu + namespace: {{ .namespace }} + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }} + external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" +spec: + ingressClassName: traefik + tls: + - hosts: + - {{ .hostname }} + secretName: {{ .tlsSecretName }} + rules: + - host: {{ .hostname }} + http: + paths: + - path: /admin + pathType: Prefix + backend: + service: + name: admin + port: + number: 80 + - path: /webmail + pathType: Prefix + backend: + service: + name: webmail + port: + number: 80 + - path: / + pathType: Prefix + backend: + service: + name: front + port: + number: 80 diff --git a/mailu/kustomization.yaml b/mailu/kustomization.yaml new file mode 100644 index 0000000..4bef852 --- /dev/null +++ b/mailu/kustomization.yaml @@ -0,0 +1,25 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: mailu +labels: + - includeSelectors: true + pairs: + app: mailu + managedBy: kustomize + partOf: wild-cloud +resources: + - namespace.yaml + - pvc.yaml + - configmap.yaml + - deployment-redis.yaml + - service-redis.yaml + - deployment-unbound.yaml + - service-unbound.yaml + - deployment-admin.yaml + - deployment-front.yaml + - deployment-postfix.yaml + - deployment-dovecot.yaml + - deployment-rspamd.yaml + - deployment-webmail.yaml + - service.yaml + - ingress.yaml diff --git a/mailu/manifest.yaml b/mailu/manifest.yaml new file mode 100644 index 0000000..0518075 --- /dev/null +++ b/mailu/manifest.yaml @@ -0,0 +1,60 @@ +name: mailu +is: mailu +description: Mailu is a simple yet full-featured mail server as a set of Docker images. It includes a mail transfer agent, mail delivery agent, webmail, antispam, antivirus, and admin interface. +version: 2024.06 +icon: https://mailu.io/master/_static/mailu_logo.svg +defaultConfig: + namespace: mailu + + # Domain configuration + domain: "{{ .cloud.baseDomain }}" + hostname: mail.{{ .cloud.domain }} + + # Container images (from ghcr.io) + images: + admin: ghcr.io/mailu/admin:2024.06 + front: ghcr.io/mailu/nginx:2024.06 + postfix: ghcr.io/mailu/postfix:2024.06 + dovecot: ghcr.io/mailu/dovecot:2024.06 + rspamd: ghcr.io/mailu/rspamd:2024.06 + clamav: ghcr.io/mailu/clamav:2024.06 + webmail: ghcr.io/mailu/webmail:2024.06 + redis: redis:alpine + + # Redis configuration (built-in Redis without authentication) + redis: + host: redis.mailu.svc.cluster.local + port: 6379 + + # Unbound DNS resolver (for DNSSEC validation) + unbound: + image: ghcr.io/mailu/unbound:2024.06 + ip: 10.96.200.1 + + # Timezone + timezone: UTC + + # Storage + storage: 100Gi + + # Initial admin account + initialAccount: + enabled: true + username: admin + domain: "{{ .cloud.baseDomain }}" + email: "{{ .operator.email }}" + + # TLS configuration + tlsSecretName: mailu-tls + externalDnsDomain: "{{ .cloud.domain }}" + + # Log level + logLevel: WARNING + + # SMTP relay (optional) + relayHost: "" + relayPort: 25 + +defaultSecrets: + - key: secretKey + - key: initialAccountPassword diff --git a/mailu/namespace.yaml b/mailu/namespace.yaml new file mode 100644 index 0000000..054927e --- /dev/null +++ b/mailu/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .namespace }} diff --git a/mailu/pvc.yaml b/mailu/pvc.yaml new file mode 100644 index 0000000..5d27196 --- /dev/null +++ b/mailu/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mailu-storage + namespace: {{ .namespace }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .storage }} diff --git a/mailu/service-redis.yaml b/mailu/service-redis.yaml new file mode 100644 index 0000000..b4e88c6 --- /dev/null +++ b/mailu/service-redis.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: {{ .namespace }} +spec: + selector: + component: redis + ports: + - name: redis + port: 6379 + targetPort: 6379 + protocol: TCP + type: ClusterIP diff --git a/mailu/service-unbound.yaml b/mailu/service-unbound.yaml new file mode 100644 index 0000000..488bb47 --- /dev/null +++ b/mailu/service-unbound.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: unbound + namespace: {{ .namespace }} +spec: + clusterIP: {{ .unbound.ip }} + selector: + component: unbound + ports: + - name: dns + port: 53 + targetPort: 53 + protocol: UDP + - name: dns-tcp + port: 53 + targetPort: 53 + protocol: TCP + type: ClusterIP diff --git a/mailu/service.yaml b/mailu/service.yaml new file mode 100644 index 0000000..1703373 --- /dev/null +++ b/mailu/service.yaml @@ -0,0 +1,123 @@ +apiVersion: v1 +kind: Service +metadata: + name: admin + namespace: {{ .namespace }} +spec: + selector: + component: admin + ports: + - name: http + port: 80 + targetPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: front + namespace: {{ .namespace }} +spec: + type: LoadBalancer + selector: + component: front + ports: + - name: http + port: 80 + targetPort: 80 + - name: https + port: 443 + targetPort: 443 + - name: smtp + port: 25 + targetPort: 25 + - name: smtps + port: 465 + targetPort: 465 + - name: submission + port: 587 + targetPort: 587 + - name: imap + port: 143 + targetPort: 143 + - name: imaps + port: 993 + targetPort: 993 + - name: pop3 + port: 110 + targetPort: 110 + - name: pop3s + port: 995 + targetPort: 995 +--- +apiVersion: v1 +kind: Service +metadata: + name: postfix + namespace: {{ .namespace }} +spec: + selector: + component: postfix + ports: + - name: smtp + port: 25 + targetPort: 25 +--- +apiVersion: v1 +kind: Service +metadata: + name: dovecot + namespace: {{ .namespace }} +spec: + selector: + component: dovecot + ports: + - name: imap + port: 143 + targetPort: 143 + - name: imaps + port: 993 + targetPort: 993 + - name: pop3 + port: 110 + targetPort: 110 + - name: pop3s + port: 995 + targetPort: 995 + - name: sieve + port: 4190 + targetPort: 4190 + - name: auth + port: 2102 + targetPort: 2102 + - name: lmtp + port: 2525 + targetPort: 2525 +--- +apiVersion: v1 +kind: Service +metadata: + name: rspamd + namespace: {{ .namespace }} +spec: + selector: + component: rspamd + ports: + - name: rspamd + port: 11332 + targetPort: 11332 + - name: http + port: 11334 + targetPort: 11334 +--- +apiVersion: v1 +kind: Service +metadata: + name: webmail + namespace: {{ .namespace }} +spec: + selector: + component: webmail + ports: + - name: http + port: 80 + targetPort: 80