Mailcow is a powerful and flexible open-source mail server suite that allows you to manage your email communications securely and efficiently. With Mailcow, you can create multiple email domains and accounts, manage users, and leverage features like spam filtering and encryption, giving you full control over your email infrastructure. This guide will show you how to set up Mailcow using Docker Compose behind an existing Traefik reverse proxy.

Changelog

DateChange
2025-10-03Major Revision: Corrected certdumper command, simplified Traefik labels, and fixed critical mailcow.conf networking settings to resolve 404 errors. Enhanced troubleshooting for network conflicts.
2025-09-18Initial Version: First version of this guide was created.

1. Prerequisites

This guide assumes you have a fully functional server environment with the following components already set up:

  • Traefik v3 and CrowdSec with Docker Compose: This is the foundation for our reverse proxy, security, and certificate management.
  • Docker and Docker Compose: Must be installed on your server.
  • Required Tools: You will need git and jq. Install them with:
    sudo apt update && sudo apt install -y git jq
    
  • sudo or root access.

2. Directory Structure

First, create a dedicated directory for your Mailcow installation.

ℹ️ PATH CONSISTENCY

This guide uses /opt/containers/mailcow/ as the primary directory for the Mailcow stack. The Traefik stack is assumed to be in /opt/containers/traefik-stack/, consistent with our Traefik v3 tutorial. If your paths differ, be sure to adjust them in all configuration files and commands throughout this guide.

sudo mkdir -p /opt/containers/mailcow

3. Clone the Mailcow Repository

Clone the latest version of the Mailcow Dockerized project from GitHub into the directory you just created.

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

4. Generate the Configuration File

Navigate into the new directory and run the configuration generation script.

cd /opt/containers/mailcow
./generate_config.sh

You will be prompted for the following information:

  • Mail server hostname (FQDN): Enter the fully qualified domain name for your mail server, for example, mail.your-domain.com.
  • Timezone: Press Enter to accept the default or provide your own.
  • Available Branches: Press 1 to select the master branch.

5. Configure for Traefik Integration

To make Mailcow work with our external Traefik instance, we need to create an override file and modify the main configuration.

5.1. Create docker-compose.override.yml

This file contains all our customizations, telling Mailcow to use the external proxy network and defining labels for Traefik to route traffic correctly. It also sets up a certdumper service, which is crucial for sharing Traefik’s Let’s Encrypt certificates with Mailcow’s services (Postfix and Dovecot).

ℹ️ HOW THE CERTDUMPER WORKS

The certdumper is essential. It extracts certificates from Traefik’s acme.json and saves them as .pem files that Postfix and Dovecot can read. The command --restart-containers mailcowdockerized-postfix-mailcow-1,... is critical; it restarts the mail services by their full Docker container name after a certificate is updated, ensuring the new certificate is loaded immediately.

Create the file:

sudo tee /opt/containers/mailcow/docker-compose.override.yml > /dev/null << 'EOF'
networks:
  proxy:
    name: proxy
    external: true

services:
  certdumper:
    image: ghcr.io/kereis/traefik-certs-dumper:latest
    restart: unless-stopped
    network_mode: none
    # Correct command to restart containers by their full name. Mailcow's project name is "mailcowdockerized".
    command: --restart-containers mailcowdockerized-postfix-mailcow-1,mailcowdockerized-dovecot-mailcow-1
    volumes:
      # Adjust this path to match YOUR Traefik stack's certificate location
      - /opt/containers/traefik-stack/traefik/certs:/traefik:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/assets/ssl:/output:rw
    environment:
      DOMAIN: ${MAILCOW_HOSTNAME}
      # Adjust this to match YOUR Traefik acme file name
      ACME_FILE_PATH: "/traefik/acme.json"
    healthcheck:
      test: ["CMD", "/usr/bin/healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 5

  nginx-mailcow:
    # We use !reset to completely remove the default port bindings set by Mailcow.
    ports: !reset
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"

      # HTTPS Router (Secure)
      # The HTTP to HTTPS redirect is handled globally by our Traefik setup, so we only need the secure router.
      - "traefik.http.routers.nginx-mailcow-secure.entrypoints=websecure"
      - "traefik.http.routers.nginx-mailcow-secure.rule=Host(`mail.your-domain.com`) || Host(`autodiscover.your-domain.com`) || Host(`autoconfig.your-domain.com`) || Host(`mta-sts.your-domain.com`)"
      - "traefik.http.routers.nginx-mailcow-secure.tls=true"
      - "traefik.http.routers.nginx-mailcow-secure.tls.certresolver=tls_resolver"
      - "traefik.http.routers.nginx-mailcow-secure.service=nginx-mailcow"
      - "traefik.http.routers.nginx-mailcow-secure.middlewares=security-headers@file,crowdsec-bouncer@docker"

      # Service Definition: Point to Mailcow's internal Nginx port.
      # This must be 80, as we are NOT changing the internal ports in mailcow.conf
      - "traefik.http.services.nginx-mailcow.loadbalancer.server.port=80"
EOF
ℹ️ CROWDSEC BOUNCER SCOPE

The crowdsec-bouncer@docker middleware only protects the HTTP/HTTPS endpoints routed through Traefik (like the Mailcow UI and SOGo). It does not protect mail services like SMTP, IMAP, or POP3, as their traffic does not pass through the Traefik router. To harden these mail protocols, you need to configure CrowdSec to parse their log files (e.g., using the crowdsecurity/postfix and crowdsecurity/dovecot collections on the host).

⚠️ IMPORTANT

You must replace your-domain.com with your actual domain name. You can do this manually or with the following command:

sudo sed -i "s/your-domain.com/your-actual-domain.com/g" docker-compose.override.yml

5.2. Adjust mailcow.conf

Next, edit the main configuration file. There are only two changes needed here.

sudo nano /opt/containers/mailcow/mailcow.conf```

<!-- Infobox Shortcode - Retro-kompatibel mit Browsern der 90er Jahre -->
<!-- Verwendung: infobox(type="info", title="Titel") mit Inhalt der Box -->
<!-- Types: info, warning, tip, note -->


<table class="retro-infobox" width="100%" cellspacing="0" cellpadding="10" border="1">
    <tr>
        <td bgcolor="#eeeeee" style="color: #000000; font-weight: bold;">
            ℹ️ 
            CRITICAL: DO NOT CHANGE PORTS OR BINDINGS!
        </td>
    </tr>
    <tr>
        <td bgcolor="#000000" style="color: #ffffff;">
            <p>Do <strong>not</strong> change <code>HTTP_PORT</code>, <code>HTTPS_PORT</code>, <code>HTTP_BIND</code>, or <code>HTTPS_BIND</code>. Leaving them at their default values is crucial. Changing <code>HTTP_BIND</code> to <code>127.0.0.1</code> will prevent Traefik from reaching the Mailcow web UI, resulting in a <strong>404 Not Found error</strong>. The internal ports do not conflict with Traefik because they are inside the Docker network, not on the host.</p>

        </td>
    </tr>
</table>


1.  **Disable Mailcow's Let's Encrypt:** Traefik is responsible for all certificate management. This is the most important change.
    ```ini
    # Find this line:
    SKIP_LETS_ENCRYPT=n
    # Change it to:
    SKIP_LETS_ENCRYPT=y
    ```

2.  **Add SAN for Internal TLS:** This helps internal services correctly recognize the hostname on the certificate provided by the `certdumper`.
    ```ini
    # Find this line (it may be commented out):
    # ADDITIONAL_SAN=
    # Change it to:
    ADDITIONAL_SAN=${MAILCOW_HOSTNAME}
    ```

Save and close the file. No other changes are needed in this file.

### 5.3. Activate MTA-STS

MTA-STS (Mail Transfer Agent-Strict Transport Security) is a security standard that helps prevent man-in-the-middle attacks by ensuring emails are transmitted over secure TLS connections.

<!-- Infobox Shortcode - Retro-kompatibel mit Browsern der 90er Jahre -->
<!-- Verwendung: infobox(type="info", title="Titel") mit Inhalt der Box -->
<!-- Types: info, warning, tip, note -->


<table class="retro-infobox" width="100%" cellspacing="0" cellpadding="10" border="1">
    <tr>
        <td bgcolor="#ccffcc" style="color: #000000; font-weight: bold;">
            💡 
            EASIER ALTERNATIVE: USE THE MAILCOW UI
        </td>
    </tr>
    <tr>
        <td bgcolor="#000000" style="color: #ffffff;">
            <p>Instead of creating the policy file by hand, you can navigate to <strong>System -&gt; Configuration -&gt; MTA-STS</strong> in the Mailcow UI after setup. The UI provides a generator that creates the correct policy and DNS records for you, which is less error-prone. The manual method below is still valid if you prefer it.</p>

        </td>
    </tr>
</table>


If you choose the manual route, create the required directory:
```bash
sudo mkdir -p /opt/containers/mailcow/data/web/.well-known/

Now, create the policy file with a simple enforce policy. This file will be served by Mailcow’s web server.

sudo tee /opt/containers/mailcow/data/web/.well-known/mta-sts.txt > /dev/null << 'EOF'
version: STSv1
mode: enforce
max_age: 15552000
mx: mail.your-domain.com
EOF
⚠️ IMPORTANT

You must replace your-domain.com with your actual domain name in the file above. You can do this with the following command:

sudo sed -i "s/your-domain.com/your-actual-domain.com/g" /opt/containers/mailcow/data/web/.well-known/mta-sts.txt

6. Launch Mailcow

You can now start the Mailcow stack.

cd /opt/containers/mailcow
docker compose up -d
💡 TROUBLESHOOTING TIPS
  • Pool Overlaps: If you see an error like Pool overlaps with other one on this address space, another Docker network is using Mailcow’s default IP range. You can fix this permanently by editing mailcow.conf and setting IPV4_NETWORK to an unused subnet prefix (e.g., IPV4_NETWORK=172.25.1). Mailcow will correctly append .0/24 to create the network.
  • RAM Usage: On a VPS with limited memory, you can disable resource-intensive services by setting SKIP_FTS=y (disables full-text search in Solr) or SKIP_CLAMD=y (disables the ClamAV antivirus engine) in mailcow.conf. This reduces security and functionality, so use it with caution.
  • IPv6 Issues: Some Mailcow versions had startup issues when ENABLE_IPV6=y was set. If you face problems, check the official Mailcow blog for release notes and patches.

7. DNS Configuration

Correct DNS setup is critical for a mail server to function.

7.1. Reverse DNS (PTR Record)

Your server’s Reverse DNS (PTR) record for both IPv4 and IPv6 must match the hostname you configured in mailcow.conf (mail.your-domain.com). This is usually set in your server provider’s control panel and is essential for not being marked as spam, especially by major providers like Gmail.

7.2. DNS Records

⚠️ USING CLOUDFLARE?

If you are using Cloudflare for your DNS, all mail-related records (including mail, imap, smtp, autodiscover, autoconfig, and mta-sts) must be set to “DNS only” (grey cloud). Cloudflare’s proxy (orange cloud) only supports web protocols like HTTP/HTTPS and will break mail services. For more details, see the official Cloudflare documentation.

In your domain’s DNS management panel, add the following records. We start with a non-restrictive DMARC policy (p=none) to prevent legitimate emails from being rejected during the initial setup.

⚠️ GMAIL & YAHOO SENDER REQUIREMENTS (2024)

As of 2024, major providers like Gmail and Yahoo enforce stricter sender policies:

  • All Senders: Must have either SPF or DKIM configured.
  • Bulk Senders (5,000+ emails/day): Must have SPF, DKIM, and a DMARC policy. They also require one-click unsubscribe links and must maintain a low spam complaint rate.

Failing to meet these requirements will result in your emails being rejected. For more details, see the official announcements from Google and Yahoo.

ℹ️ DMARC STAGED ROLLOUT

Starting with p=none is the safest approach. This allows you to monitor email traffic via the reports sent to your rua address without affecting mail delivery.

  1. Monitor: Keep p=none for a few weeks and analyze the reports to ensure all legitimate sending sources are correctly authenticated with SPF and DKIM.
  2. Quarantine: Once you are confident, switch to p=quarantine. This tells receiving servers to treat failing emails with suspicion (e.g., by sending them to the spam folder).
  3. Reject: After another monitoring period, you can move to the final policy, p=reject, which instructs receivers to block emails that fail DMARC checks.
NameTypeValue
mailAyour-server-ip
mailAAAAyour-server-ipv6
autodiscoverCNAMEmail.your-domain.com.
autoconfigCNAMEmail.your-domain.com.
imapCNAMEmail.your-domain.com.
pop3CNAMEmail.your-domain.com.
smtpCNAMEmail.your-domain.com.
@MX 10mail.your-domain.com.
SPF
@TXTv=spf1 mx a -all

💡 SPF FINE-TUNING

The v=spf1 mx a -all record is a safe default. The a part authorizes the server’s main IP address (from the A record) to send mail. If you only send mail from Mailcow, you can make this slightly stricter by using v=spf1 mx -all, which only authorizes servers listed in your MX records.

| DMARC | | | | _dmarc | TXT | v=DMARC1; p=none; rua=mailto:admin@your-domain.com | | MTA-STS | | | | mta-sts | A | your-server-ip | | mta-sts | AAAA | your-server-ipv6 | | _mta-sts | TXT | v=STSv1; id=2025091801 | | _smtp._tls | TXT | v=TLSRPTv1; rua=mailto:admin@your-domain.com | | Service Records | | | | _autodiscover._tcp | SRV | 0 1 443 mail.your-domain.com. | | _caldavs._tcp | SRV | 0 1 443 mail.your-domain.com. | | _caldavs._tcp | TXT | "path=/SOGo/dav/" | | _carddavs._tcp | SRV | 0 1 443 mail.your-domain.com. | | _carddavs._tcp | TXT | "path=/SOGo/dav/" | | _imaps._tcp | SRV | 0 1 993 mail.your-domain.com. | | _pop3s._tcp | SRV | 0 1 995 mail.your-domain.com. | | _submission._tcp | SRV | 0 1 587 mail.your-domain.com. | | _smtps._tcp | SRV | 0 1 465 mail.your-domain.com. |

A DKIM record will be added later after it’s generated by Mailcow.

ℹ️ UNUSED SRV RECORDS

This guide only lists SRV records for encrypted services (IMAPS, POP3S, etc.). If you decide not to offer certain services (e.g., POP3), you should also omit their corresponding SRV records to prevent clients from attempting to connect to them.

ℹ️ DNS RECORD NOTES
  • A vs. CNAME for mta-sts: We use A/AAAA records for mta-sts pointing directly to the server’s IP. While a CNAME pointing to mail.your-domain.com is also valid, using A/AAAA records avoids potential edge cases with some DNS resolvers.
  • Trailing Dots: Note the trailing dot (.) at the end of hostnames in MX and SRV records (e.g., mail.your-domain.com.). This signifies that the name is fully qualified. Some DNS providers add this automatically, while others require you to add it manually.

8. Firewall Configuration

Your firewall must allow traffic on several ports for email services to be reachable.

PortServiceProtocol
25SMTPTCP
587SubmissionTCP
465SMTPSTCP
143IMAPTCP
993IMAPSTCP
110POP3TCP
995POP3STCP
4190ManageSieveTCP
💡 PROTOCOL HYGIENE

For better security, consider disabling protocols you don’t need. For example, if all your clients use IMAP, you can keep the POP3 ports (110, 995) closed. It is also best practice to enforce encrypted connections, favoring Submission (587) and IMAPS (993) over their unencrypted counterparts.

ℹ️ RESERVED INTERNAL PORTS

Mailcow uses several ports internally (e.g., 8081, 9081, 65510). Avoid using these for your own services on the Docker host to prevent conflicts.

Use ufw to open these ports:

⚠️ DOCKER BYPASSES UFW RULES

By default, Docker manipulates iptables directly and bypasses UFW rules, meaning your container ports might be exposed even if ufw is configured to block them. There are two effective ways to mitigate this:

  1. Bind Services to Localhost (Recommended): As we did in mailcow.conf with HTTP_BIND, binding services to 127.0.0.1 ensures they are never exposed externally by Docker. This is the most secure approach.
  2. Modify UFW’s Configuration: For services that must be exposed, you can edit /etc/ufw/after.rules to correctly manage traffic from Docker’s network. This is more complex but necessary for direct external access. For more details, see the official Docker documentation on packet filtering.
sudo ufw allow 25/tcp
sudo ufw allow 587/tcp
sudo ufw allow 465/tcp
sudo ufw allow 143/tcp
sudo ufw allow 993/tcp
sudo ufw allow 110/tcp
sudo ufw allow 995/tcp
sudo ufw allow 4190/tcp
sudo ufw status

9. Initial Mailcow Setup

Navigate to your Mailcow UI at https://mail.your-domain.com.

  • Default Username: admin
  • Default Password: moohoo

First, change the administrator password under System -> Configuration -> Edit.

9.1. Add Your Domain

Go to Email -> Configuration and click “Add domain”. Enter your main domain (e.g., your-domain.com, not mail.your-domain.com).

9.2. Generate DKIM Key

A DKIM key is essential for email authentication.

  1. Go to System -> Configuration.

  2. Navigate to the Options -> ARC/DKIM keys tab.

  3. A 2048-bit key should already be generated for your domain. Copy the public key text from the text box.

  4. Go back to your DNS provider and add a new TXT record:

    • Name: dkim._domainkey
    • Value: Paste the entire copied key, including the v=DKIM1;k=rsa;p=... part.
    💡 LONG DKIM RECORDS

    Some DNS providers have a 255-character limit for a single TXT record string. If your 2048-bit DKIM key is longer, you may need to split it into multiple quoted strings. Many providers handle this automatically, but if you encounter issues, check your provider’s documentation on how to format long TXT records.

10. Testing and Verification

After waiting for DNS propagation, thoroughly test your setup.

10.1. Internal DNS and Certificate Check

  • Mailcow DNS Check: In the Mailcow UI, go to Email -> Configuration -> DNS next to your domain for an internal check of your records.
  • Verify Certificates: Ensure that the certificates from Traefik have been correctly passed to Mailcow’s services. Run these commands, replacing mail.your-domain.com with your mail server’s FQDN:
    # Check SMTP certificate
    echo "Q" | openssl s_client -starttls smtp -crlf -connect mail.your-domain.com:587
    
    # Check IMAP certificate
    echo "Q" | openssl s_client -starttls imap -crlf -connect mail.your-domain.com:143
    
    In the output, look for a certificate chain issued by a trusted authority. For Let’s Encrypt, the chain typically starts with ISRG Root X1. The key is to confirm it is not a self-signed certificate. If you see a self-signed certificate (where the issuer matches the subject), it indicates a problem with the certdumper service or its path mappings. In that case, check the certdumper logs:
    cd /opt/containers/mailcow
    docker compose logs certdumper
    

10.2. External Testing Tools

  • mail-tester.com: Send an email to the address provided on their site to get a score out of 10.
  • mxtoolbox.com: Provides various checks for your mail server.
  • checktls.com/TestReceiver: Use this to specifically verify your MTA-STS configuration.

11. Create Mailboxes and Log In

Under Email -> Mailboxes, you can add new users. Once a user is created, the primary way to access the webmail interface (SOGo) is by clicking Apps -> Webmail from within the Mailcow UI. While you can also try going directly to https://mail.your-domain.com/SOGo/, be aware that recent Mailcow versions may redirect unauthenticated access back to the main login page.

12. Conclusion

You have now successfully deployed a full-featured Mailcow e-mail server. By leveraging Docker for containerization and Traefik for reverse proxying and certificate management, you have a secure, robust, and maintainable mail solution. This setup provides you with complete control over your email, enhanced security through features like MTA-STS, and the flexibility to manage multiple domains and users with ease.

13. Further Reading

For more detailed information, refer to the official Mailcow documentation:

🛡️TEST YOUR EMAIL SCORE