Skip to content
Serverküche
Search

Loading search … (only available on the published site).

Applications Difficulty: Advanced

Your own mail server with Mailcow: setup from scratch

Set up Mailcow (dockerized) on your own server: Postfix, Dovecot, Rspamd and SOGo webmail under your domain – a complete mail server stack with HTTPS.

· 13 min read ·Duration: approx. 90 minutes
Table of contents

Your email is the backbone of your digital identity – password resets, invoices, contracts all run through it. Yet for most people it sits at a corporation in the US. With Mailcow you bring the complete mail server onto your own server: Postfix, Dovecot, spam filter and webmail, neatly packed in Docker. This tutorial gets the stack running – the tricky deliverability (SPF, DKIM, DMARC, reverse DNS) we handle afterwards in the deliverability recipe.

Running your own mail server is work – be honest with yourself

A mail server is not “set up once and forget”. From now on you’re responsible for deliverability, reputation, backups and updates. If your mails land in spam or the server fails, it affects your most important communication. If you don’t want to maintain this regularly, a reputable mail host is often the better choice – that’s no weakness but an honest trade-off. Whoever really wants control is in the right place here.

What are we building?

By the end, a complete mail server stack runs on your server, reachable as webmail at https://mail.YOUR_DOMAIN with a valid Let’s Encrypt certificate. Included are:

  • Postfix as the SMTP server (send/receive),
  • Dovecot for IMAP/POP3 (retrieval by mail programs),
  • Rspamd as a modern spam and signature filter,
  • SOGo as webmail including calendar and contacts (CalDAV/CardDAV),
  • an admin interface to create domains, mailboxes and aliases.

The whole thing comes as mailcow-dockerized – a ready-bundled Compose project of 18 containers that makes the notoriously complicated mail server setup manageable. We build on Debian 13 with Docker 29.6 and Compose v5.3; mailcow is a rolling release maintained through monthly tags (you update via update.sh, there’s no classic version number). We tested against the 2026-07b state with Postfix 3.10.12, Dovecot 2.3.21.1, Rspamd 4.1.4 and SOGo 5.12.10.

Mailcow needs its own server

Unlike most recipes here, mailcow does not run behind the shared Traefik reverse proxy. It brings its own web server and occupies ports 80/443 itself, plus the mail ports 25, 465, 587, 143, 993 (and more). So run mailcow on a dedicated server where nothing else needs these ports – not on the same host as your Traefik stack.

Prerequisites

  • A dedicated server with Debian 13 on which Docker is installed and that is only for mailcow (ports 80/443 and the mail ports free).
  • At least 6 GB RAM – the spam filter (Rspamd) and the optional virus scanner (ClamAV) need room. With less it gets sluggish or ClamAV has to go.
  • A domain whose DNS you control yourself. Without matching records (especially MX and reverse DNS), no other mail server accepts your mail.
  • Outbound port 25 must be open. Many providers block it by default against spam – at netcup you enable it via a support ticket. Without an open port 25 you can deliver no mail to other servers.
  • A PTR/reverse-DNS entry for the server IP, which you set in the provider panel (at netcup in the SCP). It must point to mail.YOUR_DOMAIN.

Mailcow is RAM-hungry, so we recommend at least the VPS 2000. What your specific need is, the server calculator estimates for you.

🍳 Recommendation Ad

VPS 2000 G12

8 vCores · 16 GB RAM · 512 GB NVMe

from €19.24/month

For mailcow including spam and virus filter it should be 8 GB RAM.

Go to netcup →

💶 5 € voucher for new netcup customers:36nc17844976032 (new customers only, no domains)

Step by step

Step 1: Create the DNS records

Before you install anything, set up the DNS entries – they need time to propagate, and mailcow fetches the TLS certificate for the hostname at startup. At your DNS provider (see connecting a domain to your server), create these entries. YOUR_DOMAIN is your mail domain (e.g. example.com), YOUR_SERVER_IPV4 the server’s IPv4 address:

Ausgabe
mail.YOUR_DOMAIN.   A      YOUR_SERVER_IPV4
mail.YOUR_DOMAIN.   AAAA   YOUR_SERVER_IPV6      (if you use IPv6)
YOUR_DOMAIN.        MX     10 mail.YOUR_DOMAIN.

The MX record tells other mail servers: “Mail for @YOUR_DOMAIN is accepted by mail.YOUR_DOMAIN.” The hostname mail.YOUR_DOMAIN is the FQDN of the mail server – not to be confused with the mail domain itself. The further records for deliverability (SPF, DKIM, DMARC) and the PTR entry we cover in detail in the follow-up tutorial on deliverability; the PTR entry should be set already now, though, because many servers otherwise don’t accept at all.

Tip

Set the TTL of the records low before the migration (e.g. 300 seconds). Then later corrections take effect quickly while you test. After the fine-tuning you can raise them again.

Step 2: Install prerequisites on the server

Log in via SSH to the hardened server. Mailcow’s config generator needs git and jq besides Docker – the latter is easily forgotten, and the generator otherwise aborts with Cannot find command 'jq'. Install both:

Terminal
sudo apt update && sudo apt install -y git jq

Check that Docker and the Compose plugin are running:

Terminal
docker --version && docker compose version

You should see two version lines – for us Docker version 29.6.1 and Docker Compose version v5.3.1. Mailcow requires at least Docker 24; below that the config generator aborts. If something’s missing, catch up on the Docker setup before continuing.

Step 3: Clone and configure Mailcow

Mailcow is delivered as a Git repository and not maintained via a single image tag. Clone it to /opt and change into it. The umask 0022 is important so the generated files get the right permissions:

Terminal
sudo su
umask 0022
cd /opt
git clone https://github.com/mailcow/mailcow-dockerized
cd mailcow-dockerized

Now you generate the central configuration file mailcow.conf with the interactive generator:

Terminal
./generate_config.sh

It first checks whether your server’s IP is on the Spamhaus bad ASN list – a useful early warning, because hardly any mail server accepts post from some networks:

Ausgabe
Detecting if your IP is listed on Spamhaus Bad ASN List...
Check completed! Your IP is clean

Then it asks you these questions in turn:

  • Mail server hostname (FQDN): Enter mail.YOUR_DOMAIN here – the server’s hostname, not your mail domain. The value must match the A record from step 1.
  • Timezone: usually already correctly pre-filled (e.g. Europe/Berlin) – confirm with Enter.
  • Choose the Branch with it’s number [1/2/3]: Choose 1 for the master branch – that’s the stable, recommended state. 2 is nightly (testing), 3 the deprecated legacy branch.

You only see the ClamAV question on small servers

The generator asks about the virus scanner (Do you want to disable ClamAV now?) only if the server has at most 2.5 GiB of RAM. On a machine with 6–8 GB it quietly sets SKIP_CLAMD=n, so ClamAV runs from the start. You can switch it off any time by setting SKIP_CLAMD=y in mailcow.conf and restarting the stack.

IPv6 hosts: Docker gets restarted

If the generator finds a working IPv6 connection (default route plus a reachable peer), Docker has to speak IPv6 too – otherwise mailcow could become an open relay. If /etc/docker/daemon.json is missing, it therefore asks:

Ausgabe
/etc/docker/daemon.json not found. Create it with IPv6 settings? [Y/n]

Confirm with Enter and it creates the file itself (on Docker ≥ 28 {"ipv6": true} is enough) and restarts Docker right away. This is exactly why mailcow belongs on its own server: that restart takes every other container on the host down with it. Answer n and the generator aborts, leaving you to add the setting by hand. If you deliberately want to run without IPv6, set ENABLE_IPV6=false in mailcow.conf later.

Afterwards take a look at the generated mailcow.conf. The most important values are at the top:

Terminal
grep -E "^(MAILCOW_HOSTNAME|TZ|HTTP_PORT|HTTPS_PORT|SKIP_CLAMD|ENABLE_IPV6)=" mailcow.conf

On our test server it looked like this – SKIP_CLAMD=n means “ClamAV is running”:

Ausgabe
MAILCOW_HOSTNAME=mail.YOUR_DOMAIN
HTTP_PORT=80
HTTPS_PORT=443
TZ=Europe/Berlin
SKIP_CLAMD=n
ENABLE_IPV6=true

The file also contains all port assignments. In the standard configuration mailcow claims these host ports:

