Skip to content

Command Palette

Search for a command to run...

How to Deploy Mailcow on a VPS

Updated Jul 2026

verified on Ubuntu 26.04 · Jul 2026

Self-host Mailcow — a full mail suite bundling Postfix, Dovecot, SOGo webmail, Rspamd, and ClamAV behind one admin UI — on your own VPS with Docker Compose, as a self-hosted alternative to Google Workspace or Microsoft 365.

Before you start
  • A VPS with at least 6 GB RAM — Mailcow's hard minimum (plus 1 GB swap). 8 GB+ is the realistic recommendation for anything beyond a solo test instance.
  • A fresh Ubuntu 26.04 server with root/sudo SSH access, Docker + Compose installed
  • A domain you control, able to set MX, SPF, DKIM, DMARC, and PTR records — required for real mail delivery, not for the install itself
  • Outbound port 25 unblocked by your VPS provider if you plan to send mail directly from this box — many providers block it by default; see Troubleshooting

What Mailcow is

Mailcow ("mailcow: dockerized") is a complete, self-hosted mail suite — not just an SMTP relay, but the whole stack a real mailbox provider runs: Postfix for sending and receiving mail, Dovecot for IMAP/POP3, SOGo for webmail and CalDAV/CardDAV, ActiveSync support for mobile mail clients, Rspamd for spam filtering, ClamAV for antivirus scanning, and an admin UI that ties it all together — domains, mailboxes, aliases, quotas, DKIM keys. It ships as roughly 20 Docker containers orchestrated by one Compose file, and it's licensed GPL-3.0.

The appeal is the same one that draws people to self-hosted infrastructure generally: cost and control at scale. Google Workspace and Microsoft 365 bill per mailbox per month — cheap for five people, expensive for fifty. A single well-sized VPS running Mailcow handles a meaningful number of mailboxes for the price of one Workspace seat, and your mail — the actual message content, not just metadata — never leaves infrastructure you control.

The honest trade-off: email is the least forgiving thing you can self-host. A misconfigured SPF record doesn't crash anything, it just means your mail silently lands in spam folders (or gets rejected outright) with no error you'll notice until someone complains. This guide gets Mailcow installed and booted correctly, which is a real milestone — but reliable delivery to Gmail, Outlook, and everyone else depends on DNS work covered in its own section below, and that part is on you.

Server sizing — 6 GB is the floor, not a comfortable number

Mailcow is the heaviest thing in this catalog, and it's upfront about it: the project's own minimum is 6 GB RAM plus 1 GB swap, and that's for a default config running a handful of mailboxes — it is not headroom, it is what ~20 containers actually need to start and stay up. Undersize this and containers get OOM-killed under normal load, not just spikes.

8 GB RAM is the realistic recommendation for 5–10 users, and the project suggests 16 GB if you're layering on 15+ mobile devices doing ActiveSync alongside ~50 concurrent IMAP connections. Two containers account for most of the appetite: ClamAV (antivirus signature database, loaded fully into memory) and full-text search (indexing message bodies for webmail search) — both can be disabled in mailcow.conf if you're truly resource-constrained, at the cost of losing virus scanning or webmail search respectively. Don't do that as your default plan; do it only if you've confirmed you need to.

Give the disk real room too — 40–60 GB is a sane starting point once you account for container images, the MySQL database, and actual mail storage that only grows. A Hetzner CX32 (4 vCPU / 8 GB) or Kamatera's equivalent 8 GB tier is the right starting point; don't try this on anything smaller.

Prepare the server

This guide assumes you've already worked through Docker & Compose on Ubuntu — a fresh Ubuntu 26.04 box, a non-root deploy user, Docker Engine and the Compose plugin installed, and ufw allowing SSH plus 80 and 443. If you haven't, do that first.

Mailcow is different from most guides in this catalog in one important way: it is its own reverse proxy and its own TLS terminator. There's no Caddy or Nginx Proxy Manager in front of it — the nginx-mailcow container binds directly to 80 and 443 on the host, and an acme-mailcow container issues and renews its own Let's Encrypt certificate. On top of that, Postfix and Dovecot need to be reachable directly from the internet on the standard mail ports, because mail clients and other mail servers connect to them straight, not through a proxy. So the base guide's firewall (SSH, 80, 443) isn't enough here — open the mail ports too:

ufw allow 25/tcp
ufw allow 465/tcp
ufw allow 587/tcp
ufw allow 110/tcp
ufw allow 143/tcp
ufw allow 993/tcp
ufw allow 995/tcp
ufw allow 4190/tcp
ufw status

That's SMTP (25), SMTPS (465), submission (587), POP3/POP3S (110/995), IMAP/IMAPS (143/993), and ManageSieve (4190) — the full set a mail server needs open, in addition to 80/443 for the web UI, webmail, and ACME.

Install Mailcow

Mailcow's own installer needs a short list of system tools beyond Docker — make sure they're present:

apt-get update -y
apt-get install -y git curl jq gawk

Clone the repository into /opt, the path the project's own docs use:

cd /opt
git clone https://github.com/mailcow/mailcow-dockerized
cd /opt/mailcow-dockerized

Now generate mailcow.conf. The official flow runs ./generate_config.sh as an interactive script — it prompts for the mail server hostname, timezone, and which release branch to track, one read at a time. But each of those prompts is guarded by a while [ -z "$VAR" ] loop, which means if the variable is already set in the environment before the script runs, the loop condition is false and the prompt is skipped entirely. Exporting the answers ahead of time makes the whole thing non-interactive:

SERVER_IP="203.0.113.10"   # replace with your VPS's public IP
export MAILCOW_HOSTNAME="mail.${SERVER_IP}.sslip.io"
export MAILCOW_TZ="UTC"
export MAILCOW_BRANCH="master"
export SKIP_CLAMD="n"
export FORCE=1

cd /opt/mailcow-dockerized
./generate_config.sh

A note on each of those, since they map straight onto prompts you'd otherwise see:

  • MAILCOW_HOSTNAME is the mail server's hostname — not your mail domain. With no real domain yet, <subdomain>.<IP>.sslip.io resolves straight back to your server's IP with zero DNS setup, which is exactly what this guide uses to get a working install. For real mail, you'll point this at a proper FQDN (see DNS below).
  • MAILCOW_TZ sets the container timezone; the script otherwise tries to auto-detect it from /etc/timezone and still prompts to confirm.
  • MAILCOW_BRANCH picks master (stable), nightly, or legacy — set it to master unless you specifically need one of the others.
  • SKIP_CLAMD would normally only prompt if the box has ≤2.5 GB RAM; setting it explicitly avoids depending on that check.
  • FORCE=1 covers a separate, IPv6-specific prompt the script can trigger if it wants to patch Docker's daemon.json for IPv6 support — it makes that step auto-confirm instead of asking.

The script writes mailcow.conf (symlinked to .env, which already ships in the repo) and does not start anything itself — pull the images and bring the stack up:

cd /opt/mailcow-dockerized
docker compose pull
docker compose up -d

That pulls roughly 20 images, so it takes a while on first run. Confirm the stack is up and check the web-facing container specifically:

docker compose ps
docker compose logs --tail 40 nginx-mailcow
docker compose logs -f

HTTPS + domain

Because Mailcow terminates its own TLS, there's no separate Caddy setup here — once MAILCOW_HOSTNAME resolves to the server's public IP and ports 80/443 are reachable, acme-mailcow requests and installs a Let's Encrypt certificate for that hostname automatically. With an sslip.io hostname the DNS resolution is already correct by construction, so issuance starts right away — but nginx serves on a self-signed cert first and swaps in the Let's Encrypt one a few minutes later once acme-mailcow finishes. Confirm the admin UI is answering over HTTPS from the server itself (the -k tolerates that initial self-signed cert; the trusted cert lands on its own shortly after):

for i in $(seq 1 12); do
  code=$(curl -sk -o /dev/null -w '%{http_code}' "https://${MAILCOW_HOSTNAME}/")
  [ "$code" != "000" ] && { echo "admin UI up: HTTP $code"; break; }
  sleep 5
done

A 200 (or a redirect code) means nginx is serving the admin UI over HTTPS. If you already run another site on 80/443 on this box, Mailcow's docs cover putting it behind an external reverse proxy instead — that's a deliberate deviation from this guide's default setup and out of scope here.

First-run setup

Load https://<your-hostname>/admin in a browser — for this guide, that's https://mail.<SERVER_IP>.sslip.io/admin.

Log in with the default credentials and change them immediately: username admin, password moohoo. This default is published in Mailcow's own generated config file, so treat it as public knowledge from the moment the stack is up — go to Configuration → Access → Admin Details and set a strong password before doing anything else.

