8.1 KiB
Self-hosting Mailflare (Docker)
Mailflare can run as a single container on any host instead of Cloudflare Workers. The same code serves both; the container provides its own database (SQLite on a volume), blob storage (files on the same volume), job queue, realtime WebSocket, backup schedule and an SMTP listener for inbound mail.
Quick start
git clone https://github.com/hieunc229/mailflare && cd mailflare
cp .env.docker.example .env.docker # edit: how to receive and send mail
docker compose up -d --build
Open http://your-host:3000/setup, create the admin account and add your
domain. All data lives in the mailflare-data volume (/data in the
container): the SQLite database, raw messages, attachments and backups.
Behind a reverse proxy, set APP_URL=https://mail.example.com so links in
password-reset mail and the JMAP session point at the public address, and
forward WebSocket upgrades for /api/realtime.
PWA and push notifications
Mailflare can be installed as a PWA and can deliver new-mail notifications after the browser is closed. Generate one VAPID key pair for the installation:
npm run push:keys
Set the printed VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY values in the
runtime environment, plus VAPID_SUBJECT as either a mailto: contact or the
public HTTPS URL of the installation. Keep the private key secret. Browsers
require HTTPS for service workers and Web Push (localhost is the development
exception). Users can then enable push under Settings → Inbox → Notifications.
Receiving mail
Pick one; both can be on at once.
Built-in SMTP listener (default). The container listens on port 25 and
accepts mail for every domain you add. Point the domain's MX record at the
host, set MAIL_HOSTNAME to that host's name, and make sure port 25 is
reachable from the internet (several clouds block it by default; Hetzner and
most VPS providers do not). The domain page in the app lists the MX, SPF and
DMARC records to create. Domain routing rules work as on Cloudflare: reject
rules answer the sender with a 550 during delivery, forward rules relay the
message through your outbound SMTP.
Optional: SMTP_TLS_KEY and SMTP_TLS_CERT (paths inside the container)
enable STARTTLS with your own certificate. Without them STARTTLS is not
offered, which is safe but means transport encryption depends on the sender.
Cloudflare Email Routing relay. Keep MX on Cloudflare and deploy the
Worker in deploy/cloudflare-email-relay. It posts each message to
/api/inbound on your server, signed with INBOUND_WEBHOOK_SECRET, and acts
on the reject or forward decision the server returns. Set
SMTP_INBOUND_PORT=0 if you do not want the listener at all.
Sending mail
Any SMTP relay. SMTP_URL=smtps://user:pass@host:465 (or smtp://
with STARTTLS). Works with your hosting provider's relay, Amazon SES,
Postmark, Mailgun, or a Postfix you run. Set
SMTP_TLS_REJECT_UNAUTHORIZED=false only for a relay with a self-signed
certificate on a private network.
Cloudflare Email Sending. CF_ACCOUNT_ID plus a CF_TOKEN with Email
Sending: Edit. The domain must be a Cloudflare zone with Email Sending set
up; Mailflare calls the REST API, no Workers plan needed.
Resend or Amazon SES, per domain. Instead of one global relay, each domain
can send through Resend or SES with their APIs (no SMTP involved), and receive
through them too, once Mailflare can manage the domain's DNS with CF_TOKEN.
Add the credentials on the domain page, or set RESEND_API_KEY /
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION. Receiving
through either needs APP_URL set to your public HTTPS address. See
Sending and receiving providers. Using SES as a plain SMTP
relay through SMTP_URL, described above, still works and needs none of this.
Cloudflare zone management (optional)
If CF_TOKEN can also edit DNS and Email Routing on your zones, adding a
domain configures Email Routing and the sending subdomain automatically,
exactly as on Workers. Without it, domains are recorded as manually managed
and the DNS page shows what to set by hand.
Configuration reference
| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
HTTP port |
DATA_DIR |
/data |
SQLite database, blobs and backups |
APP_URL |
request origin | Public URL behind a proxy |
SMTP_INBOUND_PORT |
25 |
Inbound SMTP; 0 disables |
MAIL_HOSTNAME |
mail.<domain> |
Host the MX record points at; SMTP banner |
SMTP_MAX_SIZE |
36 MiB | Largest raw inbound message; allows for encoding overhead on up to 25 MB of attachments. Oversized mail receives an SMTP rejection, which the sender's mail provider can report as a delivery failure. |
SMTP_TLS_KEY, SMTP_TLS_CERT |
unset | STARTTLS certificate for the listener |
SMTP_URL |
unset | Outbound relay |
SMTP_TLS_REJECT_UNAUTHORIZED |
true |
Trust self-signed relay certificates when false |
CF_ACCOUNT_ID, CF_TOKEN |
unset | Cloudflare Email Sending, and zone management if the token allows |
B2_KEY_ID, B2_APPLICATION_KEY, B2_BUCKET, B2_ENDPOINT |
unset | Store blobs in Backblaze B2 (all four required) instead of files under /data/blobs; see Deployment |
S3_BUCKET, S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY |
unset | Store blobs in AWS S3; the keys fall back to AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. Backblaze wins if both are set |
RESEND_API_KEY |
unset | Resend key, used when none is saved in the app |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION |
unset | Amazon SES credentials, used when none are saved in the app |
INBOUND_WEBHOOK_SECRET |
unset | Enables /api/inbound for the relay Worker |
TURNSTILE_SECRET_KEY |
unset | Bot protection on login and reset forms (NEXT_PUBLIC_TURNSTILE_SITE_KEY at build time) |
VAPID_PUBLIC_KEY |
unset | Public Web Push application-server key |
VAPID_PRIVATE_KEY |
unset | Secret Web Push application-server key |
VAPID_SUBJECT |
unset | Web Push contact URI (mailto: or HTTPS) |
AI_BASE_URL |
unset | OpenAI-compatible model API base URL for the assistant |
AI_API_KEY |
unset | Server-only key for that model API |
AI_MODEL |
gpt-4o-mini |
Model ID supported by the configured API |
Operations
- Backups. The daily 02:00 UTC backup and the admin Backups page work
unchanged; files land under
/data/blobs/backups. Back up the whole volume for a full copy. - Updates. Pull the new image and recreate the container; migrations run at start. The in-app update button is disabled on self-hosted installs.
- Logs.
docker compose logs -f mailflare. - Queues. Jobs are held in memory. Inbound mail is written to the volume before it is queued, so a restart never loses a message; at worst one stays unparsed until it is re-imported.
Email assistant and MCP
Set AI_BASE_URL, AI_API_KEY, and AI_MODEL in the container environment to configure the built-in assistant. These values stay on the server. Assistant chat is available by default when a provider is configured; a mailbox manager can change its writing instructions and availability through the settings button in the assistant panel. Automatic reply drafts remain off until enabled there. Auto-draft work is recorded in SQLite and retried after a restart by the local scheduler. AI failure does not reject inbound mail. Out-of-office auto-replies and AI auto-drafts are separate features; turn off out-of-office replies before enabling auto-drafts for a mailbox.
The MCP endpoint is /mcp. Create a dedicated mailbox-scoped Bearer key in Assistant → MCP and give the key to a client that supports custom HTTP headers. The endpoint uses Streamable HTTP; request_send gives the client a review URL, and only an authenticated Mailflare browser session can confirm delivery. MCP read and draft tools remain available when no AI model is configured.
Running without Docker
npm ci
npm run build:node
MAILFLARE_RUNTIME=node NODE_ENV=production DATA_DIR=./data node dist/server.mjs
Port 25 needs root or a capability (setcap cap_net_bind_service=+ep); use
SMTP_INBOUND_PORT=2525 behind a port forward otherwise.