Ausgabe
25    SMTP     (server-to-server delivery)
465   SMTPS    (send with implicit TLS)
587   Submission (send by mail programs, STARTTLS)
143   IMAP  /  993 IMAPS   (retrieval by mail programs)
110   POP3  /  995 POP3S
4190  Sieve    (server-side filter rules)
80    HTTP  /  443 HTTPS   (webmail + admin + ACME)

These ports must be free on the host – hence the dedicated server. If a reverse proxy already runs on 80/443 there, mailcow collides with it.

Step 4: Open the firewall

Your UFW firewall must let the mail and web ports through. Besides SSH, you allow:

Terminal
sudo ufw allow 25/tcp
sudo ufw allow 80,443/tcp
sudo ufw allow 465,587/tcp
sudo ufw allow 143,993/tcp
sudo ufw allow 110,995/tcp
sudo ufw allow 4190/tcp

If you additionally use the netcup firewall in the SCP, allow the same ports there too – otherwise the perimeter firewall blocks the traffic before it reaches the server.

Step 5: Start the stack

Now you download the images and start the stack. Expect about 4.5 GB on disk – the stack consists of 18 services, the biggest being SOGo, Dovecot and Postfix:

Terminal
docker compose pull
docker compose up -d

The first boot takes a few minutes – the database, search index and the individual services initialize. Watch the progress with:

Terminal
docker compose ps

The STATUS column shows Up … for every service; the containers with their own health check (such as unbound-mailcow and clamd-mailcow) additionally report (healthy). During this time, mailcow fetches the Let’s Encrypt certificate for mail.YOUR_DOMAIN in the background via its built-in ACME client. For that, the A record and port 80 must be reachable from outside – if that’s not the case, the acme-mailcow container stays in a retry loop and you see it in its logs (docker compose logs acme-mailcow).

Step 6: First login to the admin interface

Open https://mail.YOUR_DOMAIN in the browser. You land in the mailcow UI. Log in with the default credentials:

Ausgabe
User:      admin
Password:  moohoo

If the form shows “Sign in as user”, click “Sign in as admin” at the bottom – the login form on the home page otherwise logs you in as a mailbox user, not as an administrator.

Change the default password immediately

admin / moohoo is publicly known. Change the admin password immediately under System → Configuration → Access: in the Administrators table, click Edit next to the admin user. In the same tab, enable two-factor authentication (TOTP or WebAuthn) right away. As long as the default password applies, your mail server is wide open like a barn door.

After logging in you land in the mailcow dashboard with the overview of your services:

The mailcow dashboard after the first login with a system overview.
The mailcow dashboard right after login.

Step 7: Create a mail domain and first mailbox

In mailcow you separate domain (e.g. example.com) and mailbox (e.g. contact@example.com).

Via E-Mail → Configuration in the top navigation bar you manage domains and mailboxes in separate tabs:

The mail setup interface of mailcow with tabs for domains and mailboxes.
E-Mail → Configuration: manage domains and mailboxes.

  1. On the Domains tab, click Add domain and enter your domain YOUR_DOMAIN. The default values (mailbox and alias quotas) are fine for the start. Save with “Add domain and restart SOGo” at the bottom – not with “Add domain only”. Why, see the box below.

    The dialog for adding a mail domain in mailcow.
    Create a new mail domain.

  2. Switch to the Mailboxes tab and click Add mailbox – username, domain, full name and a strong password (password manager!). This mailbox can then log in to the webmail and via IMAP/SMTP.

    The dialog for creating a new mailbox in mailcow.
    Create a new mailbox.

Without a SOGo restart the webmail stays shut

SOGo reads the list of mail domains only when its container starts. If you create the first domain with “Add domain only”, SOGo logs No authentication sources defined - nobody will be able to login, and every webmail login ends on a bare Unauthorized page – even though password and mailbox are correct. Hence the second button; after the fact you get there via E-Mail → Restart SOGo. If a failed login has already been cached, restarting memcached as well helps (see “When things go wrong”).

After creating the domain, mailcow shows you the DKIM key and the recommended DNS records under the domain entry. You need those for deliverability – more on that shortly.

Step 8: Test the webmail

Open https://mail.YOUR_DOMAIN/SOGo/ (or click Apps → Webmail at the top right in the mailcow UI). If you aren’t signed in yet, you first land on the mailcow “User Login” mask – log in there with your full mail address and the mailbox password and you are passed straight through to the SOGo webmailer with inbox, calendar and contacts. A test mail from you to yourself should be in the inbox immediately – that proves that local delivery, IMAP and webmail work.

The real acid test, however, is sending to the outside and receiving from the outside – and that’s exactly where it’s decided whether your mails land in the recipient’s inbox or spam folder. That depends on SPF, DKIM, DMARC and the PTR entry and is substantial enough for its own recipe.

Note

If you want to check your deliverability right away, send a mail after the setup to a test service like mail-tester.com – it rates SPF, DKIM, DMARC, reverse DNS and block lists. Below 10/10 there’s room for improvement; how you reach the full score is the topic of the deliverability tutorial.

When things go wrong

generate_config.sh aborts with Cannot find command 'jq'. the jq package is missing. sudo apt install -y jq and start the generator again.

The generator reports “User declined to create daemon.json” and aborts. On hosts with active IPv6 you answered the question about the IPv6-capable Docker configuration with n – without it mailcow won’t run there, and an open relay looms. Create /etc/docker/daemon.json containing {"ipv6": true} (Docker 28 and newer), restart Docker with sudo systemctl restart docker and run ./generate_config.sh again – or simply confirm the question with Enter on the second attempt, then the generator does both itself.

The webmail only answers Unauthorized. SOGo doesn’t know your mail domain: it reads the domain list exclusively at startup and wasn’t restarted after the domain was created – its logs say No authentication sources defined - nobody will be able to login. Restart SOGo via E-Mail → Restart SOGo. If that doesn’t help, a failed login is still stuck in the cache: docker compose restart memcached-mailcow sogo-mailcow.

No certificate, acme-mailcow keeps restarting. Let’s Encrypt can’t reach your server on port 80, or the A record of mail.YOUR_DOMAIN is wrong. check with docker compose logs acme-mailcow whether the hostname and IP match, and that port 80 is open through UFW and the netcup firewall.

Mails to the outside aren’t delivered, logs show timeouts on port 25. Your provider blocks outbound SMTP traffic on port 25 (common as spam protection). have port 25 enabled outbound at the provider (at netcup via a support ticket). Without it you can’t send mail to other servers – receiving and local delivery still work.

Ports 80/443 can’t be bound (address already in use). A web server or reverse proxy (e.g. Traefik) already runs on the host. mailcow belongs on its own server. At a pinch, HTTP_PORT/HTTPS_PORT in mailcow.conf can be bent and mailcow put behind a proxy – but that’s advanced and no solution for the mail ports anyway.

Containers start slowly or are killed by the kernel (OOM). Too little RAM. go to at least 6–8 GB or disable ClamAV in mailcow.conf (SKIP_CLAMD=y) – the virus scanner is the biggest memory eater.

Maintenance & backups

A mail server lives off maintenance – plan it firmly.

Updates. Mailcow brings its own update script. Run it regularly (about monthly); it updates the Compose file and all images to the latest state:

Terminal
cd /opt/mailcow-dockerized
sudo ./update.sh

The script automatically makes a backup of the configuration before the update and restarts the stack. Read the release notes shown – on bigger jumps, manual steps are occasionally listed there.

Backups. Your mails, the database and the crypto keys (DKIM!) live in Docker volumes. Mailcow brings a ready-made backup script for this:

Terminal
cd /opt/mailcow-dockerized/helper-scripts
sudo BACKUP_LOCATION=/opt/mailcow-backups ./backup_and_restore.sh backup all

Then back up the target directory away from the server – cleanest encrypted with Restic. Especially important are the DKIM keys: if they’re lost, your signatures break after a restore until you reset the DNS records. A backup you’ve never restored is just a hopeful guess – test the restoration once on a test system.

Keep an eye on reputation. Occasionally check whether your server IP has landed on a block list (e.g. via one of the common block-list checkers) and take a look at the Rspamd dashboard in the mailcow UI now and then. A hacked or misconfigured mailbox that sends spam quickly ruins the reputation of your entire domain.

Last updated: Aug 25, 2026

What's next?

You might also like