Dovecot Submission Service¶
Understanding the dedicated dovecot submission service for webmail email sending.
Overview¶
The DovecotSubmissionConstruct provides a dedicated dovecot submission service that enables email sending from Roundcube webmail. This service was introduced to solve configuration challenges with the bundled dovecot in the front container when using TLS_FLAVOR=notls (required for Traefik TLS termination).
The Problem¶
When a user sends an email from webmail, the webmail backend needs to submit that email to the SMTP server. Mailu’s standard architecture uses the bundled dovecot service in the front container for this, but with TLS_FLAVOR=notls, configuring this bundled dovecot becomes extremely difficult because:
The front container’s dovecot configuration is deeply embedded in Mailu’s startup scripts
Environment variable substitution doesn’t work properly with dovecot syntax
Configuration files are generated at runtime in read-only locations
Modifying the bundled dovecot requires extensive wrapper script modifications
The Solution¶
Instead of fighting with the bundled dovecot, cdk8s-mailu deploys a separate dovecot submission service using the official dovecot/dovecot:2.3-latest image with custom configuration.
Architecture¶
Webmail (Roundcube)
↓ PLAIN auth with token (port 10025)
Dovecot Submission Service
- Accepts: nopassword=y (static passdb)
- User: uid=mail (8), gid=mail
- Mail location: maildir:/tmp/mail
↓ submission_relay_host (port 25, no auth)
Postfix
- Trusts: mynetworks (10.42.0.0/16 pod network)
- Accepts: plaintext from pod network
↓
Email Delivery ✅
Key Features¶
Clean Configuration
Uses standard dovecot.conf syntax
No complex wrapper script modifications
Easy to understand and troubleshoot
Token Authentication
Webmail uses Mailu session tokens
Dovecot accepts with
nopassword=ystatic passdbToken validation happens at webmail level, not dovecot
Trusted Network Relay
Dovecot relays to postfix:25 without authentication
Postfix trusts pod network (
10.42.0.0/16)No complex SASL configuration required
Service Isolation
Dedicated pod with clear logs
Independent scaling and resource management
No conflicts with front container’s dovecot usage
Implementation Details¶
Dovecot Configuration¶
The construct generates a dovecot.conf template with key settings:
# Protocols - only submission
protocols = submission
# Allow low UIDs (mail user is UID 8)
first_valid_uid = 8
last_valid_uid = 0
# Mail location (relay-only, no actual storage needed)
mail_location = maildir:/tmp/mail
# Submission relay configuration
submission_relay_host = postfix.mailu.svc.cluster.local
submission_relay_port = 25
submission_relay_trusted = yes
submission_relay_ssl = no
# Authentication via static passdb
passdb {
driver = static
args = nopassword=y
}
# User database (static, minimal config for relay)
userdb {
driver = static
args = uid=mail gid=mail home=/tmp
}
Build-Time Configuration Substitution¶
The construct uses build-time substitution to generate the final dovecot configuration during CDK8S synthesis:
const dovecotConf = `# Dovecot Submission Service Configuration
# Generated by CDK8S - values substituted at build time
# Admin and hostname
postmaster_address = admin@${config.domain}
hostname = ${config.domain}
# Submission relay configuration
submission_relay_host = ${postfixServiceName}
submission_relay_port = 25
submission_relay_trusted = yes
submission_relay_ssl = no
`;
Benefits of build-time substitution:
No runtime wrapper scripts or environment variable dependencies
Final configuration visible in
kubectl get configmapoutputSimpler container startup (direct dovecot execution)
Easier debugging (no runtime substitution logic)
The postfix service name is passed from the chart using the full DNS name: ${postfixService.name}.${namespace}.svc.cluster.local
Service Discovery¶
The dovecot submission service is registered in the shared ConfigMap as SUBMISSION_ADDRESS:
if (this.dovecotSubmissionConstruct?.service) {
this.sharedConfigMap.addData(
'SUBMISSION_ADDRESS',
`${this.dovecotSubmissionConstruct.service.name}.${namespace}.svc.cluster.local`
);
}
Webmail uses this environment variable to connect to the correct service, handling CDK8S’s hash-based service names automatically.
Architecture Requirements¶
AMD64 Only¶
The official dovecot/dovecot:2.3-latest image only supports AMD64 architecture. The construct automatically adds:
Node Selector:
nodeSelector:
kubernetes.io/arch: amd64
Toleration (for AMD64 taint):
tolerations:
- key: kubernetes.io/arch
operator: Equal
value: amd64
effect: NoSchedule
This ensures the pod is scheduled to an AMD64 node in mixed-architecture clusters.
Configuration Mounting¶
The final dovecot configuration (with all values substituted at build time) is mounted directly from the ConfigMap:
const configVolume = kplus.Volume.fromConfigMap(this, 'config-volume', this.configMap);
container.mount('/etc/dovecot', configVolume, {
readOnly: true,
});
The container starts dovecot directly with the mounted configuration:
command: ['/usr/sbin/dovecot', '-F', '-c', '/etc/dovecot/dovecot.conf']
No environment variables or wrapper scripts are needed - the ConfigMap contains the final, ready-to-use configuration.
Webmail Integration¶
The WebmailPatchConfigMap construct patches Roundcube’s configuration to use the dovecot submission service:
SUBMISSION_HOST="${SUBMISSION_ADDRESS:-dovecot-submission}"
sed -i "s|tls://[^:]*:10025|smtp://${SUBMISSION_HOST}:10025|g" "$RC_CONFIG"
This replaces the hardcoded FRONT_ADDRESS:10025 reference with the dynamically discovered dovecot submission service.
Authentication Flow¶
User sends email from webmail: Roundcube submits to
${SUBMISSION_HOST}:10025Dovecot authenticates: Static passdb with
nopassword=yaccepts the connectionToken validation happens at webmail level, not dovecot
Dovecot relays to postfix: Using
submission_relay_host, dovecot forwards topostfix:25without authenticationTrusted network (pod CIDR)
Postfix accepts and delivers: Postfix trusts connections from the pod network and delivers the email
Why This Approach Works¶
No Postfix Authentication Required:
Connections come from trusted pod network
Only the dovecot submission service can connect to postfix:25 from within the cluster
Webmail has already authenticated the user via Mailu’s SSO
Token Authentication at Webmail Level:
Roundcube uses Mailu’s session tokens
By the time a request reaches dovecot submission, user is already authenticated
Dovecot just needs to relay (not validate credentials)
Separate Service Isolation:
Simplifies configuration (clean dovecot.conf instead of patching Mailu’s templates)
Enables easy troubleshooting (dedicated pod with clear logs)
Allows independent scaling and resource management
Configuration Example¶
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',
// Database configuration
database: {
type: 'postgresql',
postgresql: {
host: 'postgres-rw',
port: 5432,
database: 'mailu',
secretName: 'postgres-app',
secretKeys: {
username: 'username',
password: 'password',
},
},
},
// Redis configuration
redis: {
host: 'redis',
port: 6379,
},
// Secrets
secrets: {
mailuSecretKey: 'mailu-secrets',
initialAdminPassword: 'mailu-secrets',
},
// Optional: Custom dovecot submission resources
resources: {
dovecot: {
requests: {
cpu: '100m',
memory: '256Mi',
},
limits: {
cpu: '300m',
memory: '512Mi',
},
},
},
});
app.synth();
The dovecot submission service is automatically deployed as part of the MailuChart. No additional configuration required!
Troubleshooting¶
Dovecot pod stuck in CrashLoopBackOff¶
Check logs:
kubectl logs -n mailu -l app.kubernetes.io/component=dovecot-submission --tail=50
Common issues:
Invalid configuration: Dovecot config validation failed
Check for syntax errors in dovecot.conf template
Verify environment variable substitution worked
Architecture mismatch: Pod scheduled to ARM64 node
Verify nodeSelector and toleration are applied
UID errors:
first_valid_uidnot set correctlyEnsure
first_valid_uid = 8is in the configuration
Webmail can’t send email¶
Check webmail logs:
kubectl logs -n mailu -l app.kubernetes.io/component=webmail --tail=50 | grep -i smtp
Verify service discovery:
kubectl get configmap -n mailu -l 'app.kubernetes.io/part-of=mailu' -o yaml | grep SUBMISSION_ADDRESS
Should show the full service name.
Test connection:
POD=$(kubectl get pod -n mailu -l app.kubernetes.io/component=webmail -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n mailu $POD -- nc -zv <submission-service-name> 10025
See Also¶
Architecture Overview - High-level design
CDK8S Patterns - Construct design patterns
Webmail Configuration - Customizing webmail