Prep for the immutability roadmap. No behaviour changes for any existing deployment; the new mode is opt-in and off by default.
What's here
VOLUME ["/data"] is gone from the Dockerfile. It reads like documentation, but Docker acts on it — a container started without -v gets an anonymous volume mounted at /data, and that mount stays writable even under --read-only. So it put a writable hole in a container meant to be immutable, and orphaned a volume every time one was replaced, while never persisting anything across a redeploy (each new container got a fresh empty volume). docker-compose.yml and deploy.example.sh both mount a named volume and are unaffected.
IMMUTABLE=1 asserts the instance is running immutably, and the server checks it. It refuses to start if SESSION_FILE is still set, or if the filesystem it is installed on turns out to be writable. Unchecked, the misconfiguration is silent: persisting sessions is best-effort, so a read-only /data costs one warning at the first sign-in and nothing else until the instance is replaced and everyone is signed out.
SessionBackend names what the rest of the server asks of a session store, and sessions in app.ts is typed as it. SessionStore is still the only implementation and nothing changes today. It exists so the OAuth work is written against an interface rather than a class, and so the interface can record which methods a stateless backend could satisfy alone — create/resolve/reseal/destroy each touch one session, while listForUser and destroyAllForUser have to reach sessions other than the caller's. The latter is what makes "changing your password signs out your other sessions" true (app.ts:335, app.ts:429), which is why it needs a registry.
Why this is possible at all
sessions.ts is the only thing in server/src/ that writes to disk — lines 1, 97, 99, 100. static.ts and config.ts only read, and uploads stream through byteCap without touching disk.
IMMUTABLE=1, SESSION_FILE left at the image default
refuses: "SESSION_FILE is /data/sessions.json …"
IMMUTABLE=1, --read-only forgotten
refuses: "/app/ is writable …"
no IMMUTABLE, named volume (today's deploy)
healthy, ihm-v-vol -> /data RW=true — unchanged
no IMMUTABLE, no -v
healthy, Mounts: [] — no more anonymous volume
npm run typecheck, npm test (80 pass, 3 new), npm run build, and docker build all clean.
Not here
The stateless session backend. Today IMMUTABLE=1 means a restart signs everyone out, because sessions have nowhere to live. Removing that cost means moving the session upstream into a token Stalwart issues and can revoke — the OAuth work in ROADMAP.md, which also restores the two registry-dependent methods above.
Prep for the immutability roadmap. No behaviour changes for any existing deployment; the new mode is opt-in and off by default.
### What's here
**`VOLUME ["/data"]` is gone from the Dockerfile.** It reads like documentation, but Docker acts on it — a container started without `-v` gets an anonymous volume mounted at `/data`, and that mount stays writable even under `--read-only`. So it put a writable hole in a container meant to be immutable, and orphaned a volume every time one was replaced, while never persisting anything across a redeploy (each new container got a fresh empty volume). `docker-compose.yml` and `deploy.example.sh` both mount a *named* volume and are unaffected.
**`IMMUTABLE=1` asserts the instance is running immutably, and the server checks it.** It refuses to start if `SESSION_FILE` is still set, or if the filesystem it is installed on turns out to be writable. Unchecked, the misconfiguration is silent: persisting sessions is best-effort, so a read-only `/data` costs one warning at the first sign-in and nothing else until the instance is replaced and everyone is signed out.
**`SessionBackend` names what the rest of the server asks of a session store**, and `sessions` in `app.ts` is typed as it. `SessionStore` is still the only implementation and nothing changes today. It exists so the OAuth work is written against an interface rather than a class, and so the interface can record which methods a stateless backend could satisfy alone — `create`/`resolve`/`reseal`/`destroy` each touch one session, while `listForUser` and `destroyAllForUser` have to reach sessions other than the caller's. The latter is what makes "changing your password signs out your other sessions" true (`app.ts:335`, `app.ts:429`), which is why it needs a registry.
### Why this is possible at all
`sessions.ts` is the only thing in `server/src/` that writes to disk — lines 1, 97, 99, 100. `static.ts` and `config.ts` only read, and uploads stream through `byteCap` without touching disk.
### Verified
Built and run as a container in all five shapes:
| | result |
|---|---|
| `--read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE=` | healthy, `Mounts: []`, `/data` → `EROFS` |
| `IMMUTABLE=1`, `SESSION_FILE` left at the image default | refuses: *"SESSION_FILE is /data/sessions.json …"* |
| `IMMUTABLE=1`, `--read-only` forgotten | refuses: *"/app/ is writable …"* |
| no `IMMUTABLE`, named volume (today's deploy) | healthy, `ihm-v-vol -> /data RW=true` — unchanged |
| no `IMMUTABLE`, no `-v` | healthy, `Mounts: []` — no more anonymous volume |
`npm run typecheck`, `npm test` (80 pass, 3 new), `npm run build`, and `docker build` all clean.
### Not here
The stateless session backend. Today `IMMUTABLE=1` means a restart signs everyone out, because sessions have nowhere to live. Removing that cost means moving the session upstream into a token Stalwart issues and can revoke — the OAuth work in ROADMAP.md, which also restores the two registry-dependent methods above.
**Merged** 2026-08-27 as coffey-labs/ihasmail@37bf96409d22
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Prep for the immutability roadmap. No behaviour changes for any existing deployment; the new mode is opt-in and off by default.
What's here
VOLUME ["/data"]is gone from the Dockerfile. It reads like documentation, but Docker acts on it — a container started without-vgets an anonymous volume mounted at/data, and that mount stays writable even under--read-only. So it put a writable hole in a container meant to be immutable, and orphaned a volume every time one was replaced, while never persisting anything across a redeploy (each new container got a fresh empty volume).docker-compose.ymlanddeploy.example.shboth mount a named volume and are unaffected.IMMUTABLE=1asserts the instance is running immutably, and the server checks it. It refuses to start ifSESSION_FILEis still set, or if the filesystem it is installed on turns out to be writable. Unchecked, the misconfiguration is silent: persisting sessions is best-effort, so a read-only/datacosts one warning at the first sign-in and nothing else until the instance is replaced and everyone is signed out.SessionBackendnames what the rest of the server asks of a session store, andsessionsinapp.tsis typed as it.SessionStoreis still the only implementation and nothing changes today. It exists so the OAuth work is written against an interface rather than a class, and so the interface can record which methods a stateless backend could satisfy alone —create/resolve/reseal/destroyeach touch one session, whilelistForUseranddestroyAllForUserhave to reach sessions other than the caller's. The latter is what makes "changing your password signs out your other sessions" true (app.ts:335,app.ts:429), which is why it needs a registry.Why this is possible at all
sessions.tsis the only thing inserver/src/that writes to disk — lines 1, 97, 99, 100.static.tsandconfig.tsonly read, and uploads stream throughbyteCapwithout touching disk.Verified
Built and run as a container in all five shapes:
--read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE=Mounts: [],/data→EROFSIMMUTABLE=1,SESSION_FILEleft at the image defaultIMMUTABLE=1,--read-onlyforgottenIMMUTABLE, named volume (today's deploy)ihm-v-vol -> /data RW=true— unchangedIMMUTABLE, no-vMounts: []— no more anonymous volumenpm run typecheck,npm test(80 pass, 3 new),npm run build, anddocker buildall clean.Not here
The stateless session backend. Today
IMMUTABLE=1means a restart signs everyone out, because sessions have nowhere to live. Removing that cost means moving the session upstream into a token Stalwart issues and can revoke — the OAuth work in ROADMAP.md, which also restores the two registry-dependent methods above.Merged 2026-08-27 as coffey-labs/ihasmail@37bf96409d
Rebuilt from: git history, session transcript.