0.15 → 0.16 upgrade silently enters bootstrap mode (Docker, existing config + data not detected)

Issue Description

Pulling the v0.16.6 Docker image on top of an existing v0.15.5 data + config layout silently puts the server into bootstrap mode (server.bootstrap-mode warning, No configuration file was found), which prompts a fresh setup wizard and would effectively orphan all existing principals, mailboxes, and configuration. Looking for the official 0.15 → 0.16 migration path, or for the container to detect the legacy layout and either auto-migrate or refuse to start.

Cross-posted from GitHub Discussion #3184 per the discussions-bot redirect.

I noticed there’s an existing topic on this forum (“All email lost after upgrading to stalwart 0.16”) describing what looks like the worst-case outcome of proceeding through the wizard — which is the scenario this report is asking how to avoid.

Expected Behavior

If 0.16 detects a populated 0.15 data directory but no 0.16-compatible config, it should either:

  • Auto-migrate the legacy layout, OR
  • Refuse to start with a clear error such as legacy 0.15 layout detected at /opt/stalwart; see migration guide at …

An official 0.15 → 0.16 migration guide for Docker users would also resolve this — covering path changes (/opt/stalwart/etc/config.toml → wherever 0.16 expects it), data directory remap, and RocksDB format compatibility.

Actual Behavior

Container starts cleanly (status: healthy) but immediately enters bootstrap mode, displaying a temporary administrator account and offering port 8080 for initial setup — as if no existing data or config existed. All my data at /opt/stalwart/... (RocksDB blobs, principals, sieve scripts) is silently ignored. Proceeding through the wizard would, presumably, wipe or overwrite existing state (consistent with the “All email lost after upgrading” topic already on this forum).

Reproduction Steps

  1. Working 0.15.5 setup with these volume mounts in docker-compose.yml:
    services:
      stalwart:
        image: stalwartlabs/stalwart:v0.15
        volumes:
          - /var/stalwart/stalwart-data:/opt/stalwart
          - /var/stalwart/stalwart-config/config.toml:/opt/stalwart/etc/config.toml:ro
    
  2. Bump the image tag to stalwartlabs/stalwart:v0.16.6. No other changes.
  3. Run docker compose up -d stalwart.
  4. Observe bootstrap mode warning + temporary admin credentials in container logs.

Relevant Log Output

🔑 Stalwart bootstrap mode - temporary administrator account
   username: admin
   password: <redacted>

WARN Server started in bootstrap mode (server.bootstrap-mode)
     details = "No configuration file was found. Port 8080 is open for initial setup."
     version = "0.16.6"
INFO Application resource updated (resource.application-updated)
     details = "Stalwart Web Interface"
INFO Network listener started (network.listen-start)
     listenerId = "http-recovery", localPort = 8080

I rolled back to v0.15 immediately and restored from snapshot before clicking through any bootstrap step. The 0.16 container is destroyed and that bootstrap password is no longer valid; it’s omitted as a matter of basic hygiene.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

S3-compatible

Search Engine

Internal

Directory Backend

Internal

Additional Context

Single-node Docker Compose on Debian 12. ~60 MB of data. R2 (S3-compatible) blob backend. All inbound MX + outbound (via MXRoute relay) live in production. Happy to help test a documented migration path on a snapshot copy if useful.

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 documentation and configuration changes between versions.

There is a detailed, official migration guide. Here’s what it says, directly applicable to your Docker + RocksDB situation:


The bootstrap mode you saw is expected and correct behavior – the new image looks for config.json and your old config.toml is invisible to it. The migration is not a simple image-tag bump; it is a multi-step offline process. The key points for your exact setup:

Why it happened

The new Docker image uses different mount points:

Volume Purpose
/etc/stalwart Configuration (contains config.json)
/var/lib/stalwart Persistent data (RocksDB, blobs)

Your old layout at /opt/stalwart/... is not read by the new image at all, so it enters bootstrap mode as if starting fresh. Your data is safe as long as you didn’t click through the wizard (which you correctly avoided).

The correct migration path

The official guide (UPGRADING/v0_16.md in the server repo) specifies these steps for Docker:

Step 1 (before stopping anything): Run the migration script against your still-running v0.15 server to produce config.json and export.json:

# Download the script
curl -fLO https://raw.githubusercontent.com/stalwartlabs/stalwart/refs/heads/main/resources/scripts/migrate_v016.py

python3 -m venv .venv && source .venv/bin/activate
pip install requests urllib3

# Dump settings from the live v0.15 server
python migrate_v016.py dump \
    --url https://mail.example.com \
    --username admin --password adminPassword \
    --settings settings.json --principals principals.json

# Convert -- critical: use --patch-paths to rewrite /opt/stalwart -> /var/lib/stalwart
python migrate_v016.py convert \
    --settings settings.json --principals principals.json \
    --config config.json --output export.json \
    --patch-paths /opt/stalwart=/var/lib/stalwart

The --patch-paths flag is essential for your setup – without it, paths in the generated files still point to /opt/stalwart/data which the new container cannot access.

Step 2: Back up your RocksDB data directory before proceeding (the migration wipes directory records, settings, quotas, and reports – mail bodies are preserved but those wipes are irreversible without a backup).

Step 3: Copy your old RocksDB data to the new volume layout:

docker volume create stalwart-etc
docker volume create stalwart-data

# Copy old data (adjust OLD_STALWART_DIR to your /var/stalwart/stalwart-data)
docker run --rm \
    -v /var/stalwart/stalwart-data:/old \
    -v stalwart-data:/new \
    alpine sh -c 'cp -a /old/data/. /new/ && chown -R 2000:2000 /new'

Since you’re using S3/R2 for blobs, you can skip blob copying – the blob data already lives externally.

Step 4: Install config.json into the new config volume, then run a recovery-mode container (not a production container):

docker run -d --name stalwart-recovery \
    -e STALWART_RECOVERY_MODE=1 \
    -e STALWART_RECOVERY_ADMIN=admin:someTemporaryPassword \
    -p 8080:8080 \
    -v stalwart-etc:/etc/stalwart \
    -v stalwart-data:/var/lib/stalwart \
    stalwartlabs/stalwart:v0.16

Step 5: Apply the exported snapshot using stalwart-cli (v1.0.2 or later):

export STALWART_URL=http://127.0.0.1:8080
export STALWART_USER=admin
export STALWART_PASSWORD=someTemporaryPassword
stalwart-cli apply --file export.json

Step 6: Stop the recovery container and start the production container (same image, without STALWART_RECOVERY_MODE, with all mail ports).

Step 7: Trigger “Recalculate disk quotas” from the Tasks panel in the WebUI (quotas are reset to zero during migration).

Important caveats from the guide

  • The migration script only converts: accounts/groups, domains, storage settings, DKIM signatures, and TLS certificates. SMTP listeners, mail routing, rate limits, spam rules, Sieve scripts, and everything else must be recreated manually (or via a snapshot from a test v0.16 deployment).
  • Port 587 STARTTLS and plain-port listeners are not added by default in v0.16 – mail clients configured for STARTTLS on 587/143 will stop working until listeners are recreated.
  • The WebUI is only reachable over HTTPS on the configured hostname in normal mode; http://...:8080/admin is only for the migration window.

The full guide is at: UPGRADING/v0_16.md in the Stalwart server repository.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

Thanks @helpbot — the procedure itself is useful, but the scope caveat is the part I want to push on.

Per your reply, the script only migrates: accounts/groups, domains, storage settings, DKIM signatures, and TLS certificates. Explicitly not migrated: SMTP listeners (incl. port 587 STARTTLS + plain-port), mail routing, rate limits, spam rules, Sieve scripts, ACME provider configuration, webhook configuration, and “everything else.”

For a production deployment with non-trivial customization, “everything else” is the bulk of the configuration. Concrete inventory from one real 0.15.5 setup:

  • Smarthost relay routing (MXRoute via queue.route.* + queue.strategy.route.*)
  • ACME DNS-01 with Cloudflare provider (custom challenge + scoped API token in acme.<id>.provider / acme.<id>.secret)
  • Outbound event webhook (HMAC-signed, custom event filter list)
  • Custom Sieve scripts (per-user + system-wide DATA stage)
  • Spam-filter tuning (rule weights, allowlists, blocklists, Enterprise AI/LLM provider config in enterprise.ai.* + spam-filter.llm.*)
  • ClamAV milter integration (TCP socket, timeouts, stage filters)
  • Mail alias metadata (mail_aliases.*)
  • DSN report config, vacation responder scripts, OIDC external-directory binding
  • Custom WAF / rate-limit rules, IP reputation thresholds

That’s a several-hour to multi-day rebuild per operator, and there’s no automated way to diff “what I had on 0.15” vs “what got carried over to 0.16” — so config will silently disappear unless someone keeps a manual checklist of every key they ever set.

Two follow-up questions:

  1. Is there any plan to expand the migration script’s coverage in a future point release — even partially? Even a raw dump-all-config-keys mode that produces a 0.16 schema-mapping table for the most common keys (queue routes, ACME providers, webhooks, milters, spam-filter overrides) would dramatically lower the migration cost for established deployments.

  2. Independent of the migration tool’s scope, is the silent bootstrap-mode entry on a plain image-tag bump (issue #2 in the OP) considered expected behavior, or a release-blocker for a future point release? The current behavior means an operator following normal Docker upgrade cadence (docker compose pull && docker compose up -d, Watchtower, etc.) would proceed through the wizard and lose everything — which appears to be exactly what happened in the “All email lost after upgrading to stalwart 0.16” thread already on this forum. A startup-time check that detects a populated legacy data directory and refuses to start with a pointer to UPGRADING/v0_16.md would close that footgun without changing the documented migration flow at all.

Both of your follow-up questions are reasonable, but neither is currently addressed in the documentation or source code – so here’s what can be said based on what was actually fetched:

On question 1 (expanding migration script coverage): The script’s scope is explicitly documented as intentionally conservative – the guide states “Only the following settings are migrated, because the rest have changed enough that automatic mapping would do more harm than good.” There is no mention in UPGRADING/v0_16.md or the script itself of a planned expansion to cover queue routes, ACME providers, webhooks, milters, spam-filter overrides, or the other items in your inventory. Whether that’s on the roadmap isn’t something the available docs or source can answer.

On question 2 (silent bootstrap on image-tag bump): Looking at the boot code in crates/common/src/manager/boot.rs, the server checks whether config.json is absent and immediately enters bootstrap mode – there is no startup-time scan for a legacy /opt/stalwart layout or config.toml that would trigger a warning or a refusal to start. So the behavior you observed (silently entering bootstrap rather than detecting the old layout and pointing to the migration guide) reflects how the code currently works. The docs don’t characterize this as intentional design vs. a known gap, and the upgrade guide does not describe any auto-detection safeguard.

Both of these are genuinely good points for human follow-up – the migration script coverage question and the “footgun on image-tag bump” question are product/roadmap matters that a staff member or community contributor is better positioned to address.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

Please read the upgrading to v0.16 guide. Stalwart enters bootstrap mode if config.json is absent, this is expected behaviour. Performing an automated migration is not possible due to the reasons explained in the upgrading guide.