Cutover: side by side, with the old install left standing

John's plan, 2026-09-19: stop the Enterprise server and the ihasmail
container, install the fork at its own path, copy the data across, bring up
INBUXA Admin and the new webmail, and every account carries on.

That is a better shape than the in-place swap this draft assumed, because
the rollback becomes a service swap rather than a restore: the old install
and its data are untouched, so going back is stopping one unit and starting
another. What it costs is whatever the fork accepted in between, since the
two stores diverge the moment the fork starts.

It buys one failure the in-place swap couldn't produce: both servers on one
set of ports, each with its own store, if a reboot brings the old unit back.
So the unit is disabled, not just stopped.

Also written down: the front ends' OAuth clients travel inside the store, so
a front end that keeps its client id and redirect URIs keeps working and one
deployed fresh needs them set up, which fails looking like an account
problem when it isn't.
This commit is contained in:
2026-09-19 21:31:50 -07:00
parent 63fe315457
commit 92aed3df21
+67 -26
View File
@@ -60,21 +60,48 @@ from (`compat-tests.md`).
## The sequence ## The sequence
The install is native: `/opt/stalwart` with its data in `/opt/stalwart/data` Side by side, not in place (John, 2026-09-19). The Enterprise install stays
and a `stalwart.service` unit. The fork ships `inbuxa.service` with the same where it is, untouched, and the fork goes in beside it with a copy of the
shape, including `AmbientCapabilities=CAP_NET_BIND_SERVICE`, which is what data. That makes the rollback a service swap rather than a restore, and it
lets an unprivileged process hold port 25. is the main reason to prefer this shape.
1. Announce the window. Mail will queue at the sending side while the server The install is native: `/opt/stalwart`, its data in `/opt/stalwart/data`,
is down; nothing is lost, but delivery is delayed. a `stalwart.service` unit, and ihasmail in a container beside it. The fork
2. `systemctl stop stalwart`. ships `inbuxa.service` with the same shape, including
3. Snapshot `/opt/stalwart/data` — the whole directory, from the stopped `AmbientCapabilities=CAP_NET_BIND_SERVICE`, which is what lets an
server. This is the rollback. unprivileged process hold port 25.
4. Install the fork's binary and unit. Keep the Enterprise binary where a
rollback can reach it without a download. 1. Announce the window. Mail queues at the sending side while the server is
5. Start the fork. Watch the log before opening the ports to the world. down; nothing is lost, but delivery is delayed.
6. Work through the checks below. 2. Stop `stalwart.service` and the ihasmail container. **Disable the unit
7. Let mail flow. too**, so a reboot can't bring the Enterprise build back onto the ports
while the fork is serving. Two servers on one set of ports, each with its
own store, is the one failure this plan can produce that the in-place
swap can't.
3. Copy `/opt/stalwart/data` to the fork's store path. A copy, not a move:
the original is the rollback. Budget the disk for two full copies, and
take it from the stopped server, never from under a running one.
4. Install the fork at its own path with its own config, pointing at the
copied store. Most settings travel inside the store — they live in the
registry — so the config file is mainly the store path and the hostname.
New installs default to `/var/lib/inbuxa`; an existing install keeps
whatever its configuration names (SPEC.md §2.5), so this is a free
choice as long as the config and the unit agree.
5. Start the fork. Read the log before opening the ports.
6. Bring up INBUXA Admin and the new webmail. Both talk to the server over
JMAP and OAuth, and neither runs on the mail host's web interface
(SPEC.md §5.3); beside it in containers is fine.
7. Work through the checks below.
8. Let mail flow.
### What the front ends need
Their OAuth client registrations live in the store, so they travel with the
copy. A front end that reuses the old client id and redirect URIs keeps
working; one deployed under a new hostname, or registered afresh, needs its
client set up and its redirect URIs to match, or sign-in fails in a way that
looks like an account problem and isn't. Settle which of the two it is
before the day, not during it.
## Before letting mail flow ## Before letting mail flow
@@ -86,20 +113,33 @@ lets an unprivileged process hold port 25.
- The tenant's administrator can still see its own accounts and domains, and - The tenant's administrator can still see its own accounts and domains, and
nobody else's. nobody else's.
- The queue is draining, and the task queue has no new permanent failures. - The queue is draining, and the task queue has no new permanent failures.
- **Every account still works**, which is the plan's own test: each one
signs in with the password it had, its mail is there, its aliases still
receive, and app passwords and OAuth sessions are accepted. Nothing in
the move touches credentials — they are in the store that was copied —
so a failure here means the copy or the front ends, not the accounts.
- Nothing is listening on the old install's ports but the fork, and
`stalwart.service` is disabled.
## Rollback ## Rollback
Restoring the snapshot from step 3 and starting the Enterprise build returns The Enterprise install and its data are untouched, so rolling back is: stop
the server to exactly where it was, and loses every message accepted since. the fork, re-enable and start `stalwart.service`, bring the old ihasmail
That is clean while the window is short and nothing has been accepted. container back. Minutes, and no restore.
**Rolling back after the fork has been serving is not a snapshot restore.** What it costs is everything the fork accepted while it served: the two
The fork writes objects its own features own — SCIM's event ids and the stores diverge from the moment the fork starts. Mail delivered to the fork's
rebuilt features' records — into a store the Enterprise build would then store is not in the Enterprise one, and rolling back leaves it behind. So
read. Whether it tolerates them has never been tested, and testing it is the decision to roll back is worth making early and deliberately, and the
worth an hour before the day rather than an argument at 2am. Until it is window between "started" and "letting mail flow" is where a rollback is
tested, treat the cutover as forward-only once mail has been accepted, and free.
make the decision to roll back early rather than late.
**Rolling the data back is a different thing, and untested.** Pointing the
Enterprise build at the store the fork has been writing to would ask it to
read objects the fork's own features own. Whether it tolerates them has
never been checked, and checking it is worth an hour beforehand rather than
an argument at 2am. Until it is checked, treat the fork's store as
forward-only.
## The first week ## The first week
@@ -112,8 +152,9 @@ make the decision to roll back early rather than late.
## Open ## Open
- Whether the Enterprise build can read a store the fork has written, which - Whether the Enterprise build can read a store the fork has written. With
decides whether rollback stays available after mail has been accepted. the side-by-side plan this only matters if the fork's store has to be
carried back, since the rollback path is the old store, untouched.
- Whether ACME renewal works on the host, which the test suite has not been - Whether ACME renewal works on the host, which the test suite has not been
able to settle (`container-tests.md`). able to settle (`container-tests.md`).
- Whether the front ends need anything at cutover, or follow separately - Whether the front ends need anything at cutover, or follow separately