Skip to content

stalwart-server

stalwart-server is Stalwart, a mail server that holds mailboxes for the domain, and Bulwark, a webmail client for it. The stack is optional, and nothing else in the fleet depends on it. It runs on one host, mx01. See Mail (mx01) for the build.

What it runs

stalwart
bulwark
Service Does
stalwart Takes and sends mail on the four mail ports. On port 8080 it serves its web interface, the discovery documents, and JMAP, the mail protocol over HTTP that webmail uses
bulwark Webmail, on port 3000. It reads mail from Stalwart over JMAP and signs people in through Authentik

The project is mail, so the containers are mail-stalwart and mail-bulwark.

The stack has no database container. Stalwart keeps every mailbox, account, and setting in its own data folder. Its config file is written by the setup wizard on the first start.

Neither service sits on an internal network. Stalwart has to reach other mail servers, and both have to reach Authentik's public name.

Values it reads

Key Reads Kind Described under
STALWART_LICENSE_KEY STALWART_LICENSE_KEY Secret Stalwart and Bulwark
BULWARK_SESSION_SECRET_KEY BULWARK_SESSION_SECRET_KEY Secret Stalwart and Bulwark
BULWARK_OAUTH_CLIENT_ID MAIL_OIDC_CLIENT_ID Secret Stalwart and Bulwark
BULWARK_OAUTH_CLIENT_SECRET MAIL_OIDC_CLIENT_SECRET Secret Stalwart and Bulwark

It also reads the 19 operational defaults, as every stack does.

The host page says what to put in each and when. See Stage the values.

Two more keys are settings with no reference behind them, so they are set in the inventory. See Stack values.

Key Holds Left blank
BULWARK_OAUTH_ISSUER_URL The issuer address of the Authentik provider made for mail Sign-in through Authentik cannot work
BULWARK_JMAP_SERVER_URL Stalwart's public address https://mail. and the domain

What the host needs

The project is mail, so the stack's folders sit under /opt/docker/volumes/mail and /opt/docker/logs/mail. Both are mode 0750, owned by the admin account with group 101000.

Folder Owner Mode
/opt/docker/volumes/mail/stalwart-config 102000:102000 Default
/opt/docker/volumes/mail/stalwart-data 102000:102000 Default
/opt/docker/volumes/mail/bulwark-data 101001:101001 Default
/opt/docker/volumes/mail/bulwark-data/settings 101001:101001 Default
/opt/docker/volumes/mail/bulwark-data/admin 101001:101001 Default
/opt/docker/volumes/mail/bulwark-data/admin-state 101001:101001 Default
/opt/docker/volumes/mail/bulwark-data/telemetry 101001:101001 Default
Port Allowed from Comment
25/tcp Anywhere Stalwart SMTP
465/tcp Anywhere Stalwart submissions
587/tcp Anywhere Stalwart submission
993/tcp Anywhere Stalwart IMAPS

The host's NixOS configuration creates the folders and opens the ports when the host is deployed. It reads them from the host's file in the private repo, nixos/hosts/<host>.json, which nixos-sync.yml writes from the stack's setup.yaml. See Host layout.

The stack also needs a Traefik on the same host for the web side.

The owners differ from the fleet's usual one because neither image runs as UID 1000. Stalwart runs as 2000 and Bulwark as 1001, which the host sees as 102000 and 101001. See Why 100000 and 101000.

The four mail ports are open to any address. Mail cannot pass through an HTTP router, so Stalwart publishes them on the host itself, and the network's router forwards them from the internet.

Port Carries
25 Mail from other servers
465 Submission from mail clients, TLS from the first byte
587 Submission from mail clients, with STARTTLS
993 IMAP, TLS from the first byte

Hostnames

Traefik routes six names to Stalwart and four to Bulwark. With the host mx01, the sub-domain home., and the domain myah-mitchell.com, they are:

Hostname Service
mail.myah-mitchell.com stalwart. The public name, which Stalwart builds its discovery documents from
autoconfig.myah-mitchell.com stalwart. Mail clients fetch their settings from it
mta-sts.myah-mitchell.com stalwart. Other mail servers fetch the domain's TLS policy from it
mail.home.myah-mitchell.com stalwart
mail.mx01.home.myah-mitchell.com stalwart
stalwart.mx01.home.myah-mitchell.com stalwart
webmail.myah-mitchell.com bulwark. The public name
webmail.home.myah-mitchell.com bulwark
webmail.mx01.home.myah-mitchell.com bulwark
bulwark.mx01.home.myah-mitchell.com bulwark

Both routes use the chain-no-auth chain in both modes. Stalwart does its own authentication, for clients that cannot follow a redirect, and Bulwark signs people in through Authentik itself.

Both services are labelled to be published through the hub on tf01, which happens once mx01 runs traefik-agent.

Verify

In Komodo, the stalwart-server Stack shows as running with two services.

On the host, list the Stack's containers. Komodo names the Compose project after the Stack, not after PROJECT_NAME:

docker compose -p stalwart-server ps

Both containers show healthy in the STATUS column. Each check asks the service's own HTTP port for its health, so it says nothing about the mail ports. Bulwark starts after Stalwart is healthy.

Check a mail port from another host:

nc -zv 172.16.8.121 25

The command reports that the connection succeeded.

Data worth keeping

Folder under /opt/docker/volumes/mail Holds
stalwart-data Every mailbox, account, and setting
stalwart-config The config file the setup wizard wrote
bulwark-data Bulwark's admin settings and each person's synced settings

All three are on the persistent disk, so they survive a rebuild of the VM. stalwart-data grows with the mail it holds, so watch the disk it is on.

Not yet confirmed

  • The whole stack. It was composed from Stalwart's and Bulwark's documentation and has not run in the fleet.
  • Port 8080 after the setup wizard finishes. Stalwart's documentation has a reverse proxy send the web interface, JMAP, and SCIM there, and every route does. It has not been seen on a running container.
  • Whether a port published from Docker shows Stalwart the sender's real address.
  • The stack in bootstrap mode. Bulwark needs Authentik reachable at its public name, and the public names need the hub and the tunnel.