Setup Prerequisites#
Complete guide for preparing your Kubernetes cluster before deploying Mailu.
Problem#
Mailu requires several external services and infrastructure components to function. You need to install these prerequisites before deploying Mailu.
Prerequisites Checklist#
Before deploying Mailu, ensure you have:
Required Infrastructure#
[ ] Kubernetes cluster (1.28+ recommended)
Minimum 3 worker nodes for HA
8GB RAM, 4 CPU cores minimum
Internet connectivity for image pulls
[ ] Persistent storage (ReadWriteOnce volumes)
Longhorn, local-path, cloud provider storage class
100Gi+ available for mailboxes
[ ] Ingress controller (Traefik recommended)
Ports 80, 443, 25, 587, 465, 993, 995 exposed
TLS certificate management (cert-manager)
Required Services#
[ ] PostgreSQL database (12+)
Bitnami Helm chart (simple) or CloudNativePG (production)
See: Setup PostgreSQL
[ ] Redis cache (6+)
Bitnami Helm chart
See: Setup Redis
Required Configuration#
[ ] DNS records pointing to cluster ingress
mail.example.comβ Cluster ingress IPMX record for mail domain
[ ] Kubernetes secrets for Mailu
Mailu secret key
Database credentials
Initial admin password
See: Manage Secrets
Quick Start Path#
Follow these steps in order for a complete deployment:
1. Verify Cluster Readiness#
# Check Kubernetes version
kubectl version
# Check available storage classes
kubectl get storageclass
# Check available resources
kubectl top nodes
2. Setup PostgreSQL#
Choose deployment method based on your needs:
Development/Testing β Bitnami PostgreSQL (5 minutes):
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install postgres bitnami/postgresql \
--namespace postgres --create-namespace \
--set auth.database=mailu \
--set auth.username=mailu \
--set auth.password="$(openssl rand -base64 32)"
Production β CloudNativePG (10 minutes):
# Install operator
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm install cnpg cnpg/cloudnative-pg --namespace cnpg-system --create-namespace
# Deploy cluster (see setup-postgresql.md for full config)
kubectl apply -f postgres-cluster.yaml
π Detailed guide: Setup PostgreSQL
3. Setup Redis#
Simple standalone Redis (3 minutes):
helm install redis bitnami/redis \
--namespace redis --create-namespace \
--set auth.enabled=false \
--set master.persistence.size=5Gi
π Detailed guide: Setup Redis
4. Setup Ingress (Traefik)#
If not already installed:
# Install Traefik
helm repo add traefik https://traefik.github.io/charts
helm install traefik traefik/traefik \
--namespace traefik --create-namespace \
--set ports.web.exposedPort=80 \
--set ports.websecure.exposedPort=443 \
--set ports.smtp.port=25 \
--set ports.smtp.exposedPort=25 \
--set ports.smtps.port=465 \
--set ports.smtps.exposedPort=465 \
--set ports.submission.port=587 \
--set ports.submission.exposedPort=587 \
--set ports.imaps.port=993 \
--set ports.imaps.exposedPort=993 \
--set ports.pop3s.port=995 \
--set ports.pop3s.exposedPort=995
For Traefik with mail ports, create traefik-values.yaml:
ports:
web:
port: 80
exposedPort: 80
websecure:
port: 443
exposedPort: 443
tls:
enabled: true
# Mail protocol ports
smtp:
port: 25
exposedPort: 25
protocol: TCP
submission:
port: 587
exposedPort: 587
protocol: TCP
smtps:
port: 465
exposedPort: 465
protocol: TCP
imaps:
port: 993
exposedPort: 993
protocol: TCP
pop3s:
port: 995
exposedPort: 995
protocol: TCP
service:
type: LoadBalancer # or NodePort
ingressRoute:
dashboard:
enabled: false # Enable if you want Traefik dashboard
Install with custom values:
helm install traefik traefik/traefik \
--namespace traefik --create-namespace \
--values traefik-values.yaml
Get Ingress IP:
kubectl get svc -n traefik traefik
# Note the EXTERNAL-IP for DNS configuration
5. Install cert-manager (TLS Certificates)#
# Install cert-manager
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set installCRDs=true
Create Letβs Encrypt issuer (letsencrypt-prod.yaml):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com # Change this!
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: traefik
Apply:
kubectl apply -f letsencrypt-prod.yaml
6. Configure DNS#
Point your domain to the cluster ingress:
# Get ingress IP
INGRESS_IP=$(kubectl get svc -n traefik traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "Configure DNS: mail.example.com β $INGRESS_IP"
DNS Records to create:
Record Type |
Name |
Value |
Priority |
|---|---|---|---|
A |
mail.example.com |
|
- |
MX |
example.com |
mail.example.com |
10 |
TXT |
example.com |
v=spf1 mx ~all |
- |
Verify DNS:
# Check A record
dig +short mail.example.com
# Check MX record
dig +short MX example.com
7. Create Kubernetes Secrets#
# Create Mailu secrets
kubectl create namespace mailu
kubectl create secret generic mailu-secrets \
--namespace=mailu \
--from-literal=secret-key="$(head -c 16 /dev/urandom | base64 | tr -d '=' | cut -c1-16)" \
--from-literal=password="$(openssl rand -base64 32)"
# Database credentials (if using manual PostgreSQL)
kubectl create secret generic postgres-credentials \
--namespace=mailu \
--from-literal=username="mailu" \
--from-literal=password="<from-postgres-setup>"
π Detailed guide: Manage Secrets
8. Deploy Mailu#
Create mailu.ts:
import { App } from 'cdk8s';
import { MailuChart } from 'cdk8s-mailu';
const app = new App();
new MailuChart(app, 'mailu', {
namespace: 'mailu',
domain: 'example.com',
hostnames: ['mail.example.com'],
subnet: '10.42.0.0/16', // Your Kubernetes pod CIDR
timezone: 'UTC',
database: {
type: 'postgresql',
postgresql: {
host: 'postgres-pooler.postgres.svc.cluster.local', // CNPG
// or: 'postgres-postgresql.postgres.svc.cluster.local', // Bitnami
port: 5432,
database: 'mailu',
secretName: 'postgres-app', // CNPG auto-generated
// or secretName: 'postgres-credentials', // Manual secret
secretKeys: {
username: 'username',
password: 'password',
},
},
},
redis: {
host: 'redis-master.redis.svc.cluster.local',
port: 6379,
},
secrets: {
mailuSecretKey: 'mailu-secrets',
initialAdminPassword: 'mailu-secrets',
},
components: {
webmail: true,
clamav: false, // Disable (high resources)
},
storage: {
storageClass: 'longhorn',
dovecot: { size: '50Gi' },
},
});
app.synth();
Deploy:
npm run synth
kubectl apply -f dist/mailu.k8s.yaml
# Wait for pods to start
kubectl get pods -n mailu -w
9. Create Traefik IngressRoutes#
See Configure TLS for complete IngressRoute configuration.
10. Verify Deployment#
# Check all pods running
kubectl get pods -n mailu
# Test HTTPS access
curl -I https://mail.example.com/admin
# Login to admin UI
# https://mail.example.com/admin
# Username: admin@example.com
# Password: (from mailu-secrets)
π Complete tutorial: Quick Start
Architecture Overview#
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Internet β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Traefik Ingress Controller (TLS Termination) β
β - HTTP/HTTPS (80, 443) β
β - Mail Protocols (25, 587, 465, 993, 995) β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Mailu Front (Nginx) β
β - Protocol routing β
β - Authentication proxy β
βββββββ¬ββββββββββββββββββββββββ¬ββββββββββββββββ¬ββββββββββββββββ
β β β
β β β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Admin UI β β Webmail β β Mail Servicesβ
β (Flask) β β (Roundcube) β β (SMTP/IMAP) β
ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ
β β β
βββββββββββββ¬ββββββββ΄ββββββββββββββββββββ
β
ββββββββββββββΌβββββββββββββ
β β β
β β β
ββββββββββββ ββββββββββββ ββββββββββββ
βPostgreSQLβ β Redis β β PVCs β
β (Users) β β(Sessions)β β(Mailbox) β
ββββββββββββ ββββββββββββ ββββββββββββ
Resource Planning#
Minimum Cluster Requirements#
Development/Testing:
1 node: 4 CPU, 8GB RAM, 50GB storage
Storage class: local-path
Database: SQLite or single-instance PostgreSQL
Redis: Standalone
Production (< 100 users):
3 nodes: 4 CPU, 8GB RAM, 100GB storage each
Storage class: Longhorn (2 replicas)
Database: CloudNativePG (3 replicas)
Redis: Master-Replica
Production (100-500 users):
5 nodes: 8 CPU, 16GB RAM, 200GB storage each
Storage class: Longhorn (2 replicas)
Database: CloudNativePG with backups
Redis: Sentinel
Storage Breakdown#
Component |
Size |
Purpose |
|---|---|---|
Dovecot mailboxes |
50-500Gi |
User email storage (1-5GB per user) |
PostgreSQL |
10-20Gi |
User accounts, config |
Redis |
5Gi |
Sessions, cache |
Admin data |
5Gi |
DKIM keys, config |
Postfix queue |
5-10Gi |
Mail queue |
Rspamd |
5Gi |
Spam learning |
Total storage needed: 80Gi minimum, 550Gi+ for large deployments
Troubleshooting Prerequisites#
Cannot pull images#
# Check internet connectivity
kubectl run test --image=nginx --rm -it -- curl -I google.com
# Check image pull secrets if using private registry
kubectl get secrets -n mailu
Storage class not found#
# List available storage classes
kubectl get storageclass
# If none exist, install storage provider
# Longhorn example:
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml
Ingress controller not exposing ports#
# Check Traefik service type
kubectl get svc -n traefik
# Should show type: LoadBalancer with EXTERNAL-IP
# If pending, check cloud provider load balancer support
# Or use NodePort:
kubectl patch svc -n traefik traefik -p '{"spec":{"type":"NodePort"}}'
Database connection timeout#
# Test connectivity from Mailu namespace
kubectl run -n mailu test --image=postgres:15 --rm -it -- \
psql -h postgres-postgresql.postgres.svc.cluster.local -U mailu -d mailu
# Check PostgreSQL service
kubectl get svc -n postgres
# Check PostgreSQL pods
kubectl get pods -n postgres
Redis connection refused#
# Test connectivity
kubectl run -n mailu test --image=redis:7 --rm -it -- \
redis-cli -h redis-master.redis.svc.cluster.local ping
# Check Redis service
kubectl get svc -n redis
# Check Redis pods
kubectl get pods -n redis
Security Checklist#
Before deploying to production:
[ ] Change all default passwords
PostgreSQL admin and mailu user
Redis password (if enabled)
Mailu admin password
[ ] Enable TLS everywhere
Traefik TLS termination
Letβs Encrypt certificates
HTTPS-only access
[ ] Configure firewall rules
Restrict Kubernetes API access
Allow only required mail ports
Block direct PostgreSQL/Redis access from internet
[ ] Enable network policies (optional)
Restrict pod-to-pod communication
Allow only required service access
[ ] Configure backups
PostgreSQL backups to S3
PVC snapshots (Longhorn/Velero)
Test restore procedures
[ ] Enable monitoring
Prometheus metrics
Pod resource usage alerts
Disk space alerts
Next Steps#
After prerequisites are ready:
π Quick Start Tutorial - Deploy Mailu
π Configure TLS - Set up Traefik IngressRoutes
π Manage Secrets - Create required secrets
π Backup and Restore - Protect your data
See Also#
Setup PostgreSQL - Database deployment options
Setup Redis - Cache deployment
Configure TLS - Traefik ingress setup
Component Specifications - Resource requirements
Architecture - Understanding the system