Deployment¶
Mailr is deployed with one Docker Compose file, deploy/docker-compose.yml. Every host, port, path and secret is set in deploy/.env. The first start is described step by step in Install with Docker Compose.
Services¶
| Service | Image | Profile | Description | Ports |
|---|---|---|---|---|
app |
Mailr | always | The API and the web app (mailr serve). |
8025, published as MAILR_HTTP_PORT on MAILR_HTTP_BIND (127.0.0.1) |
worker |
Mailr | always | Account and calendar sync, background jobs, reminders, the daily export (mailr worker). |
none |
worker-extra |
Mailr | none started | More workers for a larger server, started with docker compose up -d --scale worker-extra=N. See Scaling. |
none |
postgres |
pgvector/pgvector:pg17 |
always | Mailr's index, settings, search vectors and job queue. | internal only |
stalwart |
built from deploy/stalwart |
always | The mail store. JMAP stays internal; IMAP is published for mail apps. | 993 (MAILR_IMAPS_PORT) on MAILR_IMAPS_BIND (0.0.0.0, every address of the host); 143 (MAILR_IMAP_PORT) on MAILR_IMAP_BIND (127.0.0.1) |
resolver |
Unbound | always | Recursive DNS for spam blocklist lookups. | internal only |
egress |
gluetun | vpn |
Optional VPN exit for image fetches and page reading. See Image proxy and VPN egress. | internal only |
browser |
browserless (headless Chrome) | browser |
Optional browser for page readers that need JavaScript. Its network has no route out. See Web search. | internal only |
browser-gate |
Mailr (mailr browser-gate) |
browser |
The browser's only way out. Every connection of the browser is checked against Mailr's address rules here. | internal only |
dav-gateway |
nginx | phones |
Optional gateway for calendar and contacts apps on phones and computers. See Phones and desktop apps. | 8026 (MAILR_DAV_PORT) on MAILR_DAV_BIND (127.0.0.1) |
glitchtip |
GlitchTip | errors |
Optional error tracker for the app, the worker and the web app. See Error reports. | 8027 (GLITCHTIP_PORT) on GLITCHTIP_BIND (127.0.0.1) |
glitchtip-postgres |
postgres:17 (Alpine) |
errors |
GlitchTip's own database. | internal only |
cli |
Mailr | tools |
One-off maintenance commands while the stack is up: docker compose run --rm cli mailr <command>. Never started by docker compose up. |
none |
A profile is turned on in .env, for example COMPOSE_PROFILES=vpn,browser. Services without a profile always run.
Ports on the host¶
Two ports are published on the host by default: the web app on 127.0.0.1:8025 and IMAP over TLS on port 993 of every address of the host. Port 993 is open to your network, and to the internet if the host is reachable from it, so that phones and desktop mail apps can connect. To keep IMAP on one address only, set MAILR_IMAPS_BIND, for example MAILR_IMAPS_BIND=127.0.0.1 when no mail app connects from another machine.
When a port is already taken on the host, choose another one in .env: MAILR_HTTP_PORT for the web app, MAILR_IMAPS_PORT and MAILR_IMAP_PORT for IMAP, MAILR_DAV_PORT for the DAV gateway. Only the port on the host changes; mail apps then connect to the new port.
The app image¶
The image of the app and worker services is obtained in one of two ways, chosen in .env:
| Source | .env |
Commands |
|---|---|---|
| Built from the repository (default) | MAILR_IMAGE and MAILR_PULL_POLICY unset |
docker compose up -d --build |
| A published image | MAILR_IMAGE=ghcr.io/murtaza-nasir/mailr:latest and MAILR_PULL_POLICY=missing |
docker compose pull && docker compose up -d |
With the defaults, the repository is built and the result is tagged mailr:local. With a published image and MAILR_PULL_POLICY=missing, the image is pulled and never built. The mail server image is always built from deploy/stalwart. Use a published image has the steps.
Help pages¶
This guide is part of the app image. Users open it from Help in the ? menu or in their
account menu, and it is served at /help/ on the Mailr address. Only people signed in to the
server can read it. Anyone else is sent to the sign-in page first. Search engines are told not to
index it, and nothing in it is loaded from other sites.
Hardening¶
The app and the worker are the same image with different commands. Both run with every Linux capability dropped, with no-new-privileges and with a read-only root filesystem; they write only to /data, /backup and a small /tmp. The mail server's admin port, Postgres and the resolver are never published.
Every other container runs the same way: every capability is dropped, no-new-privileges is set, and each container gets back only the capabilities its image cannot start without. Each root filesystem is read-only, apart from the VPN container's, and the containers write only to their volumes and to small in-memory folders.
| Service | Capabilities kept | Writes to | Memory, processes |
|---|---|---|---|
app, cli |
none | /data, /backup, /tmp |
2 GB, 4096 |
worker, worker-extra |
none | /data, /backup, /tmp |
3 GB, 4096 |
postgres |
CHOWN, DAC_OVERRIDE, FOWNER, SETGID, SETUID: its start script sets the owner of the data folder as root, then runs Postgres as the postgres user |
its volume, /var/run/postgresql, /tmp |
4 GB, 1024 |
stalwart |
NET_BIND_SERVICE: the mail server's program does not start without the right to listen on ports below 1024 (143, 993) |
its volumes, /tmp |
2 GB, 1024 |
resolver |
SETGID, SETUID: Unbound starts as root and switches to its own user |
its volume resolver-data (the root zone's trust anchor), /tmp |
512 MB, 64 |
egress |
NET_ADMIN (the tunnel, its routes and firewall), CHOWN, DAC_OVERRIDE (its state files) |
its root filesystem: gluetun adds its own user to /etc/passwd at every start |
256 MB, 256 |
browser |
none | /tmp, its home folder |
3 GB, 1024 |
browser-gate |
none | /tmp |
256 MB, 256 |
dav-gateway |
none | /tmp, its generated configuration |
128 MB, 256 |
glitchtip |
none | its uploads volume, /tmp |
1 GB, 256 |
glitchtip-postgres |
as postgres |
its volume, /var/run/postgresql, /tmp |
512 MB, 128 |
The memory limits are well above what the services use on a small server (see Resources). Postgres's limit includes its shared buffers, the memory of the queries that run and its shared memory; the browser's includes its 1 GB of shared memory. A container that reaches its memory limit is stopped by the kernel and started again by Docker, so one service that runs out of memory cannot take the memory of the others. The process limits count threads too, and stop a runaway process from filling the host's process table. Each limit has a variable in .env (Container limits), for example MAILR_PG_MEMORY=8g with MAILR_PG_SHARED_BUFFERS=2GB on a larger host. Raise MAILR_PG_PIDS with MAILR_PG_MAX_CONNECTIONS, and MAILR_APP_PIDS and MAILR_WORKER_PIDS when many users connect many mail accounts (one thread for each open IMAP connection).
Every service has a healthcheck, and docker compose ps shows its result. The app's is in its image; the worker's is mailr worker-alive; the resolver's is a query for a name from its own zone data; the DAV gateway's and the browser's are a connection to their ports; the VPN container's is gluetun's own check of the tunnel.
Volumes¶
| Volume | Holds |
|---|---|
postgres-data |
The Postgres database. |
stalwart-data |
Every message, in the mail server's store. |
resolver-data |
The DNS resolver's copy of the root zone's trust anchor. |
mailr-data (mounted at /data, MAILR_DATA_DIR) |
Uploads, caches and the per-user Maildir exports. |
${MAILR_BACKUP_DIR} (mounted at /backup) |
The optional second copy of the export, and the nightly database dumps. See Backups. |
glitchtip-postgres-data, glitchtip-uploads |
The optional error tracker's database and files. |
The logs of the app and the worker are kept in mailr-data, under /data/logs. See Monitoring.
Postgres settings¶
The bundled Postgres is started with settings for a host of about 8 GB shared with other services. Each can be changed in .env.
| Setting | Value | Reason |
|---|---|---|
shared_buffers |
1GB (MAILR_PG_SHARED_BUFFERS) |
Postgres's own cache. The hot part of the index, with the two search vector indexes of about 450 MB, stays in it. |
effective_cache_size |
3GB (MAILR_PG_EFFECTIVE_CACHE_SIZE) |
What the planner may assume the operating system caches on top. Nothing is allocated. |
work_mem |
16MB (MAILR_PG_WORK_MEM) |
Memory for each sort or hash step of a query, before it spills to disk. |
maintenance_work_mem |
256MB (MAILR_PG_MAINTENANCE_WORK_MEM) |
Vacuum and index builds, such as the vector indexes, in fewer passes. |
max_connections |
150 (MAILR_PG_MAX_CONNECTIONS) |
The pools of the app (at most 44 connections), the worker (38, with one connection for the locks of all mail accounts, however many there are, and one for each write to a mail server that runs) and one cli command (17), with room for a second app during a deploy. Each extra worker holds up to 25 more. |
random_page_cost, jit |
1.1, off | The data is on SSD. Compiling queries cost more time than it saved. |
pg_stat_statements |
loaded | Statement statistics (Monitoring). |
log_min_duration_statement, log_lock_waits |
500 ms, on | Slow statements and lock waits are logged, without their parameters. |
The container also gets 256 MB of shared memory (shm_size) for parallel queries. A change to any of these settings takes effect when the postgres container is restarted; write a dump first (Upgrades). On a host with more memory, raise the container's memory limit MAILR_PG_MEMORY with them: shared_buffers about a quarter of that limit, and effective_cache_size about three quarters (Sizes of server has the values for 50 and 100 users).
Resources¶
For a small group of people with tens of thousands of messages, plan for:
- Memory: about 2.5 GB in total (Stalwart about 350 MB, Postgres up to about 1.5 GB with the settings above, app and worker about 600 MB), and about 300 MB more for the optional error tracker. The containers' memory limits (Hardening) are upper bounds, not reservations.
- Disk: roughly 1.3 times the mail size for the store, index and vectors, plus the export copy.
- CPU: any modern machine. No model is ever run in Mailr's own containers, so no GPU is needed.
Scaling the work¶
MAILR_WORKER_CONCURRENCY(default 4) is the number of jobs run at once in the worker's main lane: sends, reminders, model work and most other jobs. Raise it on a larger host.- Some kinds of jobs run in lanes of their own, so a slow job of one kind never holds up sends and reminders: writes to the mail providers (
MAILR_UPSTREAM_CONCURRENCYat a time, default 4, and one at a time per mail account), CalDAV sync (two at a time), image proxy fetches (two at a time), scheduled upkeep such as exports, retraining and the database dump (two at a time), Ambrose's bulk reviews (one at a time), and the sorting and indexing of imported mail with the embedding catch-up after a model change (two at a time). A big import therefore no longer delays sends, reminders or snooze wake-ups. - In every lane the users take turns: the next job is the earliest one of the user whose turn it is, and one user holds at most half of a lane's slots. Sends, reminders, snooze wake-ups and push notifications go first.
- Each user's daily work (the exports, retraining, contact scores and the purge of deleted mail) runs at a time of day of its own, so the jobs of many users are not all queued at once.
MAILR_MODEL_CONCURRENCY(default 4) limits the model calls that run at once against one model endpoint.- More workers can run side by side:
docker compose up -d --scale worker-extra=N. - Pass
--hostand--porttomailr serveto change its address. Run the app as one process: live chat runs and the event stream are kept in it.
Scaling has the details and the sizes of server for 10, 50 and 100 users.
Running on a private network¶
Mailr can run on a LAN only or behind a public reverse proxy. On a LAN, set MAILR_BASE_URL to the address used on the devices. Before you expose Mailr to the internet, turn on single sign-on and set MAILR_LOCAL_LOGIN=false, and read Security and permissions.
Commands inside the stack¶
Run any mailr command in the app container:
| Command | Description |
|---|---|
mailr serve |
Run the API and the web app. It is run in the app service. |
mailr worker |
Run the background worker. It is run in the worker service. |
mailr migrate |
Apply database migrations. They are also applied when the app starts. |
mailr db-dump |
Write a database dump now. See Backups. |
mailr db-restore |
Load a dump into a new database and compare it, or run the restore drill (--check). |
mailr gen-secret |
Print a random secret for MAILR_SECRET_KEY or MAILR_SESSION_SECRET. |
mailr create-user |
Add a user. See Users. |
mailr set-password |
Set a user's password. |
mailr create-token |
Create an API token. See Tokens and scopes. |
mailr reembed |
Rebuild the search vectors. See Models. |
mailr rethread |
Rebuild a user's conversations. See Upgrades. |
Every option is listed in the command line reference.