Introduction
Mailcow is a popular, Docker-based, open-source groupware and email suite. Because it runs entirely in Docker containers, it is generally straightforward to move to a new server: stop the containers, copy the files, start the containers again.
There is one situation where this simple approach breaks: moving between two servers with different CPU architectures, for example from an Arm64 (aarch64) server to an x86_64 server, or the other way around. If you copy every file across without changes, one specific component - Rspamd's compiled Hyperscan cache - will cause the Rspamd container to crash-loop on the new server, because compiled machine code from one CPU architecture cannot run on another.
This tutorial walks through a complete, minimal-downtime migration of a production Mailcow instance from an aarch64 source server to an x86_64 server, including the one exclusion rule you need for the architecture change, a temporary least-privilege access bridge between the two servers, and a full DNS cutover checklist for a mail server specifically (which has more moving parts than a typical website migration).
By the end of this tutorial, you will have:
- A safety backup of your Mailcow installation, stored independently of the live data.
- A new Mailcow installation on your target server with all mailboxes, messages, DKIM keys, and configuration intact.
- A short, planned downtime window (typically a few minutes) instead of an unplanned one.
- DNS fully cut over to the new server, with no mail lost in transit.
Prerequisites
- A working Mailcow installation on a source server, installed the standard way under
/opt/mailcow-dockerized. - A new target server running Debian 12 or 13, with a public IPv4 address.
- Root or sudo access on both servers.
- Access to your domain's DNS zone (to update A/AAAA, SPF, and PTR records).
- Basic familiarity with Docker and the Linux command line.
Example terminology
This tutorial uses the following placeholders. Replace them with your own values:
- Source server IP:
<source_ip>(theaarch64server you are migrating away from) - Target server IP:
<target_ip>(thex86_64server you are migrating to) - Target server user:
holu(a non-root user with sudo access, as is standard on a fresh server) - Domain:
<example.com> - Mail hostname:
mail.<example.com>
Do not use actual IPs or domains when following along - replace the placeholders above with your own values.
Step 1 - Take Inventory on the Source Server
Before touching anything, confirm the current state of your Mailcow installation and its architecture.
cd /opt/mailcow-dockerized
sudo docker compose ps
sudo cat mailcow.conf | grep -E "MAILCOW_HOSTNAME|COMPOSE_PROJECT_NAME|SNAT_TO_SOURCE"
du -sh /opt/mailcow-dockerized
sudo du -sh /var/lib/docker/volumes
uname -mCheck that all containers are healthy, note down the values printed from mailcow.conf, and confirm uname -m prints aarch64 (or whichever architecture your source server uses).
Also check whether you have a docker-compose.override.yml file - if you do, it contains custom settings that will be carried over automatically by rsync later, but it's good to be aware of it. If your Mailcow installation is not on the latest version, run ./update.sh now and update it before migrating, so that source and target run identical code.
Step 2 - Lower DNS TTLs Ahead of Time
Because the migration involves a change of public IP address, do this step one to two days before the actual cutover, so you are not waiting on DNS propagation during the migration itself.
- Lower the TTL on your A/AAAA, MX, and any
autoconfig/autodiscoverCNAME records to 300 seconds (5 minutes). - If your SPF record lists your current source IP under
ip4:, add your future target IP now as well. Keep both IPs listed until after the cutover, then remove the old one. - Your DKIM and DMARC records do not need to change - the DKIM private key will be copied over unchanged as part of the migration. It's still worth double-checking that the TXT record is correct before you start.
Step 3 - Prepare the Target Server
Only install Docker on the target server - do not run Mailcow's own installer (git clone + generate_config.sh) here. Every file, including the Mailcow code itself, will arrive via rsync in a later step, so a fresh install would only create conflicting configuration.
Install Docker Engine and the Compose plugin using the official apt repository (recommended for production use, rather than the get.docker.com convenience script):
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now dockerA minimal server image often doesn't ship with rsync pre-installed, so install it explicitly on both servers now - it's easy to miss and the first sync attempt will simply fail with "command not found" otherwise:
sudo apt-get update
sudo apt-get install -y rsyncBefore opening the target server to mail traffic, verify the following. Skipping any of these is one of the most common reasons a mail server migration ends with mail landing in spam, or not being delivered at all:
- Firewall: allow inbound 25 (SMTP), 465/587 (Submission), 143/993 (IMAP), 110/995 (POP3), 4190 (Sieve), and 80/443 (HTTP/S for ACME and webmail).
- Outbound port 25: confirm your provider does not block it. Many cloud and residential ISPs block outbound port 25 by default specifically to prevent spam, which would mean you cannot send any mail at all from the new server.
- PTR (reverse DNS) record: request a PTR record from your provider that resolves your new public IP back to
mail.<example.com>. Without a matching PTR record, many receiving mail servers will reject or flag your mail. - Blocklists: check the new IP against common blocklists (for example via MXToolbox or Spamhaus) before going live, especially if it's from a pool of previously-used addresses.
Step 4 - Set Up a Temporary, Least-Privilege Access Bridge
To copy data between the two servers without permanently opening root SSH login or handing out full sudo rights, set up a narrow, single-purpose bridge that only allows rsync, and remove it again once the migration is verified.
On the source server, generate a dedicated key for root (needed because rsync will run with sudo, and sudo rsync looks for keys under /root/.ssh, not your regular user's home directory):
sudo su
ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""Copy the key to the target server (this will prompt once for the target user's password):
ssh-copy-id -i /root/.ssh/id_ed25519.pub holu@<target_ip>
exitOn the target server, allow this one command to run as root without a password, scoped to the rsync binary only:
which sudo || sudo apt-get install -y sudo
which rsync
echo "holu ALL=(root) NOPASSWD: /usr/bin/rsync" | sudo tee /etc/sudoers.d/rsync-migration
sudo chmod 440 /etc/sudoers.d/rsync-migration
sudo visudo -cvisudo -c checks the file for syntax errors - a typo in a sudoers file can otherwise lock out sudo entirely, so don't skip this check. This setup does not change any existing login method on either server; it only adds one narrowly-scoped, temporary bridge that you will remove in Step 9.
Step 5 - Take an Additional Safety Backup
Before moving any data, take an independent backup using Mailcow's own backup script, in addition to the rsync copy you'll do next. This gives you a second, application-aware recovery point if anything goes wrong with the raw file copy.
cd /opt/mailcow-dockerized
sudo mkdir -p /root/mc-safety-backup
sudo MAILCOW_BACKUP_LOCATION=/root/mc-safety-backup ./helper-scripts/backup_and_restore.sh backup allCopy this backup off the source server as well - a backup that only exists on the server you're about to decommission isn't much of a safety net:
sudo rsync -aHhP --rsync-path="sudo rsync" /root/mc-safety-backup/ holu@<target_ip>:/root/mc-safety-backup
# Confirm the sizes roughly match on both sides
sudo du -sh /root/mc-safety-backup
ssh holu@<target_ip> "sudo du -sh /root/mc-safety-backup"Step 6 - Phase 1: Live Pre-Sync (No Downtime)
While Mailcow is still running normally on the source server, copy everything across for the first time. This initial copy can take a long time depending on your data size, but it causes no interruption to mail service.
# Application code and configuration
sudo rsync -aHhP --numeric-ids --delete --rsync-path="sudo rsync" \
/opt/mailcow-dockerized/ holu@<target_ip>:/opt/mailcow-dockerized
# Docker volumes (mailboxes, database, DKIM keys, TLS certificates)
# Note the exclusion below - this is the one architecture-specific step.
sudo rsync -aHhP --numeric-ids --delete --rsync-path="sudo rsync" \
--exclude '*rspamd-vol-1' \
/var/lib/docker/volumes/ holu@<target_ip>:/var/lib/docker/volumesWhy exclude rspamd-vol-1: every other Docker volume Mailcow uses - MariaDB's InnoDB files, Redis's RDB/AOF files, the Maildir mailboxes, and the DKIM/TLS certificate files - is architecture-independent and copies over safely. rspamd-vol-1 is the one exception: it contains Rspamd's compiled Hyperscan pattern cache, which is machine code specific to the CPU architecture it was compiled on. Copying it from an aarch64 server to an x86_64 server (or vice versa) will make the Rspamd container crash-loop on startup. The cache is only a performance optimization, not user data - Rspamd rebuilds it automatically the first time it starts on the new server, so excluding it causes no data loss, just a slightly slower first boot.
If your dataset is large, you can safely re-run this same step once or twice more over the following hours or days - rsync only transfers what has changed since the last run, so each repeat sync gets progressively faster and shrinks the amount of data left for the final cutover.
Step 7 - Phase 2: The Downtime Window (Final Sync)
Pick a low-traffic time (for example, the middle of the night). This is the only step with actual downtime, and it should now be short since Step 6 already transferred the bulk of the data.
Stop Mailcow and Docker itself on the source server - this matters because copying database files while they're still being written to can result in a corrupted copy:
cd /opt/mailcow-dockerized
sudo docker compose down
sudo systemctl stop docker.service
sudo systemctl status docker.service --no-pagerRun the exact same two rsync commands from Step 6 again. Since most of the data was already transferred, this run should be fast:
sudo rsync -aHhP --numeric-ids --delete --rsync-path="sudo rsync" \
/opt/mailcow-dockerized/ holu@<target_ip>:/opt/mailcow-dockerized
sudo rsync -aHhP --numeric-ids --delete --rsync-path="sudo rsync" \
--exclude '*rspamd-vol-1' \
/var/lib/docker/volumes/ holu@<target_ip>:/var/lib/docker/volumesStep 8 - Start Mailcow on the Target Server
Back on the target server, start Docker and bring Mailcow up:
sudo systemctl start docker.service
cd /opt/mailcow-dockerized
sudo docker compose pull
sudo docker compose up -d
sudo docker compose psdocker compose pull re-fetches every image. Mailcow's images are multi-arch, so this automatically pulls the x86_64 variant of each image rather than the aarch64 ones referenced in the source server's local image cache.
Watch the Rspamd container specifically - its first startup on the new architecture will take a little longer than usual, since it is rebuilding the Hyperscan cache you excluded in Step 6:
cd /opt/mailcow-dockerized
sudo docker compose logs -f rspamd-mailcowWait until it finishes without errors before moving on.
Since your public IP has changed, update it in the configuration file so that SOGo/webmail behaves correctly behind the new IP:
sudo nano mailcow.conf
# Update: SNAT_TO_SOURCE=<new_target_ip>
sudo docker compose up -dStep 9 - Remove the Temporary Access Bridge
Once you've confirmed the data on the target server looks correct (you can do a quick check now and a fuller one in Step 11), remove the access bridge you created in Step 4 from both servers.
On the target server:
sudo rm /etc/sudoers.d/rsync-migration
nano ~/.ssh/authorized_keys
# Remove the line containing the source server's public key, then saveOn the source server, once you no longer need to run another sync from it:
sudo rm -f /root/.ssh/id_ed25519 /root/.ssh/id_ed25519.pubThis returns both servers to exactly the access model they had before the migration.
Step 10 - Cut Over DNS
Update your DNS zone:
- A/AAAA: point
mail.<example.com>to the target server's new IP. - MX: no change needed if it already points at
mail.<example.com>- it will follow the A record change automatically. - PTR/rDNS: confirm the record you requested in Step 3 is now active for the new IP.
- SPF: remove the old source IP from the
ip4:entry, leaving only the new one. - DKIM/DMARC: no change needed - just confirm they still resolve correctly.
- autoconfig/autodiscover: usually no change needed if these already point at the same hostname.
If you run a spam gateway in front of Mailcow (for example Proxmox Mail Gateway or a similar product), don't forget that DNS is not the only place your old IP is referenced - the gateway itself has Mailcow's IP hard-coded in at least two places, and both need updating:
- The inbound relay target - the IP the gateway forwards incoming mail to after filtering. This still needs to point at wherever Mailcow is reachable from the gateway (which may or may not have changed, depending on whether the gateway and Mailcow are on the same network).
- The outbound smart host / transport rule - the IP the gateway accepts outgoing mail from, if Mailcow relays its outbound mail through the gateway rather than sending directly. This one usually does need to change, since it corresponds to Mailcow's new public IP.
Exactly where these are configured depends on which gateway product you use, so check its documentation for the relevant transport/relay settings. If you miss this step, the symptom is usually confusing: DNS looks correct, Mailcow itself is healthy, but mail either stops flowing through the gateway or gets rejected because the gateway sees mail from an IP it doesn't expect.
Once the records are updated, check propagation before moving on to validation. A tool like dnschecker.org lets you see whether mail.<example.com> is already resolving to the new IP from locations around the world - useful for spotting resolvers that are still serving a cached, outdated answer.
This is also a good moment to check DNS resolution directly from the machine you'll be running your own tests from in Step 11, since that's the resolver that actually matters for your validation:
# Linux/macOS
dig +short mail.<example.com>
nslookup mail.<example.com>
# Windows (PowerShell)
Resolve-DnsName mail.<example.com>If this still returns the old IP, either wait for the TTL to expire or flush your local/OS DNS cache before continuing - testing against a stale cached record will give you false negatives in the next step.
Step 11 - Validate
Work through this checklist before considering the migration complete:
docker compose psshows every container as healthy.- Webmail (SOGo) loads on the new server and you can log in.
- Send a test email to an external address, and receive one from an external address - check it doesn't land in spam.
- Connect a mail client (Outlook, Thunderbird) over IMAP/SMTP.
openssl s_client -connect mail.<example.com>:465returns a valid certificate.- The Rspamd web interface (via
/rspamdbehind webmail) shows normal statistics. - No stuck messages:
sudo docker compose exec postfix-mailcow postqueue -p
Step 12 - Monitor, Then Decommission the Old Server (Optional)
Don't delete the source server immediately. Some DNS resolvers and sending mail servers cache records longer than your configured TTL, and you may want a fallback for a little while.
- Keep the source server reachable but shut down (containers stopped, disk untouched) for at least one to two weeks.
- Monitor mail queues and any watchdog notifications closely for the first 48 hours after cutover.
- Once you're confident everything is stable, decommission the source server and release its resources.
Conclusion
You've now migrated a production Mailcow installation across CPU architectures with a single short downtime window, no lost mail, and a temporary access model that left no lingering credentials behind. The same two-phase rsync approach - live pre-sync followed by a short final sync - works just as well for a same-architecture migration; you'd simply skip the rspamd-vol-1 exclusion in that case.
Next steps: