### Install and run from source with npm Source: https://docs.ihasmail.org/install Installs dependencies, builds the web and server bundles, and starts the production server on port 8080. A source install is not immutable; npm start runs as an ordinary Node process on a writable filesystem. ```bash npm install npm run build # web/dist + server/dist npm start # serves the production build on :8080 ``` -------------------------------- ### Build and run production from source Source: https://docs.ihasmail.org/install?q= Installs dependencies, builds the web and server distributions, and starts the production server on port 8080. ```bash npm install npm run build # web/dist + server/dist npm start # serves the production build on :8080 ``` -------------------------------- ### Run source install under a memory cap with systemd-run Source: https://docs.ihasmail.org/install Runs npm start under a cgroup with a 512 MiB memory cap and no swap, reproducing the container's behavior where one process dies and everything else stays up. Without a cap, the process grows until the host's OOM killer chooses a victim. ```bash systemd-run --user --scope -p MemoryMax=512M -p MemorySwapMax=0 npm start ``` -------------------------------- ### Run development modes with npm run dev and dev:mock Source: https://docs.ihasmail.org/install Development modes for a source install. npm run dev starts the server on :8080 and Vite on :5173 against a real Stalwart using STALWART_URL in .env; npm run dev:mock uses the built-in mock, which signs in as demo@example.com / demo. ```bash npm run dev # against a real Stalwart (STALWART_URL in .env) npm run dev:mock # against the built-in mock — no real mailbox needed ``` -------------------------------- ### Development modes for source install Source: https://docs.ihasmail.org/install?q= Two development modes: one against a real Stalwart server (requires STALWART_URL in .env) and one against the built-in mock. The dev server runs on :8080 and Vite on :5173. ```bash npm run dev # against a real Stalwart (STALWART_URL in .env) npm run dev:mock # against the built-in mock — no real mailbox needed ``` -------------------------------- ### Run ihasmail with a mounted policy file (docker run + policy.json) Source: https://docs.ihasmail.org/configure?q= Mount a policy file read-only into the container and pass its path via SETTINGS_POLICY_FILE. The policy.json example shows the defaults, enforced, and changes sections; the server reads the file once at startup and there is no reload signal. ```bash docker run -d --name ihasmail \ -e STALWART_URL=https://mail.example.org \ -e APP_SECRET="$(openssl rand -hex 32)" \ -e SETTINGS_POLICY_FILE=/etc/ihasmail/policy.json \ -v /srv/ihasmail/policy.json:/etc/ihasmail/policy.json:ro \ -p 8080:8080 ghcr.io/coffey-labs/ihasmail:latest ``` ```json { "defaults": { "externalSenderBanner": true }, "enforced": { "externalRecipientConfirm": true }, "changes": [ { "version": "20260902084513", "settings": { "externalSenderBanner": true } }, { "version": "20261014091500", "settings": { "externalLinkWarning": true } } ] } ``` -------------------------------- ### IMMUTABLE verification errors Source: https://docs.ihasmail.org/immutable?q= The server refuses to start when IMMUTABLE=1 is set but the promise is false. The first error appears when SESSION_FILE still points to /data/sessions.json; the second when /app/ is writable. Both require either fixing the configuration or unsetting IMMUTABLE. ```text 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. ``` ```text Error: IMMUTABLE is set, but /app/ is writable. Run the container with --read-only (and --tmpfs /tmp), or unset IMMUTABLE. ``` -------------------------------- ### Start ihasmail with docker compose up Source: https://docs.ihasmail.org/install Start ihasmail with docker compose to keep sessions across restarts. It publishes port 8080, mounts the named volume ihasmail-data at /data where SESSION_FILE defaults to /data/sessions.json, and sets TRUST_PROXY=1 and IMAGE_PROXY=1. This gives the instance its own state, unlike the immutable shape. ```bash docker compose up --build -d ``` -------------------------------- ### docker run command for ihasmail with Stalwart Source: https://docs.ihasmail.org/configure?q= Starts ihasmail with STALWART_URL, STALWART_SERVERS_FILE, and APP_SECRET. Mounts the servers.json file read-only and exposes port 8080. ```bash docker run -d --name ihasmail \ -e STALWART_URL=https://mail.example.org \ -e STALWART_SERVERS_FILE=/etc/ihasmail/servers.json \ -v /srv/ihasmail/servers.json:/etc/ihasmail/servers.json:ro \ -e APP_SECRET="$(openssl rand -hex 32)" \ -p 8080:8080 ghcr.io/coffey-labs/ihasmail:latest ``` -------------------------------- ### Run ihasmail with Docker (immutable mode) Source: https://docs.ihasmail.org/immutable Starts an immutable ihasmail container with a read-only filesystem, a tmpfs for /tmp, and an empty SESSION_FILE. Requires an .env.production file and maps host port 8090 to container port 8080. ```bash docker run -d --name ihasmail --restart unless-stopped \ --read-only --tmpfs /tmp \ -e IMMUTABLE=1 -e SESSION_FILE= \ --env-file .env.production \ -p 127.0.0.1:8090:8080 \ ihasmail:local ``` -------------------------------- ### Deploy with deploy.sh: default, dry-run, and versioned Source: https://docs.ihasmail.org/install Run the deploy script with default settings, a dry run, or a specific version with automatic confirmation. The default uses origin/main and prompts before shipping new commits; --dry-run runs the guards and stops; a named ref with --yes skips the prompt, useful over SSH where there is no tty. ```shell ./deploy.sh # origin/main, asks before shipping new commits ./deploy.sh --dry-run # run the guards and stop ./deploy.sh v2026.8.30 --yes # a named ref, no prompt (there is no tty over ssh) ``` -------------------------------- ### Pass a policy file to Docker with SETTINGS_POLICY_FILE Source: https://docs.ihasmail.org/configure Mount a policy file read-only into the container and name it with SETTINGS_POLICY_FILE. The server only reads the file at startup; there is no reload signal, so editing the policy requires restarting the container. ```bash docker run -d --name ihasmail \ -e STALWART_URL=https://mail.example.org \ -e APP_SECRET="$(openssl rand -hex 32)" \ -e SETTINGS_POLICY_FILE=/etc/ihasmail/policy.json \ -v /srv/ihasmail/policy.json:/etc/ihasmail/policy.json:ro \ -p 8080:8080 ghcr.io/coffey-labs/ihasmail:latest ``` ```json { "defaults": { "externalSenderBanner": true }, "enforced": { "externalRecipientConfirm": true }, "changes": [ { "version": "20260902084513", "settings": { "externalSenderBanner": true } }, { "version": "20261014091500", "settings": { "externalLinkWarning": true } } ] } ``` -------------------------------- ### Run ihasmail immutably with docker run Source: https://docs.ihasmail.org/install Build and run ihasmail in the recommended immutable mode: the container is read-only with no volume, so the instance is its image plus an environment file. Set STALWART_URL and APP_SECRET in .env first; the run publishes to 127.0.0.1:8090 for a reverse proxy and uses IMMUTABLE=1 with SESSION_FILE empty. IMMUTABLE=1 is checked at startup, so a half-applied switch refuses to boot, and sessions live in memory, signing everyone out on each deploy. ```bash git clone https://github.com/Coffey-Labs/ihasmail.git cd ihasmail cp .env.example .env # edit .env: STALWART_URL, and APP_SECRET=$(openssl rand -base64 48) docker build -t ihasmail:local . docker run -d --name ihasmail --restart unless-stopped \ --read-only --tmpfs /tmp \ -e IMMUTABLE=1 -e SESSION_FILE= \ --env-file .env -p 127.0.0.1:8090:8080 \ ihasmail:local ``` -------------------------------- ### Find disk-write operations in server source Source: https://docs.ihasmail.org/immutable?q= Use this command to locate every disk-write operation in the server source. The output shows that only two files write: sessions.ts, which holds the optional session file, and config.ts, which performs a startup probe that writes only to prove it cannot. This confirms that with the session file disabled, no other path writes to disk. ```shell 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 ``` -------------------------------- ### Build Docker image with version from commit Source: https://docs.ihasmail.org/install?q= Computes the version from the commit date and PR/short SHA, then builds the image with IHASMAIL_VERSION set. The tag replaces '+' with '-' because Docker tags cannot contain '+'. If omitted, the build reports 0.0.0 instead of failing. ```bash V="$(node scripts/version.mjs)" # 2026.8.30+pr129 docker build --build-arg IHASMAIL_VERSION="$V" -t "ihasmail:${V/+/-}" . ``` -------------------------------- ### Server error: /app/ writable in immutable mode Source: https://docs.ihasmail.org/immutable Error shown when IMMUTABLE=1 is set but the filesystem is not read-only. Instructs to run with --read-only and --tmpfs /tmp, or unset IMMUTABLE. ```text Error: IMMUTABLE is set, but /app/ is writable. Run the container with --read-only (and --tmpfs /tmp), or unset IMMUTABLE. ``` -------------------------------- ### Verify container immutability with docker inspect and docker exec Source: https://docs.ihasmail.org/immutable?q= Run these three commands to verify the container's root filesystem is read-only, that no volumes are mounted, and that writing to the filesystem fails with EROFS. The middle check is important because a VOLUME instruction in a Dockerfile can create a writable anonymous volume even under --read-only. ```bash docker inspect ihasmail --format '{{.HostConfig.ReadonlyRootfs}}' # → true docker inspect ihasmail --format '{{json .Mounts}}' # → [] docker exec ihasmail node -e 'try{require("fs").writeFileSync("/app/x","x");console.log("WRITABLE")}catch(e){console.log(e.code)}' # → EROFS ``` -------------------------------- ### Run ihasmail immutable with Docker, Compose, and deploy script Source: https://docs.ihasmail.org/immutable?q= Run ihasmail in immutable mode with a read-only filesystem and in-memory sessions. The Docker command and Compose service both set IMMUTABLE=1, SESSION_FILE= (empty), --read-only/read_only: true, and --tmpfs /tmp, and expose port 8090 on localhost. The deploy script sets IHASMAIL_IMMUTABLE=1 so a plain deploy keeps the mode; write it as ${IHASMAIL_IMMUTABLE:-1} to allow a one-off override. No volumes: key is intentional. ```bash docker run -d --name ihasmail --restart unless-stopped \ --read-only --tmpfs /tmp \ -e IMMUTABLE=1 -e SESSION_FILE= \ --env-file .env.production \ -p 127.0.0.1:8090:8080 \ ihasmail:local ``` ```yaml services: ihasmail: image: ihasmail:local read_only: true tmpfs: - /tmp environment: IMMUTABLE: "1" SESSION_FILE: "" env_file: .env.production ports: - "127.0.0.1:8090:8080" restart: unless-stopped ``` ```bash IHASMAIL_IMMUTABLE=1 ./ihasmail-deploy.sh --yes ``` -------------------------------- ### Run immutably: -e IMMUTABLE=1 -e SESSION_FILE= Source: https://docs.ihasmail.org/troubleshooting?q= Run the container immutably by setting IMMUTABLE=1 and clearing SESSION_FILE. The image defaults SESSION_FILE to /data/sessions.json, so this explicit empty value is required. -e overrides --env-file, so this works without editing the environment file. ```bash -e IMMUTABLE=1 -e SESSION_FILE= ``` -------------------------------- ### Build Docker image with IHASMAIL_VERSION Source: https://docs.ihasmail.org/rebranding Build the Docker image with the version passed as a build argument. The command uses the output of `node scripts/version.mjs` as the value for `IHASMAIL_VERSION`. The `.dockerignore` file keeps `.git` out of the build context, so the version must be passed explicitly. ```bash docker build --build-arg IHASMAIL_VERSION="$(node scripts/version.mjs)" -t acme-mail:local . ``` -------------------------------- ### Switch back to writable mode with IHASMAIL_IMMUTABLE=0 Source: https://docs.ihasmail.org/immutable?q= Set the IHASMAIL_IMMUTABLE environment variable to 0 and run the deploy script with --yes to switch back to a writable mode. The named volume is never touched in either mode, so existing sessions remain intact. ```bash IHASMAIL_IMMUTABLE=0 ./ihasmail-deploy.sh --yes ``` -------------------------------- ### Build Docker image with IHASMAIL_VERSION Source: https://docs.ihasmail.org/install Build the Docker image with a version passed as a build argument. The version is computed by a Node script and the tag replaces '+' with '-' because Docker tags cannot contain '+'. If omitted, the build reports 0.0.0 instead of failing. ```shell V="$(node scripts/version.mjs)" # 2026.8.30+pr129 docker build --build-arg IHASMAIL_VERSION="$V" -t "ihasmail:${V/+/-}" . ``` -------------------------------- ### Architecture diagram: browser to ihasmail to Stalwart Source: https://docs.ihasmail.org/troubleshooting Shows the three components between a reader and their mail: the browser (React SPA), the ihasmail server (Node + Hono), and Stalwart (the mail server). Use this to identify which layer a problem belongs to. ```text browser ──(/api/*)──► ihasmail server ──(JMAP)──► Stalwart React SPA Node + Hono the mail server ``` -------------------------------- ### Run container read-only with tmpfs Source: https://docs.ihasmail.org/troubleshooting Add --read-only (or read_only: true in compose) to make the container actually read-only when IMMUTABLE is set, plus a tmpfs for scratch. Without this, the container exits with 'IMMUTABLE is set, but /app/ is writable'. ```bash --read-only --tmpfs /tmp ``` -------------------------------- ### Server error: SESSION_FILE not empty in immutable mode Source: https://docs.ihasmail.org/immutable Error shown when IMMUTABLE=1 is set but SESSION_FILE is not empty. Instructs to pass SESSION_FILE= (empty) or unset IMMUTABLE. ```text 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. ``` -------------------------------- ### servers.json mapping domains to Stalwart servers Source: https://docs.ihasmail.org/configure?q= JSON file referenced by STALWART_SERVERS_FILE. Maps a domain to the Stalwart server URL for one ihasmail in front of several Stalwart servers. ```json { "example.com": "https://mail.example.com", "customer-b.test": "https://jmap.customer-b.test" } ``` -------------------------------- ### Deploy script with immutable mode Source: https://docs.ihasmail.org/immutable Runs the deploy script with IHASMAIL_IMMUTABLE=1 to keep the container immutable. The script is expected to use ${IHASMAIL_IMMUTABLE:-1} to allow one-off overrides. ```bash IHASMAIL_IMMUTABLE=1 ./ihasmail-deploy.sh --yes ``` -------------------------------- ### Run ihasmail with environment-based policy (no file) Source: https://docs.ihasmail.org/configure?q= Run ihasmail without a policy file, using environment variables for defaults, enforced, and changes settings. This is intended for immutable deployments with no volume; the container runs read-only with a tmpfs for /tmp. ```bash docker run -d --name ihasmail --read-only --tmpfs /tmp \ -e IMMUTABLE=1 -e SESSION_FILE= \ -e STALWART_URL=https://mail.example.org \ -e APP_SECRET="$(openssl rand -hex 32)" \ -e SETTINGS_DEFAULTS='{"externalSenderBanner":true}' \ -e SETTINGS_ENFORCED='{"externalRecipientConfirm":true}' \ -e SETTINGS_CHANGES='[{"version":"20260902084513","settings":{"externalSenderBanner":true}}]' \ -p 8080:8080 ghcr.io/coffey-labs/ihasmail:latest ``` -------------------------------- ### Configure ihasmail policy in docker-compose.yml Source: https://docs.ihasmail.org/configure?q= Define the ihasmail service in docker-compose.yml with the policy file mounted read-only and SETTINGS_POLICY_FILE pointing to it. The policy is read once at startup; editing it requires restarting the container. ```yaml services: ihasmail: image: ghcr.io/coffey-labs/ihasmail:latest environment: SETTINGS_POLICY_FILE: /etc/ihasmail/policy.json volumes: - ./policy.json:/etc/ihasmail/policy.json:ro ``` -------------------------------- ### Caddy reverse proxy for ihasmail with SSE Source: https://docs.ihasmail.org/install Caddy configuration to put TLS in front of ihasmail. It enables compression, proxies to loopback, and sets flush_interval -1 to keep Server-Sent Events connections open. The file is available as Caddyfile.example. ```caddy mail.example.com { encode zstd gzip reverse_proxy 127.0.0.1:8080 { # Keep SSE (push) connections open flush_interval -1 } } ``` -------------------------------- ### Health check and Docker logs for ihasmail Source: https://docs.ihasmail.org/troubleshooting Two quick checks to place most faults: curl the health endpoint to see if ihasmail is up, and tail the Docker logs to see what it said when it failed. If /api/health answers but the browser cannot sign in, the fault is between ihasmail and Stalwart. ```bash curl -s http://127.0.0.1:8080/api/health # is ihasmail itself up? docker logs --tail 50 ihasmail # what did it say when it failed? ``` -------------------------------- ### Test Stalwart reachability from inside the ihasmail container Source: https://docs.ihasmail.org/troubleshooting Run inside the ihasmail container to test whether it can reach Stalwart's JMAP endpoint. Use this when the error is "Could not reach the mail server" or "The mail server did not respond in time". The command fetches the JMAP well-known URL as the container would, so it checks DNS and network reachability from inside the container. ```bash docker exec -it ihasmail wget -qO- https://mail.example.com/.well-known/jmap ``` -------------------------------- ### Accent colour definitions in app.css Source: https://docs.ihasmail.org/rebranding Defines the --accent CSS variable for light, dark, house palette, and user accents. The house palette is the default for new accounts; user accents override any theme and should be left alone unless offering a different set. ```css :root { --accent: #0f766e; … } /* light */ :root[data-theme="dark"] { --accent: #2dd4bf; … } /* dark */ :root[data-palette="ihasmail"] { --accent: #46cac3; … } /* the house palette */ :root[data-accent="blue"] { --accent: #2563eb; … } /* user accents */ ``` -------------------------------- ### Make container read-only: --read-only --tmpfs /tmp Source: https://docs.ihasmail.org/troubleshooting?q= Add these flags to make the container actually read-only when IMMUTABLE is set. The error occurs because the variable is set but the container is not read-only, almost always due to a missing --read-only or read_only: true in compose. The tmpfs provides scratch space. ```bash --read-only --tmpfs /tmp ``` -------------------------------- ### Check load-balancer affinity with curl using ihasmail session cookie Source: https://docs.ihasmail.org/troubleshooting Run twice with the same session cookie to test for load-balancer affinity. Alternating 200 and 401 responses indicate that requests are being routed to different instances without session affinity. ```bash curl -s -o /dev/null -w '%{http_code}\n' -b 'ihasmail=' https://mail.example.com/api/auth/session curl -s -o /dev/null -w '%{http_code}\n' -b 'ihasmail=' https://mail.example.com/api/auth/session ``` -------------------------------- ### Docker Compose configuration for immutable ihasmail Source: https://docs.ihasmail.org/immutable Defines the ihasmail service with read_only: true, a tmpfs mount for /tmp, and environment variables IMMUTABLE=1 and SESSION_FILE empty. Uses env_file .env.production and maps port 8090 to 8080. ```yaml services: ihasmail: image: ihasmail:local read_only: true tmpfs: - /tmp environment: IMMUTABLE: "1" SESSION_FILE: "" env_file: .env.production ports: - "127.0.0.1:8090:8080" restart: unless-stopped ``` -------------------------------- ### nginx reverse proxy for ihasmail with SSE Source: https://docs.ihasmail.org/install nginx configuration to put TLS in front of ihasmail. It binds to loopback, disables buffering for Server-Sent Events, and sets a body limit above MAX_UPLOAD_BYTES (60m vs 50 MiB default). The file is available as nginx.example.conf. ```nginx server { listen 443 ssl http2; server_name mail.example.com; # ssl_certificate ...; ssl_certificate_key ...; client_max_body_size 60m; location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Server-Sent Events (push notifications) proxy_buffering off; proxy_read_timeout 3600s; } } ``` -------------------------------- ### Disable proxy buffering for SSE in nginx and Caddy Source: https://docs.ihasmail.org/troubleshooting Disables response buffering and extends the read timeout for nginx, or sets the flush interval for Caddy, so Server-Sent Events pass through the reverse proxy without being held back. Without proxy_read_timeout, nginx cuts the connection after its default 60 seconds, causing updates to arrive in bursts a minute apart. ```nginx proxy_buffering off; proxy_read_timeout 3600s; ``` ```caddy reverse_proxy 127.0.0.1:8080 { flush_interval -1 } ``` -------------------------------- ### Check ihasmail health with curl Source: https://docs.ihasmail.org/install Checks the ihasmail health endpoint. The response shows the service is up with its name and version. The same request is used as the container healthcheck, so `docker compose ps` should report the container as `healthy` within a minute. ```bash $ curl -s http://127.0.0.1:8080/api/health {"ok":true,"name":"ihasmail","version":"2026.8.30+pr129"} ``` -------------------------------- ### Clear SESSION_FILE for immutable mode Source: https://docs.ihasmail.org/troubleshooting When running immutably (IMMUTABLE=1), clear the default SESSION_FILE=/data/sessions.json by setting SESSION_FILE to an empty value. The -e flag overrides --env-file, so this works without editing the environment file. ```bash -e IMMUTABLE=1 -e SESSION_FILE= ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.