Requirements and sizing

Bulwark is a mail client, so it needs a Stalwart mail server to talk to and somewhere to run.

What you need

BulwarkBulwark Lite
Mail serverStalwart 0.16.6 or later, or Stalwart 1.0the same
Bulwark version for Stalwart 1.01.18.0 or laterthe same
Where it runsDocker on linux/amd64 or linux/arm64, or Node.js 20.9 or later for the standalone buildany web server that serves static files, the Lite container image, or Stalwart itself
A server processyes, one Node.js processnone

Bulwark and Bulwark Lite share version numbers, so the Stalwart versions apply to both. Supported versions lists which Stalwart release adds what. Before you move a running server to Stalwart 1.0, read Upgrading to Stalwart 1.0.

The ways to install are on Docker deployment, Manual deployment (Node.js), Static hosting and Install on Stalwart (both Lite).

Architectures

Every release publishes these, each for linux/amd64 and linux/arm64:

  • the image ghcr.io/bulwarkmail/webmail
  • the Lite image ghcr.io/bulwarkmail/webmail-lite
  • the standalone tarballs bulwark-standalone-<version>-linux-amd64.tar.gz and -linux-arm64.tar.gz

There is no 32-bit ARM build (arm/v7) of any of them. A Raspberry Pi needs a 64-bit operating system: uname -m has to print aarch64. The Lite zips are plain HTML, JavaScript and CSS, so they don't depend on the architecture at all.

docker pull picks the right architecture on its own. The images are on GitHub Container Registry only, not on Docker Hub.

Download and disk size

Measured for 1.19.1. The numbers move a little from release to release.

ImageDownload (compressed)On disk after the pull (amd64)
ghcr.io/bulwarkmail/webmail:1.19.1110.5 MB (amd64), 110.3 MB (arm64)445 MB
ghcr.io/bulwarkmail/webmail-lite:1.19.123.1 MB (amd64), 23.8 MB (arm64)116 MB
Release fileSize
bulwark-standalone-1.19.1-linux-amd64.tar.gz60.0 MB
bulwark-standalone-1.19.1-linux-arm64.tar.gz59.7 MB
bulwark-lite-1.19.1.zip18.8 MB
bulwark-lite-stalwart.zip18.8 MB

Bulwark keeps no mail. What it writes under /app/data is its config, its runtime state and, if you turn settings sync on, users' settings.

Memory and CPU

These figures come from one measured setup. Read how it was measured before you size a machine with them.

BulwarkBulwark Lite (container)
Memory right after start73 MiB7.6 MiB
Memory while idle, 2 minutes62 to 74 MiB7.6 to 7.8 MiB
CPU while idle0.00 % (at most 0.02 %)0.00 %
Memory during the load269 MiB on average, 321 MiB at most9.3 MiB on average, 9.9 MiB at most
CPU during the load, in % of one core252 % on average, 289 % at most290 % on average, 293 % at most
Memory 60 seconds after the load95.8 MiB8.2 MiB

How it was measured:

  • On the project's benchmark VM: 8 virtual CPUs (Intel Xeon E5-2680 v4), 5.9 GiB of RAM, amd64, Docker 29.8.
  • The webmail:1.19.1 and webmail-lite:1.19.1 images, one container at a time, with no volumes, pointed at the Stalwart server on the same VM. The admin console was off.
  • Nobody signed in. Idle means 2 minutes with no requests, with docker stats read every 10 seconds.
  • The load was 200 cold page loads, 10 at a time. Each one fetched the login page, every script and stylesheet it references, and the runtime config, with no browser cache. curl sent them from the same VM, so it shared the CPU with the container. docker stats was read every second.
  • A cold page load moved about 1.2 to 1.3 MB for either edition. Browsers cache the scripts and stylesheets, so later visits fetch much less.

What this doesn't cover:

  • Signed-in use was not measured. Mail sync, search and everything else a signed-in user makes Bulwark's server do are not in these numbers.
  • Nothing was measured on ARM. The arm64 images are the same code, but no board was tested.

For comparison, Stalwart 0.16.22 on the same VM used about 170 MiB after running for nine days. Leave room above the load figure when you pick a machine; the figures above are not a recommendation for any number of users. There is no sizing guidance for large installs yet: the project hasn't measured Bulwark with hundreds or thousands of signed-in users, and it hasn't tested more than one replica (see Several replicas).

HTTPS

Bulwark works over plain HTTP, for example at http://192.168.1.10:3000 on a home network. These things need HTTPS:

  • More than 5 accounts in one browser: each signed-in account keeps one push connection open. Over HTTP/1.1 a browser allows about six connections per site, so Bulwark stops at 5 accounts. Once it sees HTTP/2 or HTTP/3 it allows 50. Browsers speak HTTP/2 only over TLS, so this needs a proxy that serves HTTPS with HTTP/2, which Caddy does by default and nginx does with http2 on.
  • Installing Bulwark as an app, and web push: browsers allow both only on a secure page: HTTPS, or http://localhost. See Progressive web app. Web push also needs Bulwark (not Lite).
  • Embedding Bulwark in another site: Embedded SSO needs cross-site cookies, which browsers send only over HTTPS.

Once Bulwark is on HTTPS, Stalwart's JMAP address has to be HTTPS too. A page served over HTTPS can't load from an http:// address; the browser blocks it.

Files and the app user

The Bulwark image runs as the user nextjs, uid 1001 and gid 1001. A named Docker volume on /app/data gets the right owner by itself. A bind mount to a folder on the host, such as a NAS share, has to be writable by uid 1001, or Bulwark can't save its config and session secret:

sudo mkdir -p /srv/bulwark/data
sudo chown -R 1001:1001 /srv/bulwark/data

The Lite image runs as uid 101 and listens on port 8080. It only reads the config.json (and policy.json) you mount into it, read-only is fine, and it writes nothing you need to keep.

The standalone build runs as whatever user your service file names. Its data folders have to be writable by that user; Manual deployment shows the systemd unit.

What has to reach what

The browser loads mail, calendars, contacts and files straight from Stalwart, in both editions. Bulwark's server doesn't pass the mail through. So the browser needs Stalwart's JMAP address as much as it needs Bulwark's.

FromToWhat for
BrowserBulwark's web address (Lite: the web host)the app itself
BrowserStalwart's JMAP addressthe mail, calendars, contacts and files. Stalwart needs its CORS setting on unless both share one origin
Bulwark's serverStalwart's JMAP address (JMAP_SERVER_URL)sign-in, OAuth discovery, the admin console's Stalwart features. Lite has no server, so this row doesn't apply
Bulwark's serverversion.telemetry.bulwarkmail.orgthe hourly update check. Off with BULWARK_UPDATE_CHECK=off
Browserthe push relay, over HTTPSregistering for web push
Stalwartthe push relay, over public HTTPStelling the relay that new mail arrived
Phone appStalwart's JMAP addressthe mail, the same as the browser

For push, nothing has to come in from the internet. Stalwart sends each change out to the relay, the relay wakes the browser or phone, and the device then fetches the mail from Stalwart itself. The default relay is https://notifications.relay.bulwarkmail.org. If you run your own relay, Stalwart has to reach it over HTTPS at a public name: Stalwart refuses push addresses on private IPs, and Stalwart 1.0 also refuses names that resolve to one. A relay on your LAN doesn't work.

The other outside services Bulwark can contact, and the No third-party traffic switch that turns them all off, are on Third-party traffic.

Next step

Pick an install: Docker Compose runs Bulwark next to Stalwart, Run Bulwark at home covers a home server or NAS, and Static hosting puts Lite on any web server.