How to Self-Host Mailu with Docker
What Is Mailu?
Mailu is a full-featured, self-hosted mail server built from standard open-source components (Postfix, Dovecot, Rspamd, SnappyMail) packaged into a cohesive Docker stack with a unified admin interface. It handles SMTP, IMAP, POP3, webmail, antispam, optional antivirus, and DKIM signing out of the box. Think of it as a self-hosted replacement for Gmail, Outlook, or Proton Mail where you own every message and every byte of metadata. Official site
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended) with a dedicated public IP address — shared hosting won’t work for mail
- Docker and Docker Compose installed (guide)
- 2 GB of RAM minimum (4 GB recommended if enabling ClamAV antivirus)
- 10 GB of free disk space (grows with mailbox usage)
- A domain name you control with full DNS access (MX, TXT, PTR records required)
- Reverse DNS (PTR record) configured on your server’s IP — contact your hosting provider to set this to
mail.example.com - Port 25 open — many cloud providers (AWS, GCP, Azure, Oracle) block outbound port 25 by default. Verify with your provider before starting. Hetzner, OVH, and most dedicated server providers allow it.
Self-hosting email is significantly harder than self-hosting most other services. DNS misconfiguration, missing PTR records, or a dirty IP address will cause your emails to land in spam or be rejected entirely. Read this entire guide before deploying.
DNS Configuration
DNS is the single most critical part of a mail server setup. Get this wrong and your emails go straight to spam — or never arrive at all. Configure these records before deploying Mailu.
Replace example.com with your domain and 203.0.113.10 with your server’s public IP.
MX Record
Tells the internet where to deliver email for your domain.
| Type | Name | Value | Priority | TTL |
|---|---|---|---|---|
| MX | example.com | mail.example.com | 10 | 3600 |
| A | mail | 203.0.113.10 | - | 3600 |
SPF Record
Declares which servers are authorized to send email on behalf of your domain. Receiving servers check this to detect spoofing.
| Type | Name | Value | TTL |
|---|---|---|---|
| TXT | example.com | v=spf1 mx a:mail.example.com -all | 3600 |
The -all means “reject anything not from my listed servers.” Use ~all (softfail) during testing, then switch to -all once confirmed working.
DKIM Record
DKIM cryptographically signs outgoing emails so receivers can verify they haven’t been tampered with. Mailu generates the DKIM key automatically — you’ll add this record after initial setup (see Initial Setup section).
| Type | Name | Value | TTL |
|---|---|---|---|
| TXT | dkim._domainkey.example.com | (generated by Mailu — see Initial Setup) | 3600 |
DMARC Record
Tells receiving servers what to do with emails that fail SPF and DKIM checks, and where to send reports.
| Type | Name | Value | TTL |
|---|---|---|---|
| TXT | _dmarc.example.com | v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100 | 3600 |
Start with p=quarantine (send failures to spam). Once you’ve confirmed SPF and DKIM are passing, change to p=reject for maximum protection.
Reverse DNS (PTR Record)
Your server’s IP address must resolve back to mail.example.com. This is configured at your hosting provider’s control panel, not in your domain’s DNS. Without a matching PTR record, Gmail and Outlook will reject or spam your emails.
Autodiscover / Autoconfig Records
These let email clients (Thunderbird, Outlook) automatically configure server settings:
| Type | Name | Value | TTL |
|---|---|---|---|
| CNAME | autoconfig | mail.example.com | 3600 |
| SRV | _autodiscover._tcp | 0 443 mail.example.com | 3600 |
Docker Compose Configuration
Mailu is a multi-container stack. Each service handles a specific mail function. Create a directory for Mailu and add the following files.
mkdir -p /opt/mailu && cd /opt/mailu
Create a docker-compose.yml file:
services:
front:
image: ghcr.io/mailu/nginx:2024.06
container_name: mailu_front
env_file: mailu.env
ports:
- "25:25" # SMTP
- "80:80" # HTTP (redirect to HTTPS)
- "110:110" # POP3
- "143:143" # IMAP
- "443:443" # HTTPS (webmail + admin)
- "465:465" # SMTP over TLS
- "587:587" # SMTP submission
- "993:993" # IMAPS
- "995:995" # POP3S
- "4190:4190" # ManageSieve (mail filtering rules)
volumes:
- mailu_certs:/certs
- mailu_overrides:/overrides:ro
depends_on:
- resolver
dns:
- 192.168.203.254
networks:
default:
ipv4_address: 192.168.203.254
# NOTE: front is both on the mailu network AND receives DNS queries
restart: unless-stopped
resolver:
image: ghcr.io/mailu/unbound:2024.06
container_name: mailu_resolver
env_file: mailu.env
networks:
default:
ipv4_address: 192.168.203.254
restart: unless-stopped
admin:
image: ghcr.io/mailu/admin:2024.06
container_name: mailu_admin
env_file: mailu.env
volumes:
- mailu_data:/data
- mailu_dkim:/dkim
depends_on:
- redis
- resolver
dns:
- 192.168.203.254
restart: unless-stopped
imap:
image: ghcr.io/mailu/dovecot:2024.06
container_name: mailu_imap
env_file: mailu.env
volumes:
- mailu_mail:/mail
- mailu_overrides:/overrides:ro
depends_on:
- front
- resolver
dns:
- 192.168.203.254
restart: unless-stopped
smtp:
image: ghcr.io/mailu/postfix:2024.06
container_name: mailu_smtp
env_file: mailu.env
volumes:
- mailu_mailqueue:/queue
- mailu_overrides:/overrides:ro
depends_on:
- front
- resolver
dns:
- 192.168.203.254
restart: unless-stopped
antispam:
image: ghcr.io/mailu/rspamd:2024.06
container_name: mailu_antispam
env_file: mailu.env
volumes:
- mailu_filter:/var/lib/rspamd
- mailu_overrides:/overrides:ro
depends_on:
- front
- resolver
dns:
- 192.168.203.254
restart: unless-stopped
webmail:
image: ghcr.io/mailu/webmail:2024.06
container_name: mailu_webmail
env_file: mailu.env
volumes:
- mailu_webmail:/data
- mailu_overrides:/overrides:ro
depends_on:
- front
dns:
- 192.168.203.254
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: mailu_redis
volumes:
- mailu_redis:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# Optional: ClamAV antivirus (adds ~1-2 GB RAM usage)
# Uncomment to enable virus scanning on incoming/outgoing mail
# antivirus:
# image: ghcr.io/mailu/clamav:2024.06
# container_name: mailu_antivirus
# env_file: mailu.env
# volumes:
# - mailu_filter:/data
# depends_on:
# - resolver
# dns:
# - 192.168.203.254
# restart: unless-stopped
volumes:
mailu_certs:
mailu_data:
mailu_dkim:
mailu_mail:
mailu_mailqueue:
mailu_filter:
mailu_overrides:
mailu_webmail:
mailu_redis:
networks:
default:
driver: bridge
ipam:
driver: default
config:
- subnet: 192.168.203.0/24
Architecture notes:
- front (Nginx) is the entry point — it terminates TLS and routes traffic to the correct backend service
- resolver (Unbound) provides internal DNS resolution so mail services don’t depend on external DNS
- admin is the web-based admin panel for managing domains, users, and aliases
- imap (Dovecot) handles mailbox storage and IMAP/POP3 access
- smtp (Postfix) handles sending and receiving email via SMTP
- antispam (Rspamd) scores incoming mail and filters spam
- webmail (SnappyMail) provides browser-based email access
- redis stores session data and rate-limiting counters
Environment Configuration
Create a mailu.env file in the same directory. This is where all Mailu configuration lives.
# ============================================================
# Mailu Environment Configuration
# ============================================================
# Full reference: https://mailu.io/2024.06/configuration.html
# ---- Core Settings ----
# Primary mail domain (the domain after @ in email addresses)
DOMAIN=example.com
# Hostname of this mail server (must match your MX and A records)
HOSTNAMES=mail.example.com
# Postmaster email address (receives bounces, DMARC reports)
POSTMASTER=admin
# Secret key for session signing and internal encryption
# MUST CHANGE: Generate with: openssl rand -hex 16
SECRET_KEY=changeme_generate_a_random_hex_string
# ---- Web Settings ----
# Subnet of the Docker network (must match docker-compose.yml)
SUBNET=192.168.203.0/24
# How Mailu obtains TLS certificates
# Options: letsencrypt, cert, notls, mail, mail-letsencrypt
TLS_FLAVOR=letsencrypt
# Admin interface access
# Options: true (enable admin), false (disable)
ADMIN=true
# Webmail client
# Options: snappymail, roundcube, none
WEBMAIL=snappymail
# Web administration interface URL path
WEB_ADMIN=/admin
# Webmail URL path
WEB_WEBMAIL=/webmail
# Real IP header (set if behind a reverse proxy)
# REAL_IP_HEADER=X-Forwarded-For
# REAL_IP_FROM=172.16.0.0/12
# ---- Listening Addresses ----
# Which IP to bind to (0.0.0.0 = all interfaces)
BIND_ADDRESS4=0.0.0.0
# BIND_ADDRESS6=::1
# ---- Mail Settings ----
# Message size limit in bytes (50 MB)
MESSAGE_SIZE_LIMIT=50000000
# Rate limiting: max outgoing emails per user per day
MESSAGE_RATELIMIT=200/day
# Authentication rate limiting (protects against brute force)
AUTH_RATELIMIT_IP=60/hour
AUTH_RATELIMIT_USER=100/day
# Recipient delimiter (for plus addressing: [email protected])
RECIPIENT_DELIMITER=+
# Default spam threshold (higher = more permissive, lower = stricter)
# Recommended: 80 for moderate filtering
ANTISPAM_THRESHOLD=80
# ---- Optional Services ----
# Antivirus (uncomment if you enabled clamav in docker-compose.yml)
# ANTIVIRUS=clamav
# Fetchmail (for pulling mail from external accounts)
# FETCHMAIL_ENABLED=true
# FETCHMAIL_DELAY=600
# ---- Logging ----
# Log driver (stdout recommended for Docker)
LOG_DRIVER=stdout
# ---- Database ----
# Mailu uses SQLite by default for admin data
# This is fine for small to medium deployments (< 1000 users)
# For large deployments, see: https://mailu.io/2024.06/database.html
Critical settings you MUST change:
DOMAIN— your actual mail domainHOSTNAMES— your actual mail server hostnameSECRET_KEY— generate a unique key withopenssl rand -hex 16TLS_FLAVOR— set toletsencryptfor automatic certificates
Start the stack:
docker compose up -d
Wait 2-3 minutes for all services to initialize. The first startup takes longer as Let’s Encrypt issues certificates and Rspamd downloads filter rules.
Initial Setup
Create the Admin Account
Mailu’s admin account is created via the CLI, not the web interface:
docker compose exec admin flask mailu admin admin example.com 'YourStrongPassword'
Replace example.com with your domain and set a strong password. This creates [email protected] as the administrator.
Access the Admin Panel
Open https://mail.example.com/admin in your browser. Log in with the admin credentials you just created.
Add Your Domain
Your primary domain should already appear in the admin panel (set via DOMAIN in the env file). To add additional domains:
- Go to Mail domains in the admin sidebar
- Click Add domain
- Enter the domain name
- Configure domain-specific settings (max users, max aliases, quota)
Retrieve Your DKIM Key
After Mailu starts, it generates a DKIM signing key for your domain:
- In the admin panel, go to Mail domains
- Click the DNS icon next to your domain
- Copy the displayed DKIM public key
- Add it as a TXT record in your DNS:
| Type | Name | Value | TTL |
|---|---|---|---|
| TXT | dkim._domainkey.example.com | (the key from the admin panel) | 3600 |
The key looks like: v=DKIM1; k=rsa; p=MIIBIjANBg... (a long base64 string).
Verify DNS
Use an external tool to confirm your DNS records are correct:
# Check MX record
dig MX example.com +short
# Check SPF record
dig TXT example.com +short
# Check DKIM (after adding the record)
dig TXT dkim._domainkey.example.com +short
# Check DMARC
dig TXT _dmarc.example.com +short
Send a Test Email
Send a test email from your new mail server to an external address (Gmail, Outlook). Then send one back. Check that both directions work. Use mail-tester.com to score your setup — aim for 9/10 or higher.
Configuration
Adding Users
In the admin panel:
- Go to Mail domains → click your domain
- Click Users → Add user
- Set the email address, display name, password, and quota
- Optionally enable the user as a manager (can manage their own aliases)
Aliases
Create aliases under Mail domains → your domain → Aliases. Useful patterns:
[email protected]→[email protected](catch-all alias)*@example.com→[email protected](wildcard catch-all — use carefully)
Quotas
Set per-user storage quotas in the user settings. The default is unlimited. For a personal server this is fine, but set quotas if you’re hosting mail for multiple people.
Sieve Filters
Users can create server-side mail filtering rules via ManageSieve (port 4190). Configure filters in SnappyMail under Settings → Filters, or connect a dedicated Sieve client. Common filters: move mailing lists to folders, auto-delete notifications, forward specific senders.
RESTful API
Mailu exposes a RESTful API for programmatic management. Enable it by visiting the admin panel and generating an API token under Settings. The API supports managing domains, users, aliases, and relay settings.
# Example: List all users
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://mail.example.com/api/v1/user
API documentation is available at https://mail.example.com/api/v1/docs when the admin panel is enabled.
Advanced Configuration (Optional)
ClamAV Antivirus
ClamAV scans incoming and outgoing email for viruses. To enable it:
- Uncomment the
antivirusservice indocker-compose.yml - Add
ANTIVIRUS=clamavtomailu.env - Restart the stack:
docker compose up -d
Warning: ClamAV uses 1-2 GB of RAM for its virus signature database. Only enable this if your server has 4 GB+ of RAM. For most personal mail servers, Rspamd’s built-in malware detection is sufficient without ClamAV.
Fetchmail (Migration from External Accounts)
Fetchmail pulls email from external accounts (Gmail, Outlook) into your Mailu mailbox. Useful for migration.
-
Add to
mailu.env:FETCHMAIL_ENABLED=true FETCHMAIL_DELAY=600 # Check every 600 seconds (10 minutes) -
Add the fetchmail service to
docker-compose.yml:fetchmail: image: ghcr.io/mailu/fetchmail:2024.06 container_name: mailu_fetchmail env_file: mailu.env volumes: - mailu_data:/data depends_on: - admin - smtp - imap - resolver dns: - 192.168.203.254 restart: unless-stopped -
Configure external accounts in the admin panel under each user’s settings.
Custom TLS Certificates
If you’re not using Let’s Encrypt (e.g., you have certificates from another CA):
- Set
TLS_FLAVOR=certinmailu.env - Mount your certificates into the
mailu_certsvolume:# Copy certificates into the certs volume docker cp /path/to/cert.pem mailu_front:/certs/cert.pem docker cp /path/to/key.pem mailu_front:/certs/key.pem
Mailu expects cert.pem and key.pem in the /certs directory.
Relay Through External SMTP
If your provider blocks port 25, you can relay outgoing mail through an external SMTP service (Mailgun, SendGrid, Amazon SES):
In the admin panel, go to Mail domains → your domain → Edit and configure:
- Relayed domain SMTP host:
smtp.mailgun.org - Relayed domain SMTP port:
587 - Relayed domain SMTP username/password: your relay credentials
This sends all outgoing mail through the relay while still receiving mail directly.
Reverse Proxy
Mailu handles its own TLS termination through the front (Nginx) container. In most deployments, you should NOT put Mailu behind another reverse proxy — just point your DNS directly at the server running Mailu and let it manage ports 80, 443, and all mail ports.
If you must run Mailu alongside other web services on the same server, you have two options:
Option 1: Separate IP Addresses (Recommended)
Assign a second IP to your server and bind Mailu to it exclusively by changing BIND_ADDRESS4 in mailu.env. Your other web services use the primary IP.
Option 2: External Reverse Proxy
If you need an external proxy (e.g., Nginx Proxy Manager):
- Set
TLS_FLAVOR=notlsinmailu.env(the external proxy handles TLS) - Change the
frontport mapping to avoid conflicts:ports: - "25:25" - "8080:80" # HTTP behind proxy - "110:110" - "143:143" - "465:465" - "587:587" - "993:993" - "995:995" - "4190:4190" - Set
REAL_IP_HEADER=X-Forwarded-ForandREAL_IP_FROM=172.16.0.0/12inmailu.env - Proxy HTTPS traffic to port 8080
Important: Only HTTP(S) traffic goes through the proxy. SMTP, IMAP, and POP3 ports (25, 465, 587, 143, 993, 110, 995) must still be mapped directly — reverse proxies don’t handle mail protocols.
See Reverse Proxy Setup for general configuration.
Backup
Mailu stores data across several volumes. Back up all of them:
| Volume | Contents | Critical? |
|---|---|---|
mailu_mail | All user mailboxes (Dovecot Maildir format) | Yes — this is your email |
mailu_data | Admin database (users, domains, aliases, settings) | Yes — losing this means reconfiguring everything |
mailu_dkim | DKIM signing keys | Yes — regenerating means updating DNS |
mailu_filter | Rspamd learned data, Bayes filter | Nice to have — relearns over time |
mailu_certs | TLS certificates | Optional — Let’s Encrypt regenerates them |
mailu_redis | Session data, rate limits | Optional — transient data |
mailu_mailqueue | Pending outgoing emails | Back up if possible — contains unsent mail |
Back up the critical volumes:
# Stop services for consistent backup
docker compose stop
# Back up all volumes
tar -czf mailu-backup-$(date +%Y%m%d).tar.gz \
-C /var/lib/docker/volumes \
mailu_mail mailu_data mailu_dkim mailu_filter mailu_mailqueue
# Restart services
docker compose start
For zero-downtime backups, use filesystem snapshots or rsync the Maildir directories while Dovecot is running (Maildir format is safe for this).
See Backup Strategy for a comprehensive approach.
Troubleshooting
Emails Going to Spam
Symptom: Outgoing emails land in recipients’ spam folders on Gmail, Outlook, or Yahoo.
Fix: This is almost always a DNS or reputation issue. Check in order:
- PTR record: Run
dig -x YOUR_SERVER_IP— it must returnmail.example.com. If not, contact your hosting provider. - SPF: Run
dig TXT example.com— verify your SPF record is present and correct. - DKIM: In the Mailu admin panel, check the DNS page for your domain. Verify the DKIM TXT record matches.
- DMARC: Run
dig TXT _dmarc.example.com— verify the record exists. - IP reputation: Check your IP at multirbl.valli.org. If blocklisted, you may need to request delisting or use a relay.
- Test score: Send a test to mail-tester.com — it shows exactly what’s wrong.
Port 25 Blocked by Provider
Symptom: Outgoing mail times out. Incoming mail may still work (other servers can’t reach you either if inbound 25 is blocked).
Fix: Many cloud providers block port 25 to prevent spam. Options:
- Request unblock: Some providers (Hetzner, DigitalOcean, Vultr) will unblock port 25 if you open a support ticket and explain you’re running a legitimate mail server.
- Use a relay: Configure outgoing relay through Mailgun, SendGrid, or Amazon SES (see Advanced Configuration). This handles outgoing mail but you still need inbound port 25 for receiving.
- Switch providers: If your provider won’t unblock port 25, you need a different host. Hetzner and OVH are reliably mail-friendly.
TLS Certificate Errors
Symptom: Email clients show certificate warnings. Browser shows “Not Secure” for the admin panel.
Fix:
- Verify
HOSTNAMESinmailu.envexactly matches your DNS A record. - Ensure ports 80 and 443 are reachable from the internet (Let’s Encrypt needs port 80 for HTTP-01 challenge).
- Check front container logs:
docker compose logs front | grep -i cert - If using
TLS_FLAVOR=letsencrypt, restart the front container to trigger a new certificate request:docker compose restart front - Verify there’s no other service (Apache, Nginx) already using ports 80/443 on the host.
Admin Login Fails After Setup
Symptom: Cannot log in to the admin panel despite creating the admin user.
Fix:
- Verify the admin user was created:
docker compose exec admin flask mailu user-list example.com - Reset the admin password:
docker compose exec admin flask mailu admin admin example.com 'NewPassword' - Check that
ADMIN=trueis set inmailu.env. - Ensure you’re accessing the correct URL:
https://mail.example.com/admin(not/admin/or the webmail URL).
High RAM Usage (ClamAV)
Symptom: Server running out of memory. OOM killer terminating containers.
Fix: ClamAV loads its entire virus signature database into RAM, consuming 1-2 GB. If your server has limited memory:
- Disable ClamAV: Comment out the
antivirusservice indocker-compose.ymland removeANTIVIRUS=clamavfrommailu.env. Rspamd still provides malware detection without ClamAV. - Add swap space as a safety net:
fallocate -l 2G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab - Monitor usage:
docker statsshows per-container memory consumption. Theantiviruscontainer is almost always the largest consumer.
Resource Requirements
- RAM (without ClamAV): 1-1.5 GB idle, 2 GB under load
- RAM (with ClamAV): 2.5-3.5 GB idle, 4 GB under load
- CPU: Low to moderate — mail is not CPU-intensive unless processing large attachment volumes
- Disk: ~500 MB for Mailu itself, plus mailbox storage (budget 1-5 GB per active user depending on retention)
- Network: Dedicated public IP required. Ports 25, 80, 110, 143, 443, 465, 587, 993, 995 must be accessible
Verdict
Mailu is the best Docker-native mail server for people who want a complete, batteries-included setup without spending days integrating individual components. Its admin UI, automatic DKIM key generation, and built-in webmail make it significantly easier to deploy than assembling Postfix + Dovecot + Rspamd manually.
That said, self-hosting email remains one of the hardest things to self-host. IP reputation is a real problem — if your server IP was previously used for spam, major providers will reject your mail regardless of how perfectly you configure DNS. Port 25 restrictions from cloud providers add another barrier. And unlike other self-hosted services, email has zero tolerance for downtime — if your server is offline, you miss messages.
Compared to Mailcow, Mailu is lighter on resources and simpler to configure. Mailcow has a more polished web UI and includes SOGo for calendar/contacts integration. Choose Mailu if you want a lean, efficient mail stack. Choose Mailcow if you want groupware features alongside email. See our Mailu vs Mailcow comparison for a detailed breakdown.
Recommendation: If you need self-hosted email and understand the challenges, Mailu is the right choice for personal and small-team use. If you just want reliable email and don’t need full server control, a privacy-focused provider like Proton Mail or Fastmail will save you significant operational headaches.
FAQ
Is self-hosting email worth it?
It depends on your motivation. For privacy and data sovereignty — yes, nothing beats running your own server. For cost savings — probably not, since a $4/month Fastmail account is cheaper than the server resources and time investment. For learning — absolutely, running a mail server teaches you more about internet infrastructure than almost any other project.
Can I use Mailu with multiple domains?
Yes. Add additional domains in the admin panel under Mail domains. Each domain can have its own users, aliases, and quotas. You need to configure DNS records (MX, SPF, DKIM, DMARC) for each domain separately.
Will Gmail accept email from my Mailu server?
If your DNS records (SPF, DKIM, DMARC) are correct, your PTR record matches, and your IP isn’t blocklisted — yes. New IP addresses sometimes face initial distrust. Send emails to Gmail addresses gradually (don’t blast 1,000 emails on day one) and Gmail will warm up to your server within a few days to weeks.
How do I migrate email from Gmail to Mailu?
Enable Fetchmail in Mailu (see Advanced Configuration) and configure it to pull from your Gmail account via IMAP. Gmail requires you to enable “Less secure app access” or create an App Password if 2FA is enabled. Fetchmail will download all existing email into your Mailu mailbox.
Can I run Mailu on a Raspberry Pi?
Technically yes, but not recommended. Mailu’s multi-container stack consumes 1-2 GB of RAM without ClamAV. A Raspberry Pi 4 with 4 GB can handle it for a single user, but performance will be marginal. A small VPS or mini PC is a much better choice for a mail server.
Related
Get self-hosting tips in your inbox
New guides, comparisons, and setup tutorials — delivered weekly. No spam.