~/LINUXexpert$
Articles

ihasmail Goes Immutable: No Volume, No Database, Nothing Left Behind

Every self-hosted app claims to be “stateless” at some point in its life. Usually what that means is: no database. Sometimes it means: no database, but there’s still a config directory somewhere quietly accumulating state that the container can’t live without. ihasmail, our JMAP webmail client for Stalwart Mail Server, just went further than either of those — with a new IMMUTABLE=1 mode that runs the container with a read-only root filesystem and no mounted volume at all.

The problem with “stateless”

Statelessness has degrees, and most software that claims it is really only a rung or two up the ladder:

  1. A database. Configuration and user state live in SQL next to the app. Reproducing an instance means reproducing everything written to that database since install — this is where Roundcube, SOGo, and Open-Xchange sit.
  2. A config directory. No database, but a setup wizard writes settings to a directory that has to stay writable. The container still can’t be read-only, and two installs of the same image aren’t actually the same thing.
  3. A mounted volume. The root filesystem is read-only, which is where most “immutable” deployments stop — including ihasmail’s own default configuration. The state didn’t disappear, though. It just moved to the volume.
  4. Nothing. Read-only root, no volume, no anonymous mount hiding behind one. Delete the container and you lose nothing, because there was nothing in it to lose.

ihasmail now occupies both rung 3 and rung 4, depending on how you run it. IMMUTABLE=1 is what gets you to the top.

What actually changes

ihasmail’s architecture already made this a short trip. The app has no database of its own — every mailbox, calendar, contact, and file lives in Stalwart, and the Node/Hono server in the middle exists only to keep credentials out of the browser and speak JMAP on the page’s behalf. The one thing on disk was an optional session file, written so people stayed logged in across a redeploy.

Flip on IMMUTABLE=1 and SESSION_FILE= (empty), and that last write path goes away too:

bash

docker run --read-only --tmpfs /tmp \
  -e IMMUTABLE=1 -e SESSION_FILE= \
  --env-file .env.production ihasmail:2.16

Crucially, the flag isn’t decorative. The server checks its own claim at boot rather than asking you to trust it — it refuses to start if a session file is still configured, or if the filesystem it’s running on turns out to be writable after all. A half-applied “immutable” setup fails loudly at startup instead of looking healthy right up until the next deploy goes sideways.

At that point, an ihasmail instance is nothing more than its image plus an environment file. Two hosts given the same two things are running the same thing, full stop. Upgrading stops being an operation and becomes a replacement: pull the new tag, throw away the old container, done. Nothing to migrate, nothing to back up, nothing to reconcile.

The trade-off, stated plainly

There’s exactly one cost, and it’s worth being honest about it: sessions live in memory when there’s nowhere else for them to live. Every deploy signs everyone out. On a webmail client, that’s not nothing.

For some operators, that’s actually the point — deploys land in a scheduled maintenance window and a forced sign-in guarantees nobody is left running yesterday’s build against today’s server. Open tabs help here too: after a deploy they notice the version has moved and reload themselves onto the new one rather than limping along on stale JS.

If you’d rather people stayed signed in across a restart, don’t set IMMUTABLE. Mount a volume for the session file and ihasmail behaves exactly as it always has — the mode is opt-in and off by default. This isn’t a case of the “right” way and the “old” way; it’s a knob for operators who know which trade-off they want.

Don’t take our word for it

The whole claim is checkable in two Docker commands, and it’s worth checking on anything that makes this kind of claim, not just ihasmail:

bash

# is the root filesystem actually read-only?
docker inspect <name> --format '{{.HostConfig.ReadonlyRootfs}}'

# and is anything mounted, anonymous volumes included?
docker inspect <name> --format '{{json .Mounts}}'

That second command is the one people skip, and it’s the one that matters. A VOLUME line in a Dockerfile makes Docker mount an anonymous volume whether you asked for one or not — and that mount stays writable under --read-only, which is a hole in a container you believed had none.

We know because ihasmail’s own image had exactly that bug until version 2.16.117. It was caught by running the check above, not by trusting the flag — which is the whole reason we’re telling you to run it yourself instead of taking our description at face value.

Where this leaves the field

There’s one other JMAP-native, AGPL webmail client worth naming here: Bulwark. On protocol, it’s a peer, and none of the usual arguments against legacy IMAP webmail apply to it. But it still ships a setup wizard that writes configuration to a directory that has to stay read-write, so an instance is the image plus whatever the wizard put there — state that has to be reproduced, backed up, and debugged separately from the container itself. That’s the gap IMMUTABLE=1 closes: the difference between shipping a container and shipping a container plus a procedure.

Try it

ihasmail is AGPL-3.0-or-later, targets Stalwart 0.16+, and is free of any paid tier or feature gate. The source, docs, and the read-only check above are all on GitHub:

If you’re running Stalwart already, IMMUTABLE=1 is a one-line addition to a docker run you likely already have. Run the inspect commands afterward — on ihasmail or on whatever else claims to be stateless.