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 # 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 # Must match the rspamd service below, or the controller 401s every learn. RSPAMD_CONTROLLER_PASSWORD: ${RSPAMD_CONTROLLER_PASSWORD:-devpass} 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 } # 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: # 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"] 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 # 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 # Test mail servers (integration tests + manual dev against a throwaway mailbox) # 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 # Email translation (translator/, app/translation.py): Firefox Translations # models and fastText. The first start downloads the default languages # (about 350 MB) into the volume; the admin panel adds more. translator: build: context: .. dockerfile: docker/Dockerfile.translator image: webmail/translator:dev ports: - "127.0.0.1:5005:5000" volumes: - translatordata:/data restart: unless-stopped 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: translatordata: pgdata: meilidata: blobs: rspamddata: rspamdredis: stalwartdata: