mailu app initial attempt

This commit is contained in:
2026-02-15 18:30:39 +00:00
parent ebc19a9595
commit 9b0c56f720
17 changed files with 842 additions and 0 deletions

30
mailu/configmap.yaml Normal file
View File

@@ -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 }}"

103
mailu/deployment-admin.yaml Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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: {}

View File

@@ -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

View File

@@ -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

View File

@@ -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

42
mailu/ingress.yaml Normal file
View File

@@ -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

25
mailu/kustomization.yaml Normal file
View File

@@ -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

60
mailu/manifest.yaml Normal file
View File

@@ -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

4
mailu/namespace.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .namespace }}

11
mailu/pvc.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mailu-storage
namespace: {{ .namespace }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .storage }}

14
mailu/service-redis.yaml Normal file
View File

@@ -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

View File

@@ -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

123
mailu/service.yaml Normal file
View File

@@ -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