Deployment
A production deployment needs PostgreSQL plus two application processes built from the same mail release.
The web process serves SvelteKit and queues background operations. The worker synchronizes IMAP, sends SMTP messages, runs cleanup rules, classifies mail, and dispatches notifications. Do not omit the worker from a production installation.
Prepare configuration
Copy .env.example to .env and configure at least:
DATABASE_URL="postgresql://mail:password@database.example.com:5432/mail"
ORIGIN="https://mail.example.com"
BETTER_AUTH_SECRET="replace-with-a-long-random-secret"
MAIL_SECRET_KEY="replace-with-a-separate-long-random-secret"The database must be reachable from both processes. Give both processes the same DATABASE_URL, ORIGIN, and MAIL_SECRET_KEY. Provider, AI, IMAP, and SMTP settings can instead be entered through the setup and Settings screens.
Docker Compose
The repository Compose file builds both images locally and stores public-link attachments in a named volume. It expects PostgreSQL to be provided separately through DATABASE_URL.
cp .env.example .env
# Edit .env before starting the services.
docker compose up --build -d
docker compose logs -f web workerThe application is available on port 3000. Database migrations run automatically at startup.
Prebuilt containers
Multi-architecture web and worker images are published to GitHub Container Registry:
ghcr.io/pmh-only/mailghcr.io/pmh-only/mail-worker
The latest tag follows the main branch. Prefer a release tag for production so upgrades are intentional.
docker volume create mail-public-attachments
docker run -d \
--name mail-web \
--restart unless-stopped \
--env-file .env \
-p 3000:3000 \
-v mail-public-attachments:/app/data/public-attachments \
ghcr.io/pmh-only/mail:latest
docker run -d \
--name mail-worker \
--restart unless-stopped \
--env-file .env \
ghcr.io/pmh-only/mail-worker:latestYour DATABASE_URL hostname must be resolvable from both containers. Use a Docker network when the database also runs in a container.
Reverse proxy
Terminate HTTPS in front of the web process and preserve the original host and protocol headers. ORIGIN must exactly match the public URL.
Configure the proxy to:
- Forward ordinary HTTP traffic to port
3000. - Support WebSocket upgrades for
/api/external/v1/mcp/wsif that transport is used. - Accept request bodies up to the configured public-attachment limit.
- Use timeouts that permit large streamed attachment uploads.
Passkeys and most external authentication providers require HTTPS in production.
Persistent data
Back up these items together:
- The PostgreSQL database
- The directory configured by
PUBLIC_ATTACHMENT_DIR MAIL_SECRET_KEYand other deployment secrets
PostgreSQL contains synchronized messages, settings, encrypted credentials, jobs, authentication records, and OpenPGP keys. Public-link attachment bytes are stored on disk, with their metadata in PostgreSQL. A database-only backup is incomplete when public attachments are in use.
Upgrade
- Read the release notes and pin the target version.
- Back up PostgreSQL, public attachment files, and secrets.
- Pull or build both images from the same release.
- Restart the web and worker processes.
- Confirm the worker heartbeat and mailbox sync status in the application.
Migrations are automatic and can be run by either process. Never rotate MAIL_SECRET_KEY as part of a routine upgrade; changing it makes existing encrypted values unreadable.