Files
inbuxa-server/docs/spec/cutover-run.md
T
jcoffey-dev 0117500d85 Cutover: the sheet to follow at the terminal on the night
cutover.md is the reasoning and is too long to read at 2am. This is the
same sequence as commands, for this install: eight mailboxes, two people
and a printer.

That scale settles three things the general plan leaves open. The store is
small enough that the two-pass rsync buys nothing, so it is one cp inside
the window and a simpler sequence when it matters. "Every account still
works" is two sign-ins. And a reboot inside the window is affordable, which
is the only honest proof that the fork comes up on boot and the old unit
does not — is-enabled says what is configured, a reboot says what happens.

The rollback leads with chattr -i, because step 7's guard stops the
Enterprise build exactly as it stops the fork, and finding that out during
a rollback costs the worst ten minutes of the night.

And it names the printer as its own check. It is the one user that cannot
report a fault: a hardcoded credential and an old TLS stack, of the kind a
stricter default quietly refuses. The people will phone; the printer will
just stop, and nobody will notice for a fortnight.
2026-09-19 23:08:19 -07:00

7.2 KiB

The run

Status: 2026-09-19. The sheet for INBUXA's own cutover, to be followed at the terminal.

cutover.md is why; this is what to type. Where they disagree, this page is wrong — it is the general plan cut down to one small install, and anything surprising belongs back there.

This install

Eight mailboxes, two people and a printer (John, 2026-09-19). That scale decides three things the general plan leaves open:

  • The store is small, so the copy is seconds. cutover.md step 3's two-pass rsync exists for a store large enough that copying it dominates the window. Here it is complexity with nothing to buy, and a simpler sequence at 2am is worth more. One cp -a inside the window.
  • "Every account still works" is a five-minute check, not a programme. Two people sign in; the printer sends one scan.
  • A reboot inside the window is affordable, which is the only honest way to prove the old unit stays down. See step 9.

Fill these in before starting, rather than looking them up mid-window:

Fork prefix ______ (its config and unit; not under /opt/stalwart)
Fork store /var/lib/inbuxa
Old webmail container ______
Administrator ______
Printer account, port, auth ______

Before the window — nothing stopped, no rush

  1. The artifact exists and says the right thing.

    inbuxa --version      # 2026.9.18 (Stalwart 0.16.22)
    

    Built --release, on this machine's glibc or in the image that will run it. A debug binary is not what serves this.

  2. The service user exists, because the copy is chowned to it in step 5 and the unit runs as it.

    id inbuxa || useradd -r -s /usr/sbin/nologin inbuxa
    
  3. The config and unit are written and point at the fork's own paths. Nothing either of them names may be inside /opt/stalwart: it is a reference from here on and will eventually go (cutover.md, "Retiring the old install"). The unit keeps AmbientCapabilities=CAP_NET_BIND_SERVICE, which is what lets an unprivileged process hold port 25.

  4. Write down what has to be true afterwards, while the old server can still be asked: each mailbox and its message count, the two people's addresses, the aliases, and what the printer sends as.

The window

  1. Stop the old server, and make sure it cannot come back.

    systemctl stop stalwart
    systemctl disable stalwart
    systemctl mask stalwart          # stronger: cannot start even as a dependency
    systemctl is-enabled stalwart    # expect: masked
    docker stop <old-webmail>
    

    Wait for the process to have actually exited, not just for stop to return.

  2. Copy the store and the config, and hand them to the user the fork runs as.

    cp -a /opt/stalwart/data /var/lib/inbuxa
    chown -R inbuxa:inbuxa /var/lib/inbuxa
    

    A copy, never a move. cp -a preserves ownership, so the chown is not optional — without it the fork cannot write its own store.

  3. Make the original unwritable, before the fork is started.

    chattr +i /opt/stalwart/data
    

    Until step 5 the old server's own RocksDB lock was the only thing stopping anything else opening that store, and stopping it took that away. One open by the fork adds its column family and the Enterprise build never starts on it again — the rollback would be gone, with no way back (cutover.md, "Answered"). This is the line that protects it.

  4. Start the fork, and read the log before anything else.

    systemctl enable --now inbuxa
    journalctl -u inbuxa -n 50 --no-pager
    

    Expect deprecation warnings naming any STALWART_* settings to rename, and a note that INBUXA_RECOVERY_ADMIN is ignored on a configured server — both are normal. Anything at ERROR is not.

  5. Reboot the host.

    The only honest proof that the fork comes up on boot and the old unit does not. It costs a few minutes here and nothing else does the job: is-enabled says what is configured, a reboot says what happens.

    reboot
    # then
    systemctl is-active inbuxa     # active
    systemctl is-enabled stalwart  # masked
    

Before letting mail flow

  1. Work down this list. It is short because the install is.

    • inbuxa --version names the right base.
    • Every listener is up, and the certificate served on 443 and 993 is the real one, not a self-signed fallback.
    • Both people sign in, with the passwords they already had, over IMAP and through the front ends. Nothing in this move touches credentials — they are in the store that was copied — so a failure here is the copy or the front ends, not the account.
    • Each mailbox holds what step 4 recorded.
    • A message from outside arrives. A message from inside leaves.
    • Aliases still deliver.
    • The queue is draining and the task queue has no new permanent failures.
    • Nothing is listening on the old ports but the fork.
  2. The printer. Send a scan through it.

    It is the one user that cannot tell you it is broken. Scan-to-email boxes carry a hardcoded credential, an old TLS stack, and often plain auth on 25 or 587 without STARTTLS — the kind of client a stricter default quietly starts refusing. The two humans will phone; the printer will simply stop, and it will be a fortnight before anyone asks where their document went.

    Do this before calling the cutover done, not after.

Rollback

Minutes, and no restore, because the old install and its store are untouched.

systemctl disable --now inbuxa
chattr -i /opt/stalwart/data     # FIRST — step 7 blocks the old build too
systemctl unmask stalwart
systemctl enable --now stalwart
docker start <old-webmail>

The chattr -i comes first. The guard of step 7 stops the Enterprise build opening that store exactly as it stops the fork; skipping it fails the rollback with what looks like a permissions problem, at the worst possible moment.

What it costs is everything the fork accepted while it served — mail delivered to the fork's store is not in the old one. That is free between step 8 and step 10, and stops being free the moment mail flows. Decide early and deliberately.

What it never is: pointing the old build at the fork's store. That is not a recovery path and cannot be made into one.

Afterwards

  • Do not delete /opt/stalwart. It stays, shut down, masked and read-only, until it is deliberately retired (cutover.md). It costs a stopped unit and some disk.
  • Watch for the first certificate renewal. The one failure that says nothing for ninety days and then takes the service down. Forcing a renewal answers it in a day instead.
  • Point a stock ihasmail at the fork and note what it can no longer do, each difference being either a regression against upstream's contract or a feature that now expects INBUXA's own front ends (cutover.md).
  • Time every phase and keep the numbers. This run is the first run of the migration tool (migration.md), and what it measures here is what that tool will advertise. A small server's migration should be minutes; the figure published should be one measured here, against a stated store size and account count.