Running immutably¶
This page helps you decide whether to run ihasmail immutably: with a read-only filesystem and no disk volume at all.
Immutable means exactly that. Not "mostly stateless", and not "stateless if you don't count the cache". The container keeps nothing of its own. Delete it, and you lose nothing that wasn't already in Stalwart.
In short: it makes ihasmail simpler to run, upgrade and trust. The one cost is that every deploy signs everyone out. If that is fine for you, it is the recommended setup, and the quick start uses it.
What you get¶
- An instance is its image plus a settings file. Those two things decide what you are running. Nothing builds up on the server over time.
- Upgrades are replacements. There is no database to migrate: pull the new image and replace the container. To roll back, run the previous tag — no saved state can have changed on the way.
- Nothing to back up. Back up Stalwart, which you were doing anyway.
- No drift. A read-only filesystem collects no stray cache, no half-written upload, and no file an old version left behind for a new one to trip over.
- An attacker cannot stay. Someone who finds a way to write a file cannot keep it: no planted web page, no changed JavaScript served to the next visitor, no scheduled task that survives a restart. That does not make the app invulnerable — see what this does not mean — but it removes the step that turns a break-in into a lasting problem.
What it costs¶
Every deploy signs everyone out
Sessions are kept in memory, because there is nowhere else for them. Replace the container and they are gone, so everyone signs in again afterwards.
That is the whole trade. There is no clever workaround hiding in the configuration.
Whether that is a cost or a benefit depends on how you work.
It is a benefit if your deploys happen in a maintenance window anyway. A fresh sign-in means nobody is left running yesterday's app against today's server, and you never wonder which version a tab is on.
It is a cost if people keep ihasmail open all day and would be annoyed to
sign in again whenever you patch. Then leave IMMUTABLE unset and mount a
volume for SESSION_FILE — see Keeping sessions across
restarts. The IMMUTABLE setting
is off unless you set it, and nothing else about ihasmail changes.
Open tabs are looked after either way. After a deploy, the app notices the server's version has changed and reloads itself onto the new one. It checks on its next request, and on a slow timer while the tab is visible, so nobody is left on old code.
Why it is possible¶
ihasmail has no database of its own, because Stalwart already is the
database. Mail, calendars, contacts, files and filters live there. So do each
person's settings, in a settings.json in their own account's files. So do
signatures, notification subscriptions and filter scripts.
That left exactly one place in the whole server that ever wrote to disk: an optional session file. Switch it off and there is nothing left to write.
grep -r "writeFile\|createWriteStream\|mkdir\|rename" server/src/
→ sessions.ts the optional session file
→ config.ts the startup probe, which writes only to prove that it cannot
The second is the check below, not somewhere anything is kept.
How to run it¶
services:
ihasmail:
image: ghcr.io/coffey-labs/ihasmail:latest
read_only: true
tmpfs:
- /tmp
environment:
IMMUTABLE: "1"
SESSION_FILE: ""
env_file: .env
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
There is no volumes: key. That is the point.
The example deploy script, deploy.example.sh, runs immutably by default.
Turning it off has to be deliberate: IHASMAIL_IMMUTABLE=0. If your host
wrapper sets the variable, write it as ${IHASMAIL_IMMUTABLE:-1}, so a
plain deploy keeps the mode and a one-off override still works.
Each flag does a different job:
--read-onlyis what actually makes the filesystem read-only.IMMUTABLE=1only claims you did it — and then checks.--tmpfs /tmpgives the process a scratch folder in memory. Nothing in ihasmail writes there, but Node and its standard library expect it to exist.-e SESSION_FILE=(empty) is needed because the image sets a default of/data/sessions.json. A-eon the command line beats--env-file, so this clears it without editing your settings file.
The server checks the claim¶
IMMUTABLE=1 does not change what ihasmail does. It is a promise the server
checks, and it refuses to start when the promise is false:
Error: IMMUTABLE is set, but SESSION_FILE is /data/sessions.json. An immutable
instance keeps no durable state of its own: pass SESSION_FILE= (empty) to hold
sessions in memory, or unset IMMUTABLE.
Error: IMMUTABLE is set, but /app/ is writable. Run the container with
--read-only (and --tmpfs /tmp), or unset IMMUTABLE.
Why check, when it would work anyway?
Because without the check, getting it wrong is silent. Saving sessions
is best-effort: a read-only /data produces one warning at the first
sign-in and nothing else. The instance looks healthy, reports healthy, and
serves mail perfectly — right up until it is replaced and everybody is
signed out with no explanation.
A claim nobody checks is just a comment. This one is checked.
What this does not mean¶
The limits, stated plainly, because an overstated claim is worse than a modest one:
- It is not a security guarantee. A read-only filesystem stops an attacker staying. It does not stop them reading what the process can read, or using its credentials while it runs. It removes one kind of outcome, not the bug underneath.
- It is not sharing sessions between copies. Sessions live in each container's own memory, so two ihasmail containers behind a load balancer sign people out as they bounce between them — unless the load balancer keeps each person on the same copy. Running more than one ihasmail explains how.
- It says nothing about keeping your mail safe. Your mail lives in Stalwart and always did. Immutability is about ihasmail's container, not your data — which still needs Stalwart backed up like any other mail server.
- It is not a speed feature. It will not be faster. It will be simpler, which is a different and longer-lasting kind of win.
Check it yourself¶
Don't take the setting's word for it:
# no writable root
docker inspect ihasmail --format '{{.HostConfig.ReadonlyRootfs}}'
# → true
# no volumes at all, anonymous ones included
docker inspect ihasmail --format '{{json .Mounts}}'
# → []
# and prove it from inside
docker exec ihasmail node -e 'try{require("fs").writeFileSync("/app/x","x");console.log("WRITABLE")}catch(e){console.log(e.code)}'
# → EROFS
Run the middle check even if you are confident. A VOLUME line in a
Dockerfile makes Docker mount an unnamed volume whether you asked for one or
not. That volume stays writable under --read-only — a writable hole in a
container you believed had none. ihasmail's own image had exactly that bug until
2.16.117. If you are checking some other image, look at .Mounts before you
believe it.
Going back¶
One variable, and nothing else:
The deploy script never touches its named volume in either mode. The sessions that were in it when you switched are still there when you switch back.