VPS Tutorials
Set Up a Mailcow Mail Server on a VPS (2026): Docker, DNS, Anti-Spam, Backups and Deliverability
Install the self-hosted Mailcow mail server on a VPS, with full configuration of Docker, PTR, SPF, DKIM, DMARC, TLS, anti-spam, off-site backups, upgrades and deliverability troubleshooting.

If you want to send and receive mail with your own domain without separately installing Postfix, Dovecot, Rspamd, webmail and certificate services, Mailcow is one of the more complete self-hosted mail suites available. It bundles SMTP, IMAP, SOGo webmail, anti-spam, virus scanning, DKIM and an admin panel into a single Docker Compose project.
But a mail server is not something that "just works once the containers start." Port 25 on the VPS, a dedicated public IP, PTR reverse DNS and the IP's historical reputation often matter more than CPU performance. If you pick the wrong machine, mail sent to Gmail, Outlook or corporate mailboxes may still be rejected or land in spam even when the admin panel is all green.
This tutorial starts with the checks to do before buying a VPS, uses the official Mailcow mailcow-dockerized repository to complete the installation, configures A, MX, PTR, SPF, DKIM, DMARC, TLS, anti-spam, backups and upgrades, and provides a deliverability and common-troubleshooting checklist.
Mailcow is a complete mail and collaboration suite, not a lightweight SMTP forwarder.
| Requirement | Is Mailcow suitable | Reason |
|---|---|---|
| A small team using mailboxes, calendars and contacts on its own domain | Suitable | Includes SOGo, IMAP, SMTP, ActiveSync-related capabilities and a unified admin panel |
| Needing control over mailbox data, archiving and account lifecycle | Suitable | Data stays on your own VPS and Docker Volumes |
| A website that only needs to send verification codes and notification emails | Usually not suitable | A dedicated transactional email service is less maintenance and builds sending reputation more easily |
| Bulk marketing email, cold email | Not suitable | Easily damages IP and domain reputation, and may violate the VPS terms of service |
| A VPS where you cannot get a PTR or open port 25 | Not suitable | External mail servers may reject outright or lower your reputation |
If you only need to send notifications for Ghost, WordPress or a business system and do not need employee mailboxes, prefer transactional email services such as Amazon SES, Postmark or Mailgun. Mailcow is better suited to teams that genuinely need full mailboxes, webmail, calendars and domain management.
First read the provider's Acceptable Use Policy and confirm with support that self-hosted mail servers are permitted. Some cheap VPS plans prohibit mail services or block outbound TCP 25 permanently by default.
"You can open 25 in the security group" does not mean the upstream network allows outbound traffic. Ask explicitly before buying:
- Is inbound TCP 25 open;
- Is outbound TCP 25 open, and does it require an unblock request;
- Are ports such as 465, 587, 993, 80 and 443 restricted;
- Are there waiting periods or KYC requirements for new accounts.
If the provider can only open 587, that still cannot replace the port 25 needed for server-to-server delivery.
The PTR for the public IPv4 should be set by the VPS/IP provider to the Mailcow hostname, for example:
203.0.113.10 -> mail.example.com
At the same time, the A record for mail.example.com must resolve forward to the same IP. A forward/reverse mismatch is a common reason for mail landing in spam or being rejected.
After getting the IP, first check common real-time blacklists and historical abuse records. If the IP already has a serious spam reputation, the most time-saving option is usually to have the provider change the IP rather than trying to "clean" it slowly with a new domain.
For further troubleshooting of rejections, spam folder placement and authentication results, use this alongside the VPS self-hosted mail deliverability troubleshooting guide.
The official Mailcow minimum for the default configuration is 6GiB of memory plus 1GiB of Swap, with at least 20GiB of disk not including mail data; the official recommendation is 8GiB of memory for about 5 to 10 users. ClamAV and full-text search consume noticeable memory.
| Use case | Recommended VPS configuration | Notes |
|---|---|---|
| Personal or up to 3 mailboxes | 4 cores, 8GB, 80GB SSD | Keep ClamAV and full-text search, and reserve space for the system and backups |
| 5-10 users | 4 cores, 8-16GB, 160GB SSD | Scale up based on attachments, retention period and mobile connections |
| About 50 concurrent IMAP connections, multiple ActiveSync devices | 8 cores, 16GB and up | The official example recommends 16GiB; do a capacity assessment before deploying |
| Less than 6GB of memory | Default configuration not recommended | Even with ClamAV/FTS disabled, it reduces features and safety margin |
Do not look only at the number of mailboxes. Long-lived mobile connections, full-text indexing, large attachments, spam training and backups all affect resources. If you have not settled on machine specs yet, see the VPS configuration selection guide first.
Virtualization must be full virtualization such as KVM, ESX or Hyper-V. Mailcow officially does not support OpenVZ, Virtuozzo or LXC container-based VPS. The system used as an example is Ubuntu 24.04 LTS, x86_64.
This article uses the following example:
邮件域名:example.com
Mailcow 主机名:mail.example.com
服务器 IPv4:203.0.113.10
The hostname must be a fully qualified domain name, not just mail, and you should not use the root domain example.com directly. If DNS is hosted on Cloudflare, mail.example.com must be set to DNS only and must not have the orange cloud proxy enabled, because Cloudflare's regular proxy does not forward mail ports such as SMTP and IMAP.
Set the server hostname and timezone before installation:
sudo hostnamectl set-hostname mail.example.com
sudo timedatectl set-timezone Asia/Shanghai
hostname -f
timedatectl status
Incorrect time affects TLS, log correlation and TOTP two-factor authentication, so it cannot be skipped.
Mailcow uses these TCP ports by default:
| Port | Purpose |
|---|---|
| 25 | SMTP delivery between mail servers |
| 465 / 587 | Encrypted submission from mail clients |
| 143 / 993 | IMAP / IMAPS |
| 110 / 995 | POP3 / POP3S |
| 4190 | ManageSieve filter rules |
| 80 / 443 | Admin panel, webmail, certificate validation |
Before installing, check whether Nginx, Apache, Postfix or other services already occupy the ports:
sudo ss -tlpn | grep -E -w '25|80|110|143|443|465|587|993|995|4190'
An empty output is ideal. Mailcow ships with its own Nginx and certificate components, so for a first installation it is not recommended to layer Caddy or Nginx Proxy Manager on top. If an existing website occupies 80/443, adjust the bound ports according to Mailcow's official reverse proxy documentation rather than casually editing the Compose file.
Mailcow currently requires Docker Engine 24.0 or higher and Docker Compose 2.0 or higher. Install from the official Docker repository, not from outdated distribution packages that have not been updated for years.
Install the tools Mailcow needs:
sudo apt update
sudo apt install -y git openssl curl gawk coreutils grep jq
docker --version
docker compose version
For Docker logging, disk space and rollback principles in production, see the VPS Docker Compose production configuration checklist. Do not directly edit docker-compose.yml, which Mailcow maintains automatically; use the officially supported configuration method for custom overrides.
Clone the official repository and generate the configuration:
sudo -i
umask 0022
cd /opt
git clone https://github.com/mailcow/mailcow-dockerized
cd mailcow-dockerized
./generate_config.sh
When the script asks for the hostname, enter:
mail.example.com
Then check the key items in /opt/mailcow-dockerized/mailcow.conf:
MAILCOW_HOSTNAME=mail.example.com
TZ=Asia/Shanghai
SKIP_LETS_ENCRYPT=n
SKIP_CLAMD=n
SKIP_FTS=n
ENABLE_IPV6=false
Only set ENABLE_IPV6 to true if the VPS has correctly configured public IPv6, AAAA and IPv6 PTR. Forcing it on without a complete IPv6 mail path may cause some deliveries to go out from IPv6 that has never built reputation.
generate_config.sh generates random configuration such as database passwords; do not upload mailcow.conf to a public Git repository.
Start the services:
docker compose pull
docker compose up -d
docker compose ps
Mailcow includes multiple containers, and the first pull and initialization take a few minutes. Do not just check whether one container is Up; check all core services:
docker compose ps
docker compose logs --tail=100 nginx-mailcow
docker compose logs --tail=100 postfix-mailcow
docker compose logs --tail=100 dovecot-mailcow
docker compose logs --tail=100 acme-mailcow
First create the records that let the server come online and receive mail. Field names differ slightly between DNS consoles:
mail A 203.0.113.10
@ MX 10 mail.example.com.
autodiscover CNAME mail.example.com.
autoconfig CNAME mail.example.com.
@ TXT "v=spf1 mx a -all"
_dmarc TXT "v=DMARC1; p=none; rua=mailto:[email protected]"
Also set the following in the VPS provider console:
PTR 203.0.113.10 -> mail.example.com
Start with p=none to collect DMARC reports, and after confirming that all legitimate sources pass SPF or DKIM alignment, gradually change to quarantine or reject. If the domain also sends mail through third-party services, do not copy v=spf1 mx a -all verbatim; you must merge all legitimate sending sources into a single SPF record.
After DNS propagation, check:
dig +short A mail.example.com
dig +short MX example.com
dig +short TXT example.com
dig +short TXT _dmarc.example.com
dig +short -x 203.0.113.10
A and PTR must correspond to each other, and the MX target must be a hostname, not an IP.
Open in a browser:
https://mail.example.com/admin
The official initial account is admin and the default password is moohoo. Immediately after the first login:
- Change the administrator password;
- Enable two-factor authentication;
- Create a second controlled administrator as a recovery entry point;
- Confirm the TLS certificate has been issued and the browser shows no warnings;
- Do not allow ordinary mailbox accounts to have unnecessary admin privileges.
If the admin panel is already reachable from the public internet but you have not yet changed the default password, restrict access first before continuing configuration.
In the Mailcow admin panel, complete the following in order:
- Configuration → Mail Setup → Domains, add
example.com; - Create the needed mailboxes such as
[email protected]and[email protected]; - Generate a DKIM Key for the domain;
- Copy the full DKIM TXT value shown in the panel to DNS;
- After propagation, re-run the DNS check in the panel.
The DKIM record usually looks like:
dkim._domainkey TXT "v=DKIM1; k=rsa; p=这里使用后台生成的公钥"
Do not hand-craft the public key after p=, and do not put the private key in DNS. If the record is too long, the DNS provider may automatically split it into multiple strings; as long as the final TXT query can reassemble the full value, that is fine.
Docker-published ports go through the forwarding chain. Mailcow officially warns that writing only UFW/INPUT rules may fail to restrict container ports; restrictions need to be placed in DOCKER-USER or an external cloud firewall.
A mail server must at least allow the public ports that are genuinely needed. If the admin panel is only for team use, you can restrict source IPs in the upstream firewall; SMTP 25 and the mail protocol ports used by recipients must not be blocked by mistake.
After applying rules, actually test from another machine rather than only checking locally:
nc -vz mail.example.com 25
nc -vz mail.example.com 443
nc -vz mail.example.com 587
nc -vz mail.example.com 993
openssl s_client -starttls smtp -connect mail.example.com:25 \
-servername mail.example.com </dev/null
Cloud firewalls, host rules and Docker chains must all be checked together. Do not bulk-flush iptables rules in a remote SSH session.
Before formally migrating domain mailboxes, complete at least four rounds of testing:
Send mail from Gmail, Outlook or another external mailbox to [email protected], and confirm it arrives in SOGo and the IMAP client.
Send separately to Gmail, Outlook and another corporate mailbox, and check whether the mail lands in the inbox. Do not only send between mailboxes on the same domain, since that does not go through the real internet delivery path.
On the recipient side, view the raw message headers and confirm:
spf=pass
dkim=pass
dmarc=pass
All three passing still does not guarantee inbox placement; IP reputation, content, sending cadence and user feedback also affect the result.
cd /opt/mailcow-dockerized
docker compose logs --since=30m postfix-mailcow
docker compose logs --since=30m rspamd-mailcow
docker compose exec postfix-mailcow postqueue -p
When you encounter timeout, connection refused, blocked using or 550 5.7.x, keep the full peer response before handling it, and do not repeatedly resend and create more negative signals.
Mailcow officially provides helper-scripts/backup_and_restore.sh, which can back up mail, databases, encryption material, Redis, Rspamd and Postfix data. Do not copy the script elsewhere; run it directly in the repository:
sudo mkdir -p /opt/mailcow-backups
cd /opt/mailcow-dockerized
MAILCOW_BACKUP_LOCATION=/opt/mailcow-backups \
./helper-scripts/backup_and_restore.sh backup all --delete-days 7
After checking the generated backup directory and logs, set up Cron:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 3 * * * cd /opt/mailcow-dockerized && MAILCOW_BACKUP_LOCATION=/opt/mailcow-backups ./helper-scripts/backup_and_restore.sh backup all --delete-days 7 >> /var/log/mailcow-backup.log 2>&1
/opt/mailcow-backups is still on the same VPS, so it only protects against accidental deletion, not disk failure or account suspension. Sync the backups encrypted to another object storage or off-site server, and regularly restore using a blank test machine. You can reuse the verification, retention and restore acceptance methods from the VPS backup and restore drill guide.
The criteria for a successful restore include: the administrator can log in, historical mail and attachments are complete, domain and DKIM configuration are present, and sending and receiving work — not that the script exit code is 0.
Mailcow consists of multiple network services, and security updates cannot be delayed for long. The official repository provides update.sh; do not perform unplanned upgrades on each image yourself.
Upgrade process:
cd /opt/mailcow-dockerized
./update.sh --check
# 先执行完整备份并确认异地副本可读
./update.sh
docker compose ps
docker compose logs --tail=100 nginx-mailcow
docker compose logs --tail=100 postfix-mailcow
docker compose logs --tail=100 dovecot-mailcow
After upgrading, verify the admin panel, SOGo, IMAP login, SMTP submission, external sending and receiving, the queue, TLS and DKIM. Keep the stable branch in production and do not switch directly to nightly.
dig +short MX example.com
nc -vz mail.example.com 25
docker compose logs --tail=200 postfix-mailcow
First confirm the MX points to the correct hostname, the A record points to the current IP, and inbound 25 is open. Cloudflare orange cloud, old MX records and carrier blocking can all cause timeouts.
nc -vz gmail-smtp-in.l.google.com 25
docker compose exec postfix-mailcow postqueue -p
docker compose logs --tail=300 postfix-mailcow
If all external port 25 connections time out, contact the VPS provider first to confirm outbound blocking. Do not confuse client submission port 587 with server delivery port 25.
Check A/PTR forward and reverse, SPF, DKIM, DMARC, IP blacklists and the raw message headers. Do not suddenly send in bulk from a new IP; first build reputation gradually with genuine one-to-one business communication. For more complete error codes and delivery troubleshooting, see the mail deliverability guide linked earlier.
dig +short A mail.example.com
docker compose logs --tail=300 acme-mailcow
sudo ss -lntp | grep -E ':80|:443'
Confirm the domain resolves directly to the Mailcow VPS, 80/443 are reachable and not occupied by other services. If IPv6 is not correctly configured, delete the wrong AAAA and disable Mailcow IPv6.
free -h
docker stats --no-stream
docker compose ps
journalctl -k --since '1 hour ago' | grep -i -E 'oom|killed process'
With the default configuration below 6GiB of memory, upgrading the VPS is the priority. In an emergency you can set SKIP_CLAMD=y and SKIP_FTS=y according to the official instructions, but this disables virus scanning or full-text search and should not be packaged as a zero-cost optimization.
Prefer 993 IMAPS and 587 Submission, and confirm the username is the full email address. Check the time, certificate, password and Dovecot logs:
docker compose logs --since=30m dovecot-mailcow
openssl s_client -connect mail.example.com:993 \
-servername mail.example.com </dev/null
- The VPS allows mail services, and inbound/outbound TCP 25 have been tested
- Supported virtualization such as KVM is used, with the default configuration at least 6GiB of memory plus 1GiB Swap
- The A record for
mail.example.comand the public IP PTR match forward and reverse - MX, SPF, DKIM, DMARC, autodiscover and autoconfig have resolved
- DNS records are not proxied by a regular CDN
- The default administrator password has been changed and two-factor authentication enabled
- The TLS certificate is valid, and SMTP, IMAPS and Submission ports are reachable
- External receiving and sending to multiple services have both been tested successfully
- SPF, DKIM and DMARC all pass in real message headers
- Full backups have been copied off-site, and a blank-machine restore drill has been completed
- The update process, log locations and on-call contacts have been recorded
Mailcow lowers the barrier to installing components and day-to-day management, but it cannot solve IP reputation, DNS ownership and mail compliance for you. Checking port 25, PTR and IP history first, then deploying and migrating production mailboxes, is usually more time-efficient than installing first and then appealing everywhere afterward.