From 4cd7b895e9e59f3208477ccb42002422d8080a13 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 27 Aug 2026 22:04:13 -0700 Subject: [PATCH] Deploy immutably when asked to IHASMAIL_IMMUTABLE=1 runs the container the way the README's "Running immutably" section describes: read-only root filesystem, no volume, sessions held in memory. Until now that shape could be run by hand but not deployed -- the run line mounted the data volume unconditionally, so a redeploy would have quietly put a mutable container back. The switch is one variable and nothing else. IMMUTABLE=1 is passed to the server too, which checks the claim rather than believing it, so a half-applied switch refuses to start instead of looking fine until the next redeploy signs everyone out. SESSION_FILE is cleared with -e rather than by editing the environment file, because -e wins over --env-file; that keeps going back a matter of changing the same one variable: IHASMAIL_IMMUTABLE=0 ./ihasmail-deploy.sh --yes which reproduces the previous run line exactly. The named volume is never touched in either mode, so the sessions that were in it when the switch was thrown are still there to come back to. --- deploy.example.sh | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/deploy.example.sh b/deploy.example.sh index 586aab7..69745de 100755 --- a/deploy.example.sh +++ b/deploy.example.sh @@ -41,8 +41,23 @@ HOLD="${IHASMAIL_HOLD:-$APP/.deploy-hold}" # for a reverse proxy in front (see Caddyfile.example / nginx.example.conf). NAME="${IHASMAIL_NAME:-ihasmail}" BIND="${IHASMAIL_BIND:-127.0.0.1:8090}" -# Named volume for /data (sessions). +# Named volume for /data (sessions). Unused when running immutably. VOLUME="${IHASMAIL_VOLUME:-ihasmail-data}" +# Run the container immutably: read-only root filesystem, no volume, sessions +# held in memory only. See "Running immutably" in the README. The server is told +# the same thing through IMMUTABLE=1 and checks it, so a half-applied switch -- +# the flag without the read-only filesystem, or a SESSION_FILE still pointing +# somewhere -- refuses to start here instead of looking fine until the next +# redeploy signs everyone out. +# +# The standing cost is that sessions do not outlive a deploy, because there is +# nowhere left to keep them. Going back is this variable and nothing else: +# +# IHASMAIL_IMMUTABLE=0 ./ihasmail-deploy.sh --yes +# +# The named volume is never touched either way, so whatever was in it when the +# switch was thrown is still there to come back to. +IMMUTABLE="${IHASMAIL_IMMUTABLE:-0}" # Image repository. Each build is tagged with its version as well, so an # earlier one can be run again without rebuilding it. IMAGE_REPO="${IHASMAIL_IMAGE:-ihasmail}" @@ -194,10 +209,19 @@ docker build \ -t "$IMAGE_REPO:current" \ . -echo "==> restarting container" +RUN_ARGS=(-d --name "$NAME" --restart unless-stopped -p "$BIND:8080" --env-file "$ENVF") +if [ "$IMMUTABLE" = "1" ]; then + # -e wins over --env-file, so this clears a SESSION_FILE set there or baked + # into the image, rather than needing the environment file edited to match. + RUN_ARGS+=(--read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE=) + echo "==> restarting container -- immutable: read-only, no volume, sessions in memory" + echo " (everyone signed in is signed out; IHASMAIL_IMMUTABLE=0 puts it back)" +else + RUN_ARGS+=(-v "$VOLUME:/data") + echo "==> restarting container" +fi docker rm -f "$NAME" >/dev/null 2>&1 || true -docker run -d --name "$NAME" --restart unless-stopped \ - -p "$BIND:8080" --env-file "$ENVF" -v "$VOLUME:/data" "$IMAGE_REPO:$TAG" >/dev/null +docker run "${RUN_ARGS[@]}" "$IMAGE_REPO:$TAG" >/dev/null for _ in $(seq 1 "$HEALTH_TIMEOUT"); do if health=$(curl -sf "http://$BIND/api/health"); then