Issue Description
When trying to migrate from v0.15 to v0.16 following the migration guide, I’m stuck when running v0.16 in recovery mode to migrate the data. I’d dump and convert the old config using the python script and place the new config.json to the bind mount folder of /etc/stalwart and bind mount the data folder to /var/lib/stalwart, both config and data folder own by user 2000.
Expected Behavior
Migration finished successfully
Actual Behavior
I think migration stop partway with this log in the STDOUT
2026-05-25T10:54:52Z WARN Server started in recovery mode (server.recovery-mode) details = "Port 8080 is open for troubleshooting and recovery.", hostname = "b7acaf8b247e", version = "0.16.6"
2026-05-25T10:54:52Z ERROR Configuration build error (registry.build-error) source = "SpamPyzor", id = 20080258862541, reason = "Invalid address: failed to lookup address information: Temporary failure in name resolution"
2026-05-25T10:54:52Z INFO Network listener started (network.listen-start) listenerId = "http-recovery", localIp = ::, localPort = 8080, tls = false
When I try to apply export.json next then start v0.16 not in recovery mode stalwart panic with this log
Migration aborted: Upgrading to version 0.16 is a multi-step process, please read https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md
thread 'main' (1) panicked at crates/migration/src/lib.rs:95:5:
Migration aborted: Upgrading to version 0.16 is a multi-step process, please read https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Migration aborted: Upgrading to version 0.16 is a multi-step process, please read https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md
Stalwart Version
v0.16.x
Installation Method
Docker
Database Backend
RocksDB
Blob Storage
RocksDB
Search Engine
Internal
Directory Backend
Internal
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
on
Let me look up the migration guide and the specific errors mentioned.
Now I have a clear picture. Let me also check the upgrade guide for any specific steps.
Now I have a clear picture. Let me provide a thorough answer based on the source code findings.
There are two separate issues here, and they need to be resolved in order.
Issue 1: registry.build-error for SpamPyzor (DNS lookup failure)
This error is non-fatal and does not block migration. Looking at the source, it occurs because the Pyzor spam-checking service is enabled in your converted config, and when Stalwart tries to resolve the Pyzor host address during startup, DNS is not available inside the container (or the hostname can’t be resolved). The code logs a build_error and returns None (disabling Pyzor), but the server continues. The recovery mode server starts successfully on port 8080 as you can see in the log line that follows.
This error alone should not stop your migration. You can disable Pyzor in the WebUI after migration, or ensure DNS is functional inside the container if you need it.
Issue 2: “Migration aborted: multi-step process” panic
This is the real blocker. The migration code (crates/migration/src/lib.rs) explicitly checks for this condition:
Some(5) => {
if !server.registry().is_recovery_mode() {
abort("Upgrading to version 0.16 is a multi-step process, please read ...");
}
}
Schema version 5 = a 0.15 database. The migration must be initiated via recovery mode – you cannot skip straight to running v0.16 normally. The panic is triggered because you ran v0.16 without recovery mode before the migration step was completed.
The correct sequence is:
- Start v0.16 in recovery mode (which you did – and it works, port 8080 is open).
- While in recovery mode, apply your
export.json (the converted config) through the WebUI or CLI to the running recovery-mode instance.
- Only after successfully applying the config via recovery mode should you restart v0.16 in normal mode.
The full step-by-step process is documented in the official upgrading guide: https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md
In summary: Don’t restart in normal mode before applying the config through recovery mode. Your recovery mode instance is running correctly – connect to port 8080 and apply the export.json there first.
This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.
Ah, after reading bot’s answer and deliberating, I found the cause.
On version < 0.16 stalwart container mounts on /opt/stalwart, I bindmount it to lets say /opt/stalwartmail, it contains both data and etc folder. Because v0.16 migration guide keep differentiating between data folder and etc folder and tell user to mount data folder to /var/lib/stalwart, I mount the exact data folder (/opt/stalwartmail/data) to /var/lib/stalwart and mount /opt/stalwartmail/etc to /etc/stalwart. After that I run the migration not knowing that it migrates nothing (because there’s no data folder inside that data folder).
I found out about this after scrolling up my SSH session to look for what I did, the first time I ran v0.16 without recovery mode shows no data history/account, and when I check stalwart folder, a new data folder shows up inside the data folder that I mount, that’s when it clicked, /var/lib/stalwart contains data folder inside and should have been mounted to /opt/stalwartmail not the data folder itself.
It works now.