Your own mail server with Mailcow: setup from scratch
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 SMTP must be open. Many providers block it by default against spam. At netcup the default policy "netcup Mail block" (ports 25/465/587) does that - you simply take it off the server in the SCP, no support ticket required. How to do that is in netcup firewall setup. 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. 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: 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: sudo apt update && sudo apt install -y git jq Check that Docker and the Compose plugin are running: 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: 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: ./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: 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 themaster branch - that's the stable, recommended state.2 isnightly (testing),3 the deprecatedlegacy 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 settingSKIP_CLAMD=y inmailcow.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:/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. Answern and the generator aborts, leaving you to add the setting by hand. If you deliberately want to run without IPv6, setENABLE_IPV6=false inmailcow.conf later. Afterwards take a look at the generated mailcow.conf . The most important values are at the top: 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": 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: 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: 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: 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: 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: 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
Comments
No comments yet. Start the discussion.