Self-Host Mail with Mailcow Dockerized
Deploy a full self-hosted mail server with Mailcow Dockerized: DNS setup, DKIM, SOGo groupware, Let's Encrypt TLS, automated backups, and monitoring.
Before you start
- ▸A VPS or dedicated server with at least 8 GB RAM and a static IP address
- ▸Outbound port 25 unblocked by your hosting provider
- ▸A registered domain with the ability to edit DNS records and set a PTR record
- ▸Docker Engine 24+ with the Compose v2 plugin installed
Running your own mail server has a reputation for being a nightmare of Postfix configs, Dovecot ACLs, and Rspamd tuning. Mailcow Dockerized wraps all of that into a single Docker Compose stack—Postfix, Dovecot, Rspamd, SOGo, Nginx, and more—with a polished web UI. You still need to understand what you're doing: bad DNS means bounced mail, and a misconfigured server will get you blacklisted fast. This guide walks through a production-ready deployment from scratch.
Prerequisites and System Requirements
Mailcow is not lightweight. The official minimum spec is 6 GB RAM and 2 CPU cores, but 8 GB RAM is strongly recommended for a low-traffic server. You need a VPS or dedicated machine with a static IP, a registered domain, and reverse DNS (PTR) control through your hosting provider.
- A clean Ubuntu 22.04 or Debian 12 server (the project tests against these primarily)
- A fully-qualified hostname, e.g.
mail.example.com, resolving to your server IP - Port 25 unblocked—many cloud providers (AWS, GCP, Azure) block outbound port 25 by default; request it be opened before you start
- Docker Engine 24+ and Docker Compose v2 (the
docker composeplugin, not the legacydocker-composebinary)
Install Docker Engine
Mailcow ships its own installer that checks Docker versions, but install it cleanly first.
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
Verify Compose v2 is available:
docker compose version
# Docker Compose version v2.27.0
Clone Mailcow and Generate Config
cd /opt
git clone https://github.com/mailcow/mailcow-dockerized
cd mailcow-dockerized
Run the interactive setup script. It sets your hostname and timezone, then writes mailcow.conf.
./generate_config.sh
You will be prompted for your mail server FQDN (e.g. mail.example.com) and your timezone string (e.g. Europe/Berlin). The script also selects a random HTTP/HTTPS port offset if you want Nginx on non-standard ports, which is useful when an existing web server occupies 80/443.
Open mailcow.conf and review key settings:
grep -E '^(MAILCOW_HOSTNAME|TZ|HTTPS_PORT|HTTP_PORT|SKIP_LETS_ENCRYPT)' mailcow.conf
If you have an existing reverse proxy handling TLS, set HTTPS_PORT=8443 and HTTP_PORT=8080, then proxy to those. For a standalone server, leave ports at 443/80 and keep SKIP_LETS_ENCRYPT=n.
DNS Records
Get DNS right before you pull containers. Mailcow's ACME client and incoming mail both depend on correct DNS. Add these records to your domain registrar or DNS provider:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | YOUR_SERVER_IP | 300 | |
| MX | @ | mail.example.com (priority 10) | 300 |
| TXT | @ | v=spf1 mx ~all | 300 |
| TXT | _dmarc | v=DMARC1; p=quarantine; rua=mailto:[email protected] | 300 |
| PTR | YOUR_SERVER_IP | mail.example.com | — |
The DKIM record is generated after first boot—add it once Mailcow creates the key. Set your PTR record through your hosting control panel; if the option isn't there, open a support ticket.
Start the Stack
docker compose pull
docker compose up -d
First startup takes 3–5 minutes. Mailcow initialises databases, generates DKIM keys, and requests a Let's Encrypt certificate for your hostname. Watch the ACME container:
docker compose logs -f acme-mailcow
A successful run ends with a line containing OK and the certificate paths. If you see errors, DNS probably hasn't propagated yet—wait and re-run docker compose restart acme-mailcow.
Add DKIM to DNS
Once the stack is up, retrieve the auto-generated DKIM public key:
cat /opt/mailcow-dockerized/data/conf/rspamd/override.d/dkim_signing.conf
The UI path is easier: log in at https://mail.example.com with credentials admin / moohoo (change this immediately), navigate to Configuration → Domains → DKIM, and copy the DNS TXT record shown there. Add it to your DNS as a TXT record on dkim._domainkey.example.com.
Change the admin password immediately via Access → Edit administrator.
Create a Domain and Mailbox
In the web UI:
- Go to Configuration → Mail Setup → Domains → Add domain. Enter
example.com, set quotas as appropriate. - Go to Mailboxes → Add mailbox. Create your first user, e.g.
[email protected]. - Aliases, catch-alls, and forwarding rules are all in the same panel.
SOGo Groupware
SOGo is bundled and handles CalDAV, CardDAV, and a webmail interface at https://mail.example.com/SOGo. Log in with your mailbox credentials. To connect a desktop client (Thunderbird, GNOME Calendar), use these endpoints:
- IMAP: mail.example.com:993, TLS
- SMTP: mail.example.com:587, STARTTLS
- CalDAV:
https://mail.example.com/SOGo/dav/[email protected]/Calendar/personal/ - CardDAV:
https://mail.example.com/SOGo/dav/[email protected]/Contacts/personal/
SOGo's per-user settings (vacation responder, delegates, ACLs) live under the gear icon in the top-right of the SOGo interface.
Backups
Mailcow ships a backup script. Run it manually first to verify it works:
cd /opt/mailcow-dockerized
bash helper-scripts/backup_and_restore.sh backup all
Backups land in /var/mailcow-backups by default. The archive includes MySQL dumps, Rspamd config, and all mail volumes. Schedule it with a systemd timer rather than cron:
cat > /etc/systemd/system/mailcow-backup.service <<'EOF'
[Unit]
Description=Mailcow backup
[Service]
Type=oneshot
WorkingDirectory=/opt/mailcow-dockerized
ExecStart=/bin/bash helper-scripts/backup_and_restore.sh backup all
EOF
cat > /etc/systemd/system/mailcow-backup.timer <<'EOF'
[Unit]
Description=Daily Mailcow backup
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now mailcow-backup.timer
Sync backups off-site with rclone or rsync to object storage. A local-only backup on the same disk that holds your mail is not a backup.
Monitoring
Mailcow exposes a built-in watchdog container that restarts unhealthy services, but you should add external checks.
Check Container Health
docker compose ps
All containers should show healthy or running. The watchdog-mailcow container logs restarts.
Mail Queue and Rspamd
The UI at Information → Mail Queue shows stuck messages. The Rspamd dashboard at https://mail.example.com/rspamd (login via the UI under Configuration → Rspamd UI) shows spam scores, actions, and throughput graphs in real time.
External Blacklist Check
# Check your IP against common DNSBLs
dig +short YOUR_SERVER_IP.zen.spamhaus.org
dig +short YOUR_SERVER_IP.bl.spamcop.net
No output means you're not listed. If you are, follow the delisting process for that specific list before expecting reliable mail delivery.
Verify SPF, DKIM, DMARC
Send a test message to [email protected] or use mail-tester.com. You should score 9/10 or higher; anything lower points to a misconfigured DNS record.
Updates
Mailcow ships an update script that pulls new images and runs database migrations:
cd /opt/mailcow-dockerized
bash update.sh
Read the release notes before updating—breaking changes are flagged there. Take a backup immediately before running updates in production.
Troubleshooting
ACME Certificate Fails
Port 80 must be reachable from the internet for HTTP-01 challenge. Verify with curl -I http://mail.example.com from an external host. If a firewall is blocking it, open port 80 even if you redirect to HTTPS—the challenge hits the bare HTTP URL.
Outbound Mail Goes to Spam
In order of likelihood: missing or wrong DKIM TXT record, PTR record not set, SPF record too permissive or missing, server IP on a blacklist. Use mail-tester.com to identify which check is failing.
Port 25 Blocked by Provider
AWS EC2, Google Cloud, and Azure all block port 25 by default. AWS requires a service quota increase request via the console. GCP requires a similar request. There is no workaround—you must get the block lifted or use a relay like SendGrid for outbound with Mailcow receiving inbound through a different host.
Container Keeps Restarting
docker compose logs --tail=50 postfix-mailcow
Replace postfix-mailcow with the failing container name from docker compose ps. Permission errors on volumes usually mean a previous run left root-owned files; the Mailcow docs cover fixing ownership with the provided helper scripts.
Frequently asked questions
- Can I run Mailcow on a server that already hosts websites on ports 80 and 443?
- Yes. Set HTTP_PORT and HTTPS_PORT to alternate values in mailcow.conf and proxy requests from your existing Nginx or Caddy instance. Mailcow's documentation includes an example reverse-proxy config.
- How do I migrate existing mail from another server or Gmail?
- Use the imapsync tool or Mailcow's built-in sync job feature under Configuration → Sync Jobs. It connects to the source server over IMAP and copies messages folder by folder.
- Is Mailcow suitable for a small business with 20–50 mailboxes?
- Yes, that is a typical use case. A 4-core, 8 GB RAM VPS handles it comfortably. Make sure your hosting provider does not block port 25 and that you have dedicated IP reputation.
- How do I renew TLS certificates—does it happen automatically?
- The acme-mailcow container checks and renews certificates daily. As long as port 80 is reachable, renewal is fully automatic. You can force a check with `docker compose restart acme-mailcow`.
- Can I use my own wildcard certificate instead of Let's Encrypt?
- Yes. Set SKIP_LETS_ENCRYPT=y in mailcow.conf, then place your certificate and key at the paths defined in the Nginx config inside data/conf/nginx/. Restart the nginx-mailcow container after copying the files.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.