Migrating a domain and cutting over
Move a domain’s mail from another host onto this server with no bounced mail and no lost messages.
Audience: the operator running the migration. Feature: Migration. When it fails: When a migration fails.
The whole plan rests on one property: copying is idempotent. A message already here is never copied again, so you can re-run as often as you like. That is what lets you sync before the switch and again after it, and it is why the order below works.
Before you start
Section titled “Before you start”- The domain exists on this server, and every mailbox you are migrating exists here with a password set — or, for a source that lists its mailboxes, will be made by Create and map in step 3, which generates each password and shows it once. A plain IMAP migration copies mail into mailboxes; it does not create them.
- You can log in to the source server over IMAP. Ideally with a master/admin user — one credential that reads every mailbox. Without one you need each mailbox’s own password. The hosted sources need neither: one app-only credential serves every mailbox (for Microsoft 365, every mailbox its service principal was given), and the setup is the grant instead (Google, Microsoft 365).
- You have the list of source addresses — or the old host has a directory, in which case its API gets the list for you: cPanel, Plesk, Google Workspace or Microsoft 365. Plain IMAP cannot enumerate them.
-
mailctl doctoris clean and the destination has room: a migration writes the full size of every source mailbox. Check quotas now, not at 04:00.
1. Lower the MX TTL — the day before
Section titled “1. Lower the MX TTL — the day before”At your DNS host, set the domain’s MX record TTL to 300 seconds.
This is the step people skip and regret. TTL is how long the rest of the internet keeps sending mail to the old server after you change the record. At the common default of 3600–86400 s, mail keeps landing on the old host for hours or a day after cutover; at 300 s it stops within minutes.
Do this at least one old-TTL period ahead — if the TTL is currently 86400, lower it a full day before.
2. Create the migration
Section titled “2. Create the migration”Migrations → New migration (the stepper’s Source step).
- Hostname, encryption (TLS/993 for almost every real host) and the master credential. The Encryption picker is on Any IMAP server only; a cPanel or Plesk job always reads IMAP on 993 with TLS.
- Test with one real mailbox address before saving. It reports two independent results, and you
need both. The page labels them: Control panel: Connected — n mailboxes,
m forwarders across k domains. is the control panel answering over its API, which says nothing
about whether any mailbox can be opened. Mailbox sign-in (IMAP): Mailbox opened — n folders found.
is the IMAP probe, and that is the one that proves the credential a
migration will actually use. A discovery that succeeds beside a probe that fails is the normal shape
of a wrong master credential — not a contradiction, and the page says so under the two lines —
and it is a problem to fix now rather than at 3 a.m. With no master user the probe cannot log in and says so: Server reached … The login was
not tested. Enter the master user, or that mailbox’s own password (the page asks for it when there
is no master user, and never stores it), to prove the login before the night of the move
(
migration.md→ How a migration runs).
3. Map the mailboxes
Section titled “3. Map the mailboxes”From a directory (a control panel, Google Workspace or Microsoft 365): on the job’s Discover step press Read the source. You get every mailbox with its quota (and, on cPanel, its size — Plesk’s API does not report one); Create and map makes the missing ones here and points the job at them. Copy the generated passwords when they are shown — they cannot be retrieved afterwards, and the old ones cannot be carried across. Then Import the forwarders, autoresponders and filters.
Read the warnings above that table before moving on. They are where a panel says what it could not tell us — a Plesk reports no filters at all, which is not the same as there having been none.
From anything else: on the Map step paste the source addresses into Add source addresses — whitespace, commas or newlines all work. A local mailbox with the same address is matched automatically.
Either way, anything left showing “— pick a mailbox —” will fail with target_missing until you point it
somewhere. Check the mapping before you start; this is the last cheap moment to catch a typo.
4. Run the initial sync
Section titled “4. Run the initial sync”On the Copy step, Start migration. Then leave it alone.
- Expect hours for a large account. 10 GB over a typical link is a long evening.
- Mail keeps arriving at the old host throughout. That is correct and expected — you have not moved anything yet.
- The page updates every couple of seconds. Per-mailbox counters and the job log are both live.
- If the source starts throttling you, pause, set
MIGRATION_MAX_BYTES_PER_SECONDin.env, recreate the worker withdocker compose -f /opt/mailserver/compose/compose.yaml --env-file /opt/mailserver/.env up -d worker— it reads the value only when it starts — and run again. Being slower than the source’s patience is faster than being blocked by it.
You can pause freely. There is no separate Resume button: after Pause the button reads Start migration again, and pressing it re-runs every mailbox on the job — the finished ones too, as a delta. Messages already copied are skipped, so nothing is copied twice, but every mailbox’s counters start again from zero.
5. Verify the counts
Section titled “5. Verify the counts”Before you touch DNS, for each mailbox compare copied against what the source reports.
On this side:
docker compose -f /opt/mailserver/compose/compose.yaml --env-file /opt/mailserver/.env exec dovecot doveadm mailbox status -u <address> messages INBOXInvestigate every failed mailbox now — the checklist step exists precisely to stop you switching MX with a broken mailbox you have not noticed. When a migration fails maps each error code to a fix.
A small shortfall is usually duplicates: imapsync copies a message once per folder even when the source holds
it twice, and skips a message with no Message-Id or Received header. Both count in the job’s skipped figure, not as copied.
Deleted-but-not-expunged messages are copied, with their flag, so they are not the explanation. A large
shortfall is a real failure.
6. Switch the MX record
Section titled “6. Switch the MX record”Point the domain’s MX at this server. New mail now arrives here.
The old host still holds everything it received up to this moment, plus anything that arrives during propagation — which is why there is one more step.
Watch Delivery log for the first inbound messages. If nothing arrives within a few minutes of the old TTL expiring, check the MX record, and the domain’s DNS & routing section (Domains → the domain), before assuming the worst.
7. Run the final delta
Section titled “7. Run the final delta”Wait until the old TTL has fully expired plus a margin — an hour is comfortable — so that nothing more is being delivered to the old host.
Then Run again (delta). It copies only what reached the old host after the initial sync, which is usually a handful of messages and takes a minute.
When that run has finished, tick Run the final delta on the checklist; ticking it while the run, or a copy still stopping,
is writing is refused (409 job_running), because it would apply the quotas the copy is writing into. Ticking it marks the migration finished and destroys the stored
source credentials — after this the job cannot run again without re-entering them, which is the point. It also
gives each mailbox the job created its quota, unless another live migration still maps it, which it had none of while the copy ran
(migration.md → Quotas).
8. Afterwards
Section titled “8. Afterwards”- Restore the MX TTL to something ordinary (3600 s).
- Confirm mail flows both ways with a real send and reply.
- Check SPF, DKIM and DMARC for the domain now point at this server — DNS records & live verification. A migrated domain still sending on the old host’s SPF record will start failing authentication.
- Leave the old mailboxes in place for a couple of weeks. They cost nothing and they are the only backup of the pre-cutover state.
- Re-point client devices, or let autoconfig do it — Client autoconfiguration (autoconfig / autodiscover).
If you have to back out
Section titled “If you have to back out”Point the MX record back at the old host. Nothing this migration did is destructive: the source was never modified, and mail delivered here in the meantime stays here. Re-running the migration later picks up whatever is new.
Timing, roughly
Section titled “Timing, roughly”| Day −1 | Lower the MX TTL |
| Day 0, evening | Create, map, start the initial sync |
| Day 0, overnight | Sync runs |
| Day 1, morning | Verify counts, switch MX |
| Day 1, +1 hour | Final delta, tick the checklist, restore TTL |