# tailwart — Stalwart mailbox as a Tailscale sidecar (NO WAN presence). # # The container shares ts-stalwart's network namespace, so its only interfaces # are lo and tailscale0. All mail ports listen on the tailnet only; the public # edge is the separate caddy/ layer-4 proxy, which can run on another host. # # Prereq: the shared tailnet infra (Postgres the-record-prod, Redis # slo-time-prod, Garage) must be up, and the stalwart role/db/bucket created # (see README). Bring up: docker compose up -d name: tailwart services: ts-stalwart: image: tailscale/tailscale:latest hostname: ${STALWART_MAGIC_NAME} environment: TS_AUTHKEY: ${TS_OAUTH_CLIENT_SECRET}?ephemeral=true TS_EXTRA_ARGS: --advertise-tags=tag:stalwart TS_HOSTNAME: ${STALWART_MAGIC_NAME} TS_ACCEPT_DNS: "true" TS_AUTH_ONCE: "true" TS_USERSPACE: "false" TS_ENABLE_HEALTH_CHECK: "true" TS_LOCAL_ADDR_PORT: "127.0.0.1:9002" dns: [1.1.1.1, 1.0.0.1] devices: - /dev/net/tun:/dev/net/tun cap_add: - NET_ADMIN - NET_RAW healthcheck: # Healthy only when BOTH the tailnet link is up AND Postgres is reachable # over it. The stalwart service gates on this (depends_on: service_healthy), # so it can no longer start into the race where it tries the DB before the # tailnet route exists — which logged "Failed to create tables" and dropped # in-flight cert/setting writes (e.g. lost the ACME cert on 2026-06-10). test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9002/healthz && nc -z -w3 ${DB_MAGIC_NAME}.${TS_TAILNET} 5432"] interval: 10s timeout: 8s retries: 6 start_period: 30s restart: unless-stopped stalwart: image: stalwartlabs/stalwart:v0.16.7 network_mode: "service:ts-stalwart" environment: # Consumed by config/config.toml via its %{env:NAME}% macros. Keeping # secrets in env (not the mounted toml) means the toml is commit-safe. STALWART_DB_NAME: ${STALWART_DB_NAME} STALWART_DB_USER: ${STALWART_DB_USER} STALWART_DB_PASSWORD: ${STALWART_DB_PASSWORD} DB_HOST: ${DB_MAGIC_NAME}.${TS_TAILNET} REDIS_URL: redis://${REDIS_MAGIC_NAME}.${TS_TAILNET}:6379/${STALWART_REDIS_DB} S3_ENDPOINT: http://${GARAGE_MAGIC_NAME}.${TS_TAILNET}:3900 S3_REGION: ${GARAGE_REGION} S3_BUCKET: ${STALWART_S3_BUCKET} S3_ACCESS_KEY: ${GARAGE_ACCESS_KEY_ID} S3_SECRET_KEY: ${GARAGE_SECRET_ACCESS_KEY} STALWART_HOSTNAME: ${STALWART_HOSTNAME} STALWART_DOMAIN: ${STALWART_DOMAIN} STALWART_SMARTHOST: ${STALWART_SMARTHOST} STALWART_FALLBACK_ADMIN_SECRET: ${STALWART_FALLBACK_ADMIN_SECRET} volumes: # Bootstrap config (v0.16 JSON): tells Stalwart only where Postgres lives; # all other settings live in the DB. Mounted at the image's default # --config path (/etc/stalwart/config.json). Secret comes from the # STALWART_DB_PASSWORD env above, referenced via the EnvironmentVariable # secret type inside the file — so this stays commit-safe. - ./config/config.json:/etc/stalwart/config.json:ro # Working dir: ACME cert cache + outbound queue spool. Named volume (not # anonymous) so a recreate doesn't orphan it and drop queued mail/certs. - stalwart-data:/var/lib/stalwart depends_on: ts-stalwart: condition: service_healthy restart: unless-stopped volumes: stalwart-data: