Blue Folder

INSTALL GUIDE · DOCKER

Blue Folder Mail with Docker

The canonical self-host path. One container, one volume, one command. Works on any Linux box with the Docker Compose plugin — a VPS, a cloud VM, a Mac, a NAS or a Raspberry Pi.

Great fit
A single always-on Linux box with Docker is the happy path. Your data lives in a volume you back up, and the app is a single container.

Prerequisites

Install — one command

The installer checks Docker, fetches the code, prompts for your secrets and starts the stack.

curl -fsSL https://raw.githubusercontent.com/gordonvividtone/bluefolder-mail/main/install.sh | bash

Or do it by hand from a clone

  1. Clone and configure
    git clone https://github.com/gordonvividtone/bluefolder-mail.git
    cd bluefolder-mail
    cp .env.example .env

    Edit .env and set at least INBOUND_SECRET (a long random value — openssl rand -hex 24) and RESEND_API_KEY.

  2. Start it
    docker compose up -d
  3. Open and create your mailbox

    Go to http://localhost:8080, generate your key and save your backup password — it can't be recovered — then add your domains in Settings.

Useful commands: docker compose logs -f to tail logs, docker compose down to stop while keeping your data, docker compose pull && docker compose up -d to update.

Where your data lives

The SQLite metadata index and your sealed mail blobs persist in the bfm-data volume. Back that volume up and you've backed up everything.

Default — local disk

Nothing to configure. The container writes to the volume. Good for a single box.

Optional — S3-compatible object storage

To keep sealed blobs in a bucket (AWS S3, Backblaze B2, MinIO or Cloudflare R2), set in .env:

STORAGE_BACKEND=s3
S3_ENDPOINT=...
S3_BUCKET=...
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...

A commented MinIO service ships in docker-compose.yml if you want object storage on the same box. With S3, the only stateful piece left is the SQLite index — keep that on the volume.

The blobs are already sealed to your public key before they're stored, so the bucket (or anyone with it) only ever holds ciphertext.

Sending mail — Resend

Outbound goes through the Resend HTTP API, the same as on Cloudflare.

Receiving mail — the honest part

Receiving internet email reliably is the genuinely hard part of self-hosting. There are two paths.

Recommended — webhook

Let an email provider do the hard MTA work (MX, TLS, spam, reputation) and have it POST each message to your instance, which seals and stores it through the same ingestion as everything else.

Works with Mailgun Routes, SendGrid Inbound Parse, Postal and Resend inbound, among others. (Before your mailbox exists, /_inbound returns 503 so the provider retries; a wrong secret returns 401.)

Advanced — built-in SMTP

Set ENABLE_SMTP=1 and expose port 25 to run a real inbound MTA. Only do this if you can satisfy all of: a static public IP with matching PTR / reverse-DNS, correct MX records, SPF/DKIM/DMARC per domain, and a clean-reputation IP.

Port 25 outbound is blocked by default on AWS, GCP, Azure and DigitalOcean, and most cloud and residential IPs are blocklisted. Get any of this wrong and mail silently lands in spam or is rejected. The webhook path exists precisely so you don't have to run an MTA.

HTTPS & the front door

Off Cloudflare there's no Cloudflare Access, so ACCESS_ENFORCE=0 and the port is reachable by anyone who can hit it. Add auth and TLS before exposing it publicly — pick one:

A two-line Caddy site block gives you automatic Let's Encrypt TLS:

mail.example.com {
    reverse_proxy localhost:8080
}
Even unprotected, the server only ever holds ciphertext it cannot decrypt — but anyone reaching the API could list metadata or move and delete messages, so always put auth in front.
That's the whole thing. Data in a volume, sending via Resend, inbound by webhook, TLS via Caddy. The full reference is DEPLOY.md. Other platforms →