A fully self-hosted mail server — single Docker container or single binary.
Send, receive, and manage email with a sleek web admin panel, built-in webmail, open tracking, fail2ban protection, DKIM signing, CalDAV/CardDAV, and more. No complex setup. No third-party dependencies.
Less moving parts. Less failure.
Alpine · Postfix · Dovecot · OpenDKIM · Rust · PostgreSQL
- Features
- Installation
- First Login
- Admin Dashboard
- Port Reference
- Configuration
- Persistent Data
- DNS Setup
- Architecture
- Email Flow
| Feature | Description |
|---|---|
| 🛠️ Auto-Provisioning | One-command SSH deployment to any Linux VPS — idempotent, verbose, zero credential storage |
| 📋 Admin Dashboard | Clean web UI to manage every aspect of your mail server |
| 🌐 Domain Management | Add unlimited mail domains with one-click DKIM key generation and per-domain BIMI logo support |
| 👤 User Accounts | Create mailboxes with display names, passwords, and per-account storage quotas |
| 🔀 Aliases & Catch-all | Forward addresses, wildcards (*@domain.com), and flexible routing rules |
| 📤 Forwarding | Forward mail from local addresses to any external destination, with optional local copy |
| 🦶 Email Footers | Automatically inject branded HTML and plain-text footers into outbound mail per domain |
| 📡 Open Tracking | Pixel-based email open tracking with per-message reports and conditional rules |
| ⏱️ Rate Limiting | Configurable per-account/per-domain outbound sending rate limits with conditional rules |
| 🌐 Built-in Webmail | Read, compose, and manage email directly from your browser with IMAP IDLE push |
| 🔒 Fail2ban Protection | Auto-ban IPs on repeated auth failures; manage whitelist & blacklist with full audit log |
| 🛡️ 2FA (TOTP) | Two-factor authentication for the admin panel |
| 📦 Queue Management | View and flush the Postfix mail queue from the dashboard |
| 🗑️ Unsubscribe Management | Track and manage unsubscribe requests |
| 🔍 DNSBL / Spam Blocking | DNS block-list management integrated with Postfix |
| 📄 DNS Runbook | Per-domain DNS record viewer with SPF, DKIM, DMARC, BIMI guidance |
| 📊 DMARC Reports | Designate inboxes to receive DMARC aggregate reports and visualize pass/fail results |
| 🔁 Outbound Relays | Route outbound mail through external SMTP servers, assignable per domain, account, or alias |
| 🔔 Webhook Notifications | Send HTTP webhooks on processed outbound emails |
| ⚙️ Config Viewer | Inspect live Postfix/Dovecot/OpenDKIM configs from the UI |
| 📁 WebDAV File Storage | Per-account WebDAV server at /dav/{email}/ for file storage and FileLink sharing |
| 📅 CalDAV Calendar Server | Per-account CalDAV server at /caldav/{email}/ for calendar sync with any CalDAV client |
| 📇 CardDAV Contact Server | Per-account CardDAV server at /carddav/{email}/ for contact sync with any CardDAV client |
| 🖼️ BIMI Support | Serve per-domain SVG brand logos at /bimi/{domain}/logo.svg for supporting mail clients |
| 🤖 MCP API | Model Context Protocol endpoint for AI assistant integration (list/read/send/delete email) |
| 📡 REST & SOAP APIs | Programmatic access to mail operations via REST and SOAP endpoints |
| 📝 Self-Registration | Optional user self-registration portal for invite-based account creation |
| 🚨 Abuse Reporting | Built-in abuse complaint handling and reporting |
The simplest path. Docker Compose starts the mail server and a PostgreSQL database together with a single command. Everything — TLS certificates, DKIM keys, Postfix/Dovecot/OpenDKIM configs — is generated automatically on first start.
Prerequisites: Docker Engine 24+ and Docker Compose v2.
Step 1 — Clone the repository and create your environment file
git clone https://github.com/tayyebi/mailserver.git
cd mailserver
cp .env.example .envStep 2 — Set your hostname
Open .env and set HOSTNAME to the fully-qualified domain name you'll use for mail (e.g. mail.example.com). Change SEED_PASS while you're there.
# .env (minimum required change)
HOSTNAME=mail.example.com
SEED_PASS=changemeStep 3 — Start the stack
docker compose up -dThis starts two containers:
db— PostgreSQL 16 (data stored in themaildbvolume)mailserver— the mail server (data stored in themaildatavolume)
Step 4 — Open the admin dashboard
http://your-server-ip:8080
Login with admin / changeme (or whatever you set in SEED_PASS).
Upgrading
docker compose pull
docker compose up -dUse this if you already have a PostgreSQL instance you want to reuse.
Prerequisites: Docker Engine 24+ and a running PostgreSQL instance.
Step 1 — Create the database
CREATE USER mailserver WITH PASSWORD 'strongpassword';
CREATE DATABASE mailserver OWNER mailserver;Step 2 — Run the container
docker run -d --name mailserver \
--restart unless-stopped \
-p 25:25 -p 587:587 -p 465:465 -p 2525:2525 \
-p 143:143 -p 993:993 -p 110:110 -p 995:995 \
-p 8080:8080 \
-v maildata:/data \
-e HOSTNAME=mail.example.com \
-e DATABASE_URL=postgres://mailserver:strongpassword@your-pg-host/mailserver \
-e SEED_PASS=changeme \
-e TZ=UTC \
ghcr.io/tayyebi/mailserver:mainStep 3 — Open the admin dashboard
http://your-server-ip:8080
To put the admin panel behind HTTPS, place Nginx or Caddy in front and proxy to port 8080. The mail ports (25, 587, 465, 143, 993, etc.) connect directly.
The mailserver binary is fully self-contained: config templates, database migrations, and static assets are all compiled in. You only need to install the system mail services it manages.
Supported distros: Debian/Ubuntu (tested), Alpine, RHEL/CentOS (via dnf/yum).
Debian / Ubuntu:
apt-get update
apt-get install -y \
postfix postfix-pcre \
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd \
opendkim opendkim-tools \
openssl curl \
postgresql postgresql-clientWhen the Postfix installer prompts for a mail type, choose "No configuration" — the binary generates the config itself.
Alpine:
apk add --no-cache \
postfix dovecot dovecot-lmtpd dovecot-pop3d \
opendkim opendkim-utils \
openssl curl \
postgresql16# Start PostgreSQL (Debian/Ubuntu)
systemctl enable --now postgresql
# Create the database and user
sudo -u postgres psql <<'SQL'
CREATE USER mailserver WITH PASSWORD 'strongpassword';
CREATE DATABASE mailserver OWNER mailserver;
SQLDownload the latest binary from the Releases page or pull it from the container image:
# Option A — from GitHub Releases
curl -L https://github.com/tayyebi/mailserver/releases/latest/download/mailserver \
-o /usr/local/bin/mailserver
chmod +x /usr/local/bin/mailserver
# Option B — extract from the Docker image (always matches main branch)
docker create --name tmp ghcr.io/tayyebi/mailserver:main
docker cp tmp:/usr/local/bin/mailserver /usr/local/bin/mailserver
docker rm tmp
chmod +x /usr/local/bin/mailservermkdir -p /etc/mailserver
cat > /etc/mailserver/env <<'EOF'
HOSTNAME=mail.example.com
DATABASE_URL=postgres://mailserver:strongpassword@localhost/mailserver
ADMIN_PORT=8080
SEED_USER=admin
SEED_PASS=changeme
TZ=UTC
EOF
chmod 600 /etc/mailserver/env# vmail user for Dovecot mailbox ownership
useradd -r -s /sbin/nologin -d /dev/null vmail 2>/dev/null || true
# Required directories
mkdir -p /data/ssl /data/dkim /data/mail
chown -R vmail:vmail /data/mailset -a; source /etc/mailserver/env; set +a
# Generate TLS certificates (skipped if /data/ssl/cert.pem already exists)
mailserver gencerts
# Seed the admin user into the database
mailserver seed
# Generate Postfix / Dovecot / OpenDKIM config files
mailserver genconfigmailserver genconfig writes the live mail-service config directly into the system paths used by Postfix, Dovecot, and OpenDKIM. There is no extra "copy templates into /etc" step on bare metal.
The main generated files are:
/etc/postfix/main.cf
/etc/postfix/master.cf
/etc/postfix/virtual_domains
/etc/postfix/vmailbox
/etc/postfix/virtual_aliases
/etc/postfix/recipient_bcc
/etc/postfix/sender_login_maps
/etc/postfix/transport_maps
/etc/postfix/sasl_passwd
/etc/dovecot/dovecot.conf
/etc/dovecot/passwd
/etc/opendkim/opendkim.conf
/etc/opendkim/KeyTable
/etc/opendkim/SigningTable
/etc/opendkim/TrustedHosts
Postfix map files are also compiled with postmap during genconfig, and DKIM private keys are written under /data/dkim.
You can quickly inspect the generated files before enabling services:
ls -l /etc/postfix /etc/dovecot /etc/opendkim
postconf -nImportant: Treat these files as generated output. If you re-run
mailserver genconfig(or start the managed service), it will regenerate them in place.
cat > /etc/systemd/system/mailserver.service <<'EOF'
[Unit]
Description=Mailserver (Postfix + Dovecot + OpenDKIM)
After=network.target postgresql.service
Wants=network.target
[Service]
Type=simple
EnvironmentFile=/etc/mailserver/env
ExecStartPre=/usr/local/bin/mailserver genconfig
ExecStart=/usr/local/bin/mailserver serve
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now mailserverNote:
servemanages configuration regeneration but does not start Postfix/Dovecot/OpenDKIM — those are managed by the OS service manager. Theentrypoint.shin the Docker image handles starting all four together; for bare-metal you manage each service separately.
Start the mail services:
systemctl enable --now dovecot opendkim postfixhttp://your-server-ip:8080
If you ever lose access:
RESET_USER=admin RESET_PASS=newpassword mailserver reset-passwordSpin up a fresh mailserver on any Linux VPS in one command — no manual SSH steps, no config files to write by hand. Run this from your local machine.
Prerequisites: The mailserver binary on your local machine (see Step 3 above).
mailserver provision \
--host mail.example.com \
--user root \
--key ~/.ssh/id_ed25519The command connects over SSH and idempotently:
- Detects the package manager —
apt-get,apk,dnf, oryum - Installs system dependencies — Postfix, Dovecot, OpenDKIM, OpenSSL, PostgreSQL — skipped if already present
- Creates system users and directories —
vmail,opendkim,/data/…— skipped if already present - Uploads the current binary — copies itself to
/usr/local/bin/mailserver - Runs initial setup —
gencerts(skipped if certs exist),seed,genconfig - Installs the system service — writes a
systemdunit (or OpenRC init script on Alpine) — skipped if already installed - Enables and starts the service
Every step produces verbose log output so you can see exactly what is and isn't being done.
Options:
| Flag | Default | Description |
|---|---|---|
--host <host> |
(required) | Remote hostname or IP address |
--port <port> |
22 |
SSH port |
--user <user> |
(required) | SSH login username |
--key <path> |
— | Path to SSH private key (recommended) |
--password <pwd> |
— | Password for SSH auth or passphrase for an encrypted key |
--env-file <path> |
— | Local .env file to upload as /etc/mailserver/env |
Credentials are held in memory only — they are never written to disk.
Examples:
# Key-based auth (recommended)
mailserver provision --host mail.example.com --user root --key ~/.ssh/id_ed25519
# Upload an environment file (sets HOSTNAME, DATABASE_URL, SEED_PASS, etc. on the remote)
mailserver provision --host mail.example.com --user root \
--key ~/.ssh/id_ed25519 --env-file .env.prod
# Password auth
mailserver provision --host mail.example.com --user root --password s3cr3tUse the included manifest file if you deploy on Kubernetes providers (EKS/GKE/AKS, etc.).
It uses the current registry image: ghcr.io/tayyebi/mailserver:main.
Step 1 — Edit required placeholders
Update these values in k8s-mailserver-manifest.yaml before applying:
DATABASE_URLSEED_PASSHOSTNAME
Step 2 — Apply the manifest
kubectl apply -f k8s-mailserver-manifest.yamlStep 3 — Get external endpoint
kubectl -n mailserver get svc mailserverUse the service external IP/hostname to access the admin panel on port 8080.
Route mail ports (25/465/587/143/993/110/995) to the same endpoint.
Also configure DNS records for mail delivery:
A/AAAA— resolves your mail host name to the load balancer endpointMX— routes domain mail flow to your mail hostPTR— reverse DNS for the public IP (required by many receiving providers)SPF,DKIM,DMARC— sender authentication and deliverability protection
| Field | Value |
|---|---|
| URL | http://your-server:8080 |
| Username | admin (or SEED_USER) |
| Password | admin (or SEED_PASS) |
⚠️ Change your password immediately after first login via Settings → Password.
If you lose access, reset the password without a login session:
RESET_USER=admin RESET_PASS=newpassword mailserver reset-passwordEnable TOTP-based 2FA from the Settings page. Once enabled, append your 6-digit code to your password at login.
Example: password secret + TOTP 123456 → enter secret123456
Add your mail domains, generate DKIM signing keys with one click, and get a ready-to-use DNS runbook showing every record you need (MX, SPF, DKIM, DMARC, BIMI, PTR). Upload a per-domain SVG logo for BIMI support in compatible mail clients.
Create email accounts for your users. Set display names, passwords, and per-account storage quotas. Each account automatically gets WebDAV, CalDAV, and CardDAV access at the corresponding endpoints.
Create forwarding rules between addresses. Use *@yourdomain.com as a catch-all to capture mail sent to any address on the domain. Toggle open tracking and footer injection per alias.
Set up rules to forward mail from a local address to any external email address. Optionally keep a local copy in the original mailbox.
Define HTML and plain-text footers that are automatically appended to outbound emails. Rules let you scope footers by sender pattern, domain, or alias.
When tracking is enabled on an alias, outgoing emails get a tiny invisible tracking pixel injected into the HTML body. Every time the recipient opens the email, a record is created. View detailed per-message open reports from the Tracking section.
Define per-account or per-domain outbound sending rate limits (e.g. max N messages per hour). Conditional rules allow fine-grained control.
A lightweight webmail client built right into the admin panel. Browse folders, read messages, compose new emails (with CC, BCC, Reply-To, priority, and custom headers), and delete messages. Uses IMAP IDLE for real-time push delivery of new messages.
Monitors Postfix and Dovecot logs for repeated authentication failures. Offending IPs are automatically banned. Configure thresholds, manage whitelist/blacklist, and review a full audit log.
Inspect the live Postfix mail queue and flush stuck messages directly from the admin panel — no SSH required.
Designate one or more mailboxes as DMARC report inboxes. The dashboard automatically parses incoming DMARC aggregate reports and visualizes pass/fail results.
Per-domain DNS health checker. Catch delivery problems before they affect your users.
Inspect the live Postfix, Dovecot, and OpenDKIM configuration files generated from your database.
Configure external SMTP relays to route outbound mail through third-party providers (SendGrid, SES, etc.). Relays can be assigned globally or scoped to a specific domain, account, or alias.
Each mail account gets a personal WebDAV drive at /dav/{email}/. Users can mount it in their OS file manager and share individual files via one-time FileLink download URLs.
A built-in CalDAV server at /caldav/{email}/ for syncing calendars with Thunderbird, Apple Calendar, DAVx⁵ on Android, etc.
A built-in CardDAV server at /carddav/{email}/ for syncing contacts with Apple Contacts, Thunderbird, DAVx⁵, etc.
A Model Context Protocol endpoint at POST /mcp exposes mail operations to AI assistants. Supported tools: list_accounts, list_emails, read_email, send_email, delete_email.
| Port | Protocol | Purpose |
|---|---|---|
25 |
SMTP | Inbound mail from the Internet |
587 |
SMTP Submission | Outbound mail (authenticated) |
465 |
SMTPS | Outbound mail over TLS (authenticated) |
2525 |
SMTP Alt | Alternative submission port |
143 |
IMAP | Email retrieval (STARTTLS) |
993 |
IMAPS | Email retrieval over TLS |
110 |
POP3 | Email retrieval (STARTTLS) |
995 |
POP3S | Email retrieval over TLS |
8080 |
HTTP | Admin dashboard, webmail, WebDAV, CalDAV, CardDAV, APIs |
All runtime settings are managed from the admin dashboard. The only file you need to edit before starting is .env:
| Variable | Default | Description |
|---|---|---|
HOSTNAME |
mail.example.com |
Fully-qualified domain name of the mail server |
ADMIN_PORT / HTTP_PORT |
8080 |
Admin dashboard port |
SMTP_PORT |
25 |
Inbound SMTP port |
SUBMISSION_PORT |
587 |
Submission port |
DATABASE_URL |
postgres://mailserver:mailserver@localhost/mailserver |
PostgreSQL connection string |
SEED_USER |
admin |
Initial admin username (used only on first seed run) |
SEED_PASS |
admin |
Initial admin password (used only on first seed run) |
TZ |
UTC |
Timezone |
All mail data is stored under /data:
| Path | Contents |
|---|---|
/data/ssl/ |
TLS certificates (auto-generated self-signed on first start) |
/data/dkim/ |
DKIM signing keys (generated per domain from the dashboard) |
/data/mail/ |
User mailboxes in Maildir format (/data/mail/{domain}/{user}/Maildir) |
When using Docker Compose, /data is stored in the maildata volume. On bare metal, it lives directly on the host. Back up the entire /data directory and your PostgreSQL database to preserve all mail and configuration.
After adding a domain in the admin panel, go to Domains → DNS to get the exact DNS records you need to publish:
| Record | Purpose |
|---|---|
| MX | Points incoming mail to your server |
| SPF | Authorizes your server to send mail for the domain |
| DKIM | Cryptographic signature for outbound mail (key generated in the dashboard) |
| DMARC | Policy for handling SPF/DKIM failures |
| BIMI | Brand logo display in supporting mail clients (requires DMARC enforcement) |
| PTR | Reverse DNS — set at your VPS provider |
The dashboard shows copy-pasteable values for every record.
graph TB
Internet((Internet))
subgraph container ["Docker Container / Bare-metal Server"]
direction TB
subgraph incoming ["Inbound Path"]
Postfix["Postfix\nSMTP :25 / :587 / :465"]
Dovecot["Dovecot\nIMAP :143/:993 POP3 :110/:995"]
OpenDKIM["OpenDKIM\nDKIM milter :8891"]
end
subgraph outbound ["Outbound Pipeline"]
Filter["Content Filter\n(footer · tracking · rate-limit)"]
end
subgraph app ["Rust Application :8080"]
Admin["Admin Dashboard"]
Webmail["Webmail (IMAP IDLE)"]
WebDAV["WebDAV /dav/"]
CalDAV["CalDAV /caldav/"]
CardDAV["CardDAV /carddav/"]
Pixel["Pixel Tracker /pixel/"]
MCP["MCP API /mcp"]
BIMI["BIMI /bimi/"]
end
Postgres[("PostgreSQL")]
end
subgraph volume ["Persistent Data /data"]
SSL["/data/ssl"]
DKIMStore["/data/dkim"]
Mail["/data/mail"]
end
Internet -->|"SMTP :25"| Postfix
Internet -->|"SMTP :587/:465"| Postfix
Internet -->|"IMAP/POP3"| Dovecot
Internet -->|"HTTP :8080"| app
Postfix -->|"LMTP :24"| Dovecot
Postfix -->|"pipe (outbound)"| Filter
Postfix <-->|"DKIM milter"| OpenDKIM
Filter -->|"reinject :10025"| Postfix
Filter <-->|"lookups"| Postgres
Admin -->|"read / write"| Postgres
Admin -->|"genconfig"| Postfix
Admin -->|"genconfig"| Dovecot
Admin -->|"genconfig"| OpenDKIM
Webmail -->|"IMAP"| Dovecot
WebDAV -->|"read / write"| Postgres
CalDAV -->|"read / write"| Postgres
CardDAV -->|"read / write"| Postgres
Pixel -->|"record open"| Postgres
MCP -->|"read / write"| Postgres
Postfix --- SSL
OpenDKIM --- DKIMStore
Dovecot --- Mail
sequenceDiagram
actor Sender as Sender
participant Postfix
participant Filter as Content Filter
participant Postgres as PostgreSQL
participant OpenDKIM
participant Dovecot
actor Recipient as Recipient
rect rgb(230, 244, 255)
Note over Sender,Recipient: Inbound Email
Sender->>Postfix: SMTP :25
Postfix->>Dovecot: LMTP :24
Dovecot->>Recipient: store in Maildir
end
rect rgb(230, 255, 235)
Note over Sender,Recipient: Outbound Email
Sender->>Postfix: SMTP :587 (authenticated)
Postfix->>Filter: pipe via pixelfilter
Filter->>Postgres: check rate limit rules
alt Rate limit exceeded
Filter-->>Sender: reject (552)
end
Filter->>Postgres: lookup footer_html & tracking config
alt Footer configured
Filter->>Filter: inject footer (HTML + plain text)
end
alt Open tracking enabled
Filter->>Postgres: insert tracked_message record
Filter->>Filter: inject tracking pixel into HTML body
end
Filter->>Postfix: reinject via SMTP :10025
Postfix->>OpenDKIM: DKIM sign (milter :8891)
OpenDKIM-->>Postfix: signed message
Postfix->>Recipient: deliver to remote MTA
end
rect rgb(255, 250, 230)
Note over Sender,Recipient: Tracking Pixel Open
Recipient->>Postfix: (later) recipient opens email
Recipient->>Postgres: GET /pixel?id=… → record pixel_open
end