A fully self-hosted mail server in a single Docker container.
Send, receive, and manage email β with a sleek web admin panel, built-in webmail, open tracking, fail2ban protection, and DKIM signing. No complex setup. No third-party dependencies.
Less moving parts. Less failure.
Alpine Β· Postfix Β· Dovecot Β· OpenDKIM Β· Rust Β· PostgreSQL β all in one container.
| Feature | Description |
|---|---|
| π 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 |
| π€ User Accounts | Create mailboxes with passwords and storage quotas |
| π Aliases & Catch-all | Forward addresses, wildcards (*@domain.com), and routing rules |
| π‘ Open Tracking | Pixel-based email open tracking with per-message reports |
| π Built-in Webmail | Read, compose, and manage email directly from your browser |
| π Fail2ban Protection | Auto-ban IPs on repeated auth failures; manage whitelist & blacklist |
| π‘οΈ 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 guidance |
| π Webhook Notifications | Send HTTP webhooks on processed outbound emails |
| βοΈ Config Viewer | Inspect live Postfix/Dovecot/OpenDKIM configs from the UI |
Docker Compose starts Mailserver together with a PostgreSQL database automatically:
cp .env.example .env
# Edit .env to set your HOSTNAME and other settings
docker compose up -dThen open http://your-server:8080 in your browser.
If you already have a PostgreSQL instance, you can run the container directly:
docker run -d --name mailserver \
-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:mailserver@your-pg-host/mailserver \
ghcr.io/tayyebi/mailserver:main| Field | Value |
|---|---|
| Username | admin |
| Password | admin |
β οΈ Change your password immediately after first login via Settings.
Enable 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, PTR).
Create email accounts for your users. Set display names, passwords, and per-account storage quotas.
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 per 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.
A lightweight webmail client is 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 β all without leaving the browser.
Mailserver includes a built-in fail2ban system that watches /var/log/mail.log for repeated authentication failures on SMTP, IMAP, and POP3. Offending IPs are automatically banned. You can:
- Configure thresholds and ban duration per service
- Manually ban or unban individual IPs or CIDR ranges
- Maintain a permanent whitelist and blacklist
- Review a full audit log of all ban/unban events
Inspect the live Postfix mail queue and flush stuck messages directly from the admin panel β no SSH required.
Per-domain DNS health checker with individual shortcut links for each record type. Catch delivery problems before they affect your users.
Inspect the live Postfix, Dovecot, and OpenDKIM configuration files generated from your database β useful for debugging.
| 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 |
All 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 |
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 |
SEED_PASS |
admin |
Initial admin password |
TZ |
UTC |
Container timezone |
All mail data is stored in the maildata Docker volume mounted at /data:
| Path | Contents |
|---|---|
/data/ssl/ |
TLS certificates (auto-generated self-signed on first start) |
/data/dkim/ |
DKIM signing keys |
/data/mail/ |
User mailboxes (Maildir format) |
The PostgreSQL database (accounts, domains, aliases, tracking data) is required by the mail server. When using Docker Compose, it runs in a separate db container with its data stored in the maildb volume. When running standalone, point DATABASE_URL to your own PostgreSQL instance.
After adding a domain in the admin panel, go to Domains β DNS to get the exact DNS records you need to publish:
- 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
- PTR β reverse DNS (set at your VPS provider)
The dashboard shows copy-pasteable values for every record.
graph LR
Internet((Internet))
subgraph Docker Container
Supervisor[Supervisord]
Admin[Rust Admin Dashboard :8080]
Filter[Content Filter + Footer Injector]
Postfix[Postfix SMTP :25/587/465]
Dovecot[Dovecot IMAP/POP3 :143/993/110/995]
OpenDKIM[OpenDKIM]
Postgres[(PostgreSQL DB)]
Supervisor --> Admin
Supervisor --> Postfix
Supervisor --> Dovecot
Supervisor --> OpenDKIM
Admin -->|read/write| Postgres
Filter -->|tracking & footer lookups| Postgres
Admin -->|generate configs from DB| Postfix
Admin -->|generate passwd from DB| Dovecot
Admin -->|generate key tables from DB| OpenDKIM
Postfix -->|DKIM signing| OpenDKIM
Postfix -->|LMTP delivery| Dovecot
Postfix -->|pipe emails| Filter
Filter -->|reinject via SMTP :10025| Postfix
end
subgraph Persistent Volume /data
SSL["/data/ssl"]
DKIM["/data/dkim"]
Mail["/data/mail"]
DB["/data/db"]
end
Internet -->|SMTP| Postfix
Internet -->|IMAP/POP3| Dovecot
Internet -->|HTTPS| Admin
Postgres --- DB
Dovecot --- Mail
Postfix --- SSL
OpenDKIM --- DKIM
sequenceDiagram
participant Sender as Sender (Internet)
participant Postfix
participant Filter as Content Filter
participant Postgres as PostgreSQL DB
participant OpenDKIM
participant Dovecot
participant Recipient as Recipient (Mailbox)
Note over Sender,Recipient: Inbound Email
Sender->>Postfix: SMTP :25
Postfix->>Dovecot: LMTP :24
Dovecot->>Recipient: store in Maildir
Note over Sender,Recipient: Outbound Email
Sender->>Postfix: SMTP :587 (authenticated)
Postfix->>Filter: pipe via pixelfilter
Filter->>Postgres: lookup tracking + footer_html
alt Footer configured
Filter->>Filter: inject domain footer (HTML/plain text)
end
alt Tracking enabled
Filter->>Postgres: insert tracked_message
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
Note over Sender,Recipient: Tracking Pixel Open
Sender->>Postfix: (later) recipient opens email
Recipient->>Postgres: GET /pixel?id=... β record pixel_open