Once you're in as admin:

  1. Add a mail domain under Configuration → Domains — this is the domain mail will actually be sent and received for (distinct from MAILCOW_HOSTNAME, the server's own hostname).
  2. Add a mailbox on that domain and set a password for it.
  3. Log into webmail at https://<your-hostname>/SOGo/ with that mailbox's credentials, and send a test message to another mailbox on the same server to confirm the internal Postfix → Dovecot path works end to end.
  4. Check the DKIM key was generated under Configuration → ARC/DKIM keys for the domain you added — Mailcow generates this automatically per domain, and you'll need its public key value for DNS in the next section.

Internal mail flowing between two mailboxes on the same server confirms the install itself is healthy. It does not confirm mail to and from the outside world works — that depends entirely on the DNS work below.

DNS & deliverability — not covered by install verification

This section is the reader's setup, and it is not something a fresh test VPS can verify. Getting a mail server to actually deliver to Gmail, Outlook, and everyone else depends on DNS records and sender reputation that only exist once you're running on a real domain with a stable IP — a throwaway verification box can confirm Mailcow boots, not that mail sent from it arrives. Treat everything below as required homework before you rely on this server for real mail:

  • MX record on your mail domain, pointing at MAILCOW_HOSTNAME (the server hostname, not the domain itself): example.com. MX 10 mail.example.com.
  • SPF (TXT on the domain): v=spf1 mx -all is a reasonable starting point if this server is the only thing that sends mail for the domain.
  • DKIM — copy the public key Mailcow generated (Configuration → ARC/DKIM keys) into a TXT record at the selector name it shows you (typically dkim._domainkey.example.com).
  • DMARC (TXT at _dmarc.example.com): start permissive while you confirm delivery, e.g. v=DMARC1; p=none; rua=mailto:you@example.com, then tighten to p=quarantine or p=reject once SPF/DKIM are confirmed passing.
  • PTR / reverse DNS — your VPS provider, not your DNS provider, controls this. Ask them to set the server's IP to resolve back to MAILCOW_HOSTNAME. A missing or mismatched PTR record is one of the most common reasons outbound mail lands in spam.
  • Port 25 outbound — many VPS providers block it by default on new accounts specifically to fight spam; you'll need to request it be unblocked (see Troubleshooting). Without it, this server cannot deliver mail directly to other servers at all.

None of this is verified by a successful install — a boot-verified Mailcow and a deliverable Mailcow are two different milestones, and only the first one is what this guide's verification checks.

Backups

Mailcow ships its own backup tool, and it's the right one to use — it knows which volumes and databases make up a complete instance:

export MAILCOW_BACKUP_LOCATION=/opt/mailcow-backups
mkdir -p /opt/mailcow-backups
cd /opt/mailcow-dockerized
./helper-scripts/backup_and_restore.sh backup all

That backs up MySQL, mail storage (vmail), Redis, Rspamd data, Postfix data, and the encryption keys (crypt) into timestamped folders under MAILCOW_BACKUP_LOCATION. The crypt volume is the one to treat like a root password — it holds the encryption keys for stored mail, and a MySQL/vmail backup restored without the matching crypt data is unreadable.

A backup that only lives on the same server dies with the server — get these archives to an S3-compatible bucket or another machine on a schedule, and periodically test a restore (backup_and_restore.sh restore) into a throwaway stack. An untested backup is a guess.

Upgrades

Mailcow moves fast, and — being ~20 coupled containers — an upgrade touches a lot of moving parts at once. Use the project's own update script rather than a manual docker compose pull:

cd /opt/mailcow-dockerized
./update.sh --force

--force skips the script's confirmation prompts, which is what makes it safe to run unattended here — only reach for that on a box where you've already read the release notes, since it stops every container, pulls new images, and applies any pending database migrations automatically. That's exactly why a current backup (above) before upgrading is non-negotiable: a migration changes your data in place, and there's no clean way back to an older version once it's run.

Troubleshooting

Outbound mail is rejected or never arrives, and Postfix logs show a connection timeout to port 25 on the receiving end. Your VPS provider is very likely blocking outbound port 25 by default — this is standard anti-spam policy at Hetzner, DigitalOcean, Kamatera, and most others for new accounts. Open a support ticket asking for it to be unblocked; most providers do this within a day once you explain the use case, some require a form.

Mail delivers but lands in spam. Check SPF/DKIM/DMARC are actually passing (mail clients show this in message headers, or use an external mail-tester tool), and confirm your PTR record resolves to MAILCOW_HOSTNAME. A fresh, low-volume sending IP also just has no reputation yet — this improves over the first weeks of legitimate sending volume; there's no shortcut around it.

docker compose ps shows a container repeatedly restarting. Almost always insufficient RAM — check docker stats and free -h against the 6 GB minimum / 8 GB recommended sizing above. ClamAV and full-text search are the two heaviest containers; if you're genuinely resource-constrained, SKIP_CLAMD=y in mailcow.conf and a docker compose up -d restart buys headroom at the cost of virus scanning.

The admin UI or webmail won't load over HTTPS. Confirm MAILCOW_HOSTNAME in mailcow.conf matches the hostname you're browsing to exactly, and that docker compose logs --tail 40 acme-mailcow shows a certificate was actually issued — a DNS mismatch at the time acme-mailcow first ran is the most common cause of a missing cert.

generate_config.sh still prompts despite the exported variables. Confirm you exported each variable (not just set it) in the same shell that runs the script, and that you're running it from inside /opt/mailcow-dockerized — the script refuses to run anywhere else.

Verification + next steps

You're done with the install when: docker compose ps shows every container healthy or running, https://<your-hostname>/admin loads over HTTPS with a valid certificate, you've logged in and changed the default admin password, a mailbox exists and can send/receive mail to another mailbox on the same server via webmail, and a DKIM key has been generated for your domain.

You're done with deliverability — a separate, later milestone — only once MX, SPF, DKIM, DMARC, and PTR are all in place on a real domain, outbound port 25 is confirmed open, and a test message to an external mailbox (Gmail, for instance) both arrives and lands outside spam. Budget real time for that second part; it's DNS propagation and sender-reputation building, not a configuration step you can rush.

Given the RAM floor, size up front rather than resizing later — a Hetzner CX32 or Kamatera's 8 GB tier is the sensible starting point for Mailcow specifically, even though smaller boxes work fine for lighter self-hosted apps. See Best VPS for self-hosting for the ranked picks.

Next steps

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.