2026-07-17 23:20:34 +02:00
|
|
|
name: webmail
|
|
|
|
|
services:
|
|
|
|
|
postgres:
|
|
|
|
|
image: postgres:16
|
|
|
|
|
environment:
|
|
|
|
|
POSTGRES_USER: webmail
|
|
|
|
|
POSTGRES_PASSWORD: webmail_dev
|
|
|
|
|
POSTGRES_DB: webmail_dev
|
|
|
|
|
ports: ["127.0.0.1:5433:5432"]
|
|
|
|
|
volumes: [pgdata:/var/lib/postgresql/data]
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD-SHELL", "pg_isready -U webmail -d webmail_dev"]
|
|
|
|
|
interval: 5s
|
|
|
|
|
timeout: 3s
|
|
|
|
|
retries: 10
|
|
|
|
|
|
|
|
|
|
redis:
|
|
|
|
|
image: redis:7
|
|
|
|
|
ports: ["127.0.0.1:6380:6379"]
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
|
|
|
interval: 5s
|
|
|
|
|
timeout: 3s
|
|
|
|
|
retries: 10
|
|
|
|
|
|
|
|
|
|
meilisearch:
|
|
|
|
|
image: getmeili/meilisearch:v1.8
|
|
|
|
|
environment:
|
|
|
|
|
MEILI_MASTER_KEY: dev_master_key_change_me
|
|
|
|
|
MEILI_ENV: development
|
|
|
|
|
ports: ["127.0.0.1:7700:7700"]
|
|
|
|
|
volumes: [meilidata:/meili_data]
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:7700/health | grep -q available"]
|
|
|
|
|
interval: 5s
|
|
|
|
|
timeout: 3s
|
|
|
|
|
retries: 10
|
|
|
|
|
|
2026-07-18 19:53:31 +02:00
|
|
|
# Sync-engine v2 workers (refactor §1): two roles sharing one image. Scale the
|
|
|
|
|
# sync role with `--scale worker-sync=N` (lease protocol prevents double-owning).
|
|
|
|
|
worker-sync:
|
|
|
|
|
build: { context: ../backend }
|
|
|
|
|
command: ["python", "-m", "workers.run"]
|
|
|
|
|
environment: &worker-env
|
|
|
|
|
WORKER_ROLE: sync
|
|
|
|
|
APP_DOMAIN: dev.webmail.local
|
|
|
|
|
DATABASE_URL: postgresql+asyncpg://webmail:webmail_dev@postgres:5432/webmail_dev
|
|
|
|
|
REDIS_URL: redis://redis:6379/0
|
|
|
|
|
MASTER_KEY: CHANGE_ME_base64_32_bytes
|
|
|
|
|
BLOB_ROOT: /var/blobs
|
2026-09-21 22:17:29 +02:00
|
|
|
# Must match the rspamd service below, or the controller 401s every learn.
|
|
|
|
|
RSPAMD_CONTROLLER_PASSWORD: ${RSPAMD_CONTROLLER_PASSWORD:-devpass}
|
2026-07-18 19:53:31 +02:00
|
|
|
volumes: [blobs:/var/blobs]
|
|
|
|
|
ulimits: { nofile: 65536 }
|
|
|
|
|
depends_on:
|
|
|
|
|
postgres: { condition: service_healthy }
|
|
|
|
|
redis: { condition: service_healthy }
|
|
|
|
|
|
|
|
|
|
worker-jobs:
|
|
|
|
|
build: { context: ../backend }
|
|
|
|
|
command: ["python", "-m", "workers.run"]
|
|
|
|
|
environment:
|
|
|
|
|
<<: *worker-env
|
|
|
|
|
WORKER_ROLE: jobs
|
|
|
|
|
volumes: [blobs:/var/blobs]
|
|
|
|
|
ulimits: { nofile: 65536 }
|
|
|
|
|
depends_on:
|
|
|
|
|
postgres: { condition: service_healthy }
|
|
|
|
|
redis: { condition: service_healthy }
|
|
|
|
|
|
2026-09-17 08:09:24 +02:00
|
|
|
# Anti-spam engine, mirroring the production stack (see ../docker-compose.yml).
|
|
|
|
|
# Dev publishes the ports on loopback so the plugin can be driven from the host
|
|
|
|
|
# (RSPAMD_URL=http://127.0.0.1:11333) as well as from a container.
|
|
|
|
|
rspamd:
|
|
|
|
|
image: rspamd/rspamd:3.9
|
|
|
|
|
# Root only long enough to write the controller password; rspamd then drops
|
|
|
|
|
# to _rspamd itself. /etc/rspamd is root-owned in the image.
|
|
|
|
|
user: root
|
|
|
|
|
entrypoint:
|
|
|
|
|
- /bin/sh
|
|
|
|
|
- -c
|
|
|
|
|
- |
|
|
|
|
|
mkdir -p /etc/rspamd/override.d
|
|
|
|
|
printf 'password = "%s";\nenable_password = "%s";\n' \
|
|
|
|
|
"$$RSPAMD_CONTROLLER_PASSWORD" "$$RSPAMD_CONTROLLER_PASSWORD" \
|
|
|
|
|
> /etc/rspamd/override.d/worker-controller.inc
|
|
|
|
|
exec /usr/bin/rspamd -f -u _rspamd -g _rspamd
|
|
|
|
|
environment:
|
|
|
|
|
RSPAMD_CONTROLLER_PASSWORD: ${RSPAMD_CONTROLLER_PASSWORD:-devpass}
|
|
|
|
|
volumes:
|
|
|
|
|
- ./rspamd/local.d:/etc/rspamd/local.d:ro
|
|
|
|
|
- rspamddata:/var/lib/rspamd
|
|
|
|
|
ports:
|
|
|
|
|
- "127.0.0.1:11333:11333" # scan worker (/checkv2)
|
|
|
|
|
- "127.0.0.1:11334:11334" # controller (/learnspam, /stat)
|
|
|
|
|
healthcheck:
|
2026-09-17 20:57:28 +02:00
|
|
|
# rspamc, not wget/curl: the rspamd image ships neither, so the obvious
|
|
|
|
|
# HTTP probe fails 127 forever and the container sits "unhealthy" while
|
|
|
|
|
# the engine is answering perfectly.
|
|
|
|
|
test: ["CMD-SHELL", "rspamc stat > /dev/null 2>&1"]
|
2026-09-17 08:09:24 +02:00
|
|
|
interval: 10s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 12
|
|
|
|
|
start_period: 15s
|
|
|
|
|
depends_on:
|
|
|
|
|
redis-rspamd: { condition: service_healthy }
|
|
|
|
|
|
|
|
|
|
# Bayes/fuzzy storage, separate from the app redis on purpose (SPEC §C.2).
|
|
|
|
|
redis-rspamd:
|
|
|
|
|
image: redis:7-alpine
|
|
|
|
|
command: [redis-server, --save, "300", "1", --maxmemory, 128mb, --maxmemory-policy, allkeys-lru]
|
|
|
|
|
volumes:
|
|
|
|
|
- rspamdredis:/data
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
|
|
|
interval: 5s
|
|
|
|
|
timeout: 3s
|
|
|
|
|
retries: 10
|
|
|
|
|
|
2026-10-12 19:25:10 +02:00
|
|
|
# Hosted mailboxes (SPEC Appendix D). Everything but where its database is
|
|
|
|
|
# (config.json) lives in that database and is set up by
|
|
|
|
|
# `python -m app.hosted.provision`, which also creates the database and its
|
|
|
|
|
# role — so the first start waits (restarting) until that has run once.
|
|
|
|
|
# Dev publishes the ports on loopback, off the standard numbers.
|
|
|
|
|
stalwart:
|
|
|
|
|
image: stalwartlabs/stalwart:v0.16.19
|
|
|
|
|
environment:
|
|
|
|
|
STALWART_DB_PASSWORD: ${STALWART_DB_PASSWORD:-stalwart_dev}
|
|
|
|
|
STALWART_RECOVERY_ADMIN: admin:${STALWART_ADMIN_SECRET:-stalwart_admin_dev}
|
|
|
|
|
volumes:
|
|
|
|
|
- ./stalwart/config.json:/etc/stalwart/config.json:ro
|
|
|
|
|
- stalwartdata:/var/lib/stalwart
|
|
|
|
|
ports:
|
|
|
|
|
- "127.0.0.1:8180:8080" # management API (JMAP)
|
|
|
|
|
- "127.0.0.1:1025:25" # SMTP — mail from "outside"
|
|
|
|
|
- "127.0.0.1:1465:465" # submissions
|
|
|
|
|
- "127.0.0.1:1587:587" # submission (STARTTLS)
|
|
|
|
|
- "127.0.0.1:1993:993" # IMAPS
|
|
|
|
|
depends_on:
|
|
|
|
|
postgres: { condition: service_healthy }
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
|
2026-07-17 23:20:34 +02:00
|
|
|
# Test mail servers (integration tests + manual dev against a throwaway mailbox)
|
2026-10-04 23:51:31 +02:00
|
|
|
# Spelling and grammar checking for the compose window (POST /api/spellcheck
|
|
|
|
|
# forwards to it; the browser never talks to it directly). No n-gram data:
|
|
|
|
|
# it would add several GB per language for a modest gain in grammar checks.
|
|
|
|
|
languagetool:
|
|
|
|
|
image: erikvl87/languagetool:6.8
|
|
|
|
|
environment:
|
|
|
|
|
Java_Xms: 256m
|
|
|
|
|
Java_Xmx: 1g
|
|
|
|
|
ports:
|
|
|
|
|
- "127.0.0.1:8010:8010"
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8010/v2/languages || exit 1"]
|
|
|
|
|
interval: 10s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 12
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
|
2026-10-18 20:00:31 +02:00
|
|
|
# Email translation (app/translation.py). The first start downloads every
|
|
|
|
|
# language model (several GB) into the volume; later starts reuse them.
|
|
|
|
|
libretranslate:
|
|
|
|
|
image: libretranslate/libretranslate:v1.9.6
|
|
|
|
|
environment:
|
|
|
|
|
LT_DISABLE_WEB_UI: "true"
|
|
|
|
|
LT_UPDATE_MODELS: "false"
|
|
|
|
|
ports:
|
|
|
|
|
- "127.0.0.1:5005:5000"
|
|
|
|
|
volumes:
|
|
|
|
|
- ltmodels:/home/libretranslate/.local
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:5000/languages', timeout=4)\""]
|
|
|
|
|
interval: 15s
|
|
|
|
|
timeout: 6s
|
|
|
|
|
retries: 240
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
|
2026-07-17 23:20:34 +02:00
|
|
|
greenmail:
|
|
|
|
|
image: greenmail/standalone:2.0.1
|
|
|
|
|
environment:
|
|
|
|
|
GREENMAIL_OPTS: >-
|
|
|
|
|
-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0
|
|
|
|
|
-Dgreenmail.users=dev:devpass@dev.local -Dgreenmail.verbose
|
|
|
|
|
ports:
|
|
|
|
|
- "127.0.0.1:3025:3025" # SMTP
|
|
|
|
|
- "127.0.0.1:3143:3143" # IMAP
|
|
|
|
|
- "127.0.0.1:3993:3993" # IMAPS
|
|
|
|
|
|
|
|
|
|
volumes:
|
2026-10-18 20:00:31 +02:00
|
|
|
ltmodels:
|
2026-07-17 23:20:34 +02:00
|
|
|
pgdata:
|
|
|
|
|
meilidata:
|
2026-07-18 19:53:31 +02:00
|
|
|
blobs:
|
2026-09-17 08:09:24 +02:00
|
|
|
rspamddata:
|
|
|
|
|
rspamdredis:
|
2026-10-12 19:25:10 +02:00
|
|
|
stalwartdata:
|