Complete Mailcow Multi-Server Configuration Guide

Overview

This guide documents how to configure two Mailcow servers to communicate using private IP addresses for reliable email delivery, bypassing public internet routing issues.

Server Architecture

Server Hostname Public IP Private IP Domain
Server 1 hostone Public ip 10.36.4.10 domain1.com
Server 2 hosttwo public Ip 10.36.4.4 domain2.com

Part 1: Initial Server Setup

1.1 Install Mailcow

Follow the official Mailcow installation guide for each server:

bash

cd /opt
git clone https://github.com/mailcow/mailcow-dockerized
cd mailcow-dockerized
./generate_config.sh
docker compose pull
docker compose up -d

1.2 Configure Domains in Mailcow UI

  • Add your domains in Mailcow admin interface

  • Set up DNS records (MX, SPF, DKIM, DMARC)


Part 2: Private Network Configuration

2.1 Identify Private IPs

On each server, find the private IP:

bash

ip addr show | grep -E "inet.*(eth0|ens|enp)"

Note down the private IPs (usually in 10.x.x.x172.x.x.x, or 192.168.x.x range).

2.2 Test Private IP Connectivity

bash

# From Server 1 to Server 2
ping -c 4 10.36.4.4
nc -4 -zv 10.36.4.4 25

# From Server 2 to Server 1
ping -c 4 10.36.4.10
nc -4 -zv 10.36.4.10 25

Part 3: Postfix Transport Maps Configuration

3.1 On Server 1 (domain1.com)

Configure to send emails for domain2.com via private IP:

bash

# Enter Postfix container
docker exec -it mailcowdockerized-postfix-mailcow-1 bash

# Create transport file
echo "domain2.com    smtp:[10.36.4.4]:25" > /etc/postfix/transport
echo "mail.domain2.com    smtp:[10.36.4.4]:25" >> /etc/postfix/transport

# Process the transport map
postmap /etc/postfix/transport

# Get current transport_maps value
current_transport=$(postconf -h transport_maps)

# Add your hash map to the beginning
postconf -e "transport_maps = hash:/etc/postfix/transport, $current_transport"

# Verify configuration
postconf transport_maps
postmap -q samratnepal.com hash:/etc/postfix/transport

# Reload Postfix
postfix reload
exit

3.2 On Server 2 (domain2.com)

Configure to send emails for domain1.com via private IP:

bash

# Enter Postfix container
docker exec -it mailcowdockerized-postfix-mailcow-1 bash

# Create transport file
echo "domain1.com    smtp:[10.36.4.10]:25" > /etc/postfix/transport
echo "mail.domain1.com    smtp:[10.36.4.10]:25" >> /etc/postfix/transport

# Process the transport map
postmap /etc/postfix/transport

# Get current transport_maps value
current_transport=$(postconf -h transport_maps)

# Add your hash map to the beginning
postconf -e "transport_maps = hash:/etc/postfix/transport, $current_transport"

# Verify configuration
postconf transport_maps
postmap -q domain1.com hash:/etc/postfix/transport

# Reload Postfix
postfix reload
exit

Part 4: Disable Greylisting Between Servers

4.1 On Both Servers

Create Rspamd whitelist configuration:

bash

# On Server 1 (10.36.4.10)
docker exec -it mailcowdockerized-rspamd-mailcow-1 bash
mkdir -p /etc/rspamd/local.d

cat > /etc/rspamd/local.d/greylist.conf << 'EOF'
# Whitelist Server 2
whitelisted_ip = "public ip";
whitelisted_ip = "10.36.4.4";
whitelisted_ip = "10.36.4.0/24";  # Entire private network
EOF

exit
docker restart mailcowdockerized-rspamd-mailcow-1

# On Server 2 (10.36.4.4)
docker exec -it mailcowdockerized-rspamd-mailcow-1 bash
mkdir -p /etc/rspamd/local.d

cat > /etc/rspamd/local.d/greylist.conf << 'EOF'
# Whitelist Server 1
whitelisted_ip = "public";
whitelisted_ip = "10.36.4.10";
whitelisted_ip = "10.36.4.0/24";  # Entire private network
EOF

exit
docker restart mailcowdockerized-rspamd-mailcow-1

Part 5: Firewall Configuration

5.1 On Both Servers

Ensure iptables allows traffic between private IPs:

bash

# Check current FORWARD policy
sudo iptables -L FORWARD -n -v

# If FORWARD policy is DROP, change to ACCEPT
sudo iptables -P FORWARD ACCEPT

# Allow traffic between servers
sudo iptables -I DOCKER-USER -s 10.36.4.0/24 -j ACCEPT

# Make iptables rules persistent
sudo apt-get install -y iptables-persistent
sudo netfilter-persistent save

5.2 Disable Mailcow Netfilter Isolation

Edit mailcow.conf on both servers:

bash

cd /opt/mailcow-dockerized
nano mailcow.conf

Add or modify:

text

SKIP_NETFILTER_ISOLATION=y

Then restart Mailcow:

bash

docker compose down
docker compose up -d

Part 6: Testing the Configuration

6.1 Test Email from Server 1 to Server 2

bash

swaks --to developers@samratnepal.com --from dipeshmahato@spectranepal.com --server localhost --port 587 --tls --auth LOGIN --auth-user dipeshmahato@spectranepal.com --auth-password "your-password"

6.2 Test Email from Server 2 to Server 1

bash

swaks --to dipeshmahato@spectranepal.com --from developers@samratnepal.com --server localhost --port 587 --tls --auth LOGIN --auth-user developers@samratnepal.com --auth-password "your-password"

6.3 Monitor Logs During Testing

bash

# Watch Postfix logs
docker logs mailcowdockerized-postfix-mailcow-1 -f --tail 50

# Watch Rspamd logs
docker logs mailcowdockerized-rspamd-mailcow-1 -f --tail 50

# Watch Dovecot logs
docker logs mailcowdockerized-dovecot-mailcow-1 -f --tail 50

6.4 Check Mail Queues

bash

# On both servers
docker exec mailcowdockerized-postfix-mailcow-1 mailq

Part 7: Troubleshooting Commands

7.1 Check Transport Map

bash

# Test if transport map is working
docker exec mailcowdockerized-postfix-mailcow-1 postmap -q samratnepal.com hash:/etc/postfix/transport
# Should return: smtp:[10.36.4.4]:25

7.2 Check Private IP Connectivity

bash

# Test ping
ping -c 4 10.36.4.4

# Test port 25
nc -4 -zv 10.36.4.4 25

7.3 Clear Stuck Emails

bash

# Delete all queued messages
docker exec mailcowdockerized-postfix-mailcow-1 postsuper -d ALL

# Delete specific message
docker exec mailcowdockerized-postfix-mailcow-1 postsuper -d MESSAGE_ID

7.4 Force Queue Processing

bash

docker exec mailcowdockerized-postfix-mailcow-1 postqueue -f

Part 8: Maintenance Commands

8.1 Restart Services

bash

# Restart specific container
docker restart mailcowdockerized-postfix-mailcow-1

# Restart all Mailcow services
cd /opt/mailcow-dockerized
docker compose down
docker compose up -d

8.2 Backup Transport Maps

bash

# Save transport files for future reference
docker cp mailcowdockerized-postfix-mailcow-1:/etc/postfix/transport ./transport.backup
docker cp mailcowdockerized-postfix-mailcow-1:/etc/postfix/transport.db ./transport.db.backup

8.3 Restore Transport Maps

bash

docker cp ./transport.backup mailcowdockerized-postfix-mailcow-1:/etc/postfix/transport
docker exec mailcowdockerized-postfix-mailcow-1 postmap /etc/postfix/transport
docker exec mailcowdockerized-postfix-mailcow-1 postfix reload

Part 9: Enable Debug Logging (If Needed)

9.1 Enable Postfix Verbose Logging

bash

cd /opt/mailcow-dockerized
echo "smtp      unix  -       -       y       -       -       smtp -v" >> data/conf/postfix/extra.cf
docker restart mailcowdockerized-postfix-mailcow-1

9.2 Enable Rspamd Debug Logging

bash

docker exec -it mailcowdockerized-rspamd-mailcow-1 bash
mkdir -p /etc/rspamd/local.d

cat > /etc/rspamd/local.d/logging.inc << 'EOF'
level = "debug";
debug_modules = ["greylist", "spf", "dkim", "dmarc", "rbl", "mime", "bayes"];
log_format = "id: <$mid>, from: <$smtp_from>, to: <$smtp_rcpt>, ip: $ip, action: $action";
log_usec = true;
EOF

exit
docker restart mailcowdockerized-rspamd-mailcow-1

Part 10: Quick Setup Script

Save this as setup-mailcow-transport.sh for future server setups:

bash

#!/bin/bash
# Usage: ./setup-mailcow-transport.sh [local-domain] [remote-domain] [remote-private-ip]

LOCAL_DOMAIN=$1
REMOTE_DOMAIN=$2
REMOTE_IP=$3

if [ -z "$LOCAL_DOMAIN" ] || [ -z "$REMOTE_DOMAIN" ] || [ -z "$REMOTE_IP" ]; then
    echo "Usage: $0 [local-domain] [remote-domain] [remote-private-ip]"
    echo "Example: $0 spectranepal.com samratnepal.com 10.36.4.4"
    exit 1
fi

echo "Configuring Postfix transport for $REMOTE_DOMAIN -> $REMOTE_IP"

# Enter Postfix container
docker exec -it mailcowdockerized-postfix-mailcow-1 bash -c "
# Create transport file
echo '$REMOTE_DOMAIN    smtp:[$REMOTE_IP]:25' > /etc/postfix/transport
echo 'mail.$REMOTE_DOMAIN    smtp:[$REMOTE_IP]:25' >> /etc/postfix/transport

# Process transport map
postmap /etc/postfix/transport

# Add to transport_maps
current_transport=\$(postconf -h transport_maps)
postconf -e \"transport_maps = hash:/etc/postfix/transport, \$current_transport\"

# Reload Postfix
postfix reload

# Test
postmap -q $REMOTE_DOMAIN hash:/etc/postfix/transport
"

echo "Configuration complete!"

Summary Checklist

  • Private IPs identified and tested with ping

  • Port 25 connectivity verified between servers

  • Postfix transport maps configured on both servers

  • Rspamd greylisting disabled for server IPs

  • iptables FORWARD policy set to ACCEPT

  • SKIP_NETFILTER_ISOLATION enabled in mailcow.conf

  • Test emails sent both directions

  • Logs show delivery via private IPs


Important Notes

  • The square brackets [IP] in transport maps disable MX lookups and force direct delivery

  • Transport maps must be processed with postmap after any changes

  • Always add your custom transport map at the beginning of transport_maps for precedence

  • Whitelist both public and private IPs in Rspamd to bypass greylisting

  • After major changes, restart both Postfix and Rspamd containers

This configuration ensures reliable email delivery between your Mailcow servers using private network communication, bypassing public internet routing issues and reducing latency.