Mailu
Full-featured open-source mail server as a set of Docker containers: SMTP (Postfix), IMAP (Dovecot), Rspamd antispam, admin UI with DKIM management and Roundcube webmail.
Configuration
version: "3.8"
# Shared Mailu configuration (equivalent to the upstream mailu.env file)
x-mailu-env: &mailu-env
- SECRET_KEY=${SECRET_KEY}
- DOMAIN=${DOMAIN}
- HOSTNAMES=${DOMAIN}
- POSTMASTER=admin
# TLS for the mail protocols is obtained through Let's Encrypt (HTTP-01
# through Traefik on port 80). The web UI itself is served plain HTTP to
# Traefik, which terminates HTTPS.
- TLS_FLAVOR=mail-letsencrypt
# Mailu grants relay/XCLIENT trust to this network range. It must cover the
# Docker networks of this project (Docker allocates them from 172.16.0.0/12
# by default). Narrow it down if you know your exact subnet.
- SUBNET=${SUBNET}
- ADMIN=true
- WEBMAIL=roundcube
- API=false
- WEBDAV=none
- ANTIVIRUS=none
- SCAN_MACROS=false
# Ports enabled inside the front container. 4190 (sieve) stays internal for
# the webmail filters UI and is not published on the host.
- PORTS=25,80,443,465,587,993,4190
- MESSAGE_SIZE_LIMIT=50000000
- FULL_TEXT_SEARCH=en
- WEBROOT_REDIRECT=/webmail
- WEB_ADMIN=/admin
- WEB_WEBMAIL=/webmail
- SITENAME=Mailu
- WEBSITE=https://mailu.io
# Trust Traefik (dokploy-network) to forward the real client IP for rate limiting
- REAL_IP_FROM=${SUBNET}
- REAL_IP_HEADER=X-Forwarded-For
# Initial admin account: admin@${DOMAIN} (created only if it does not exist)
- INITIAL_ADMIN_ACCOUNT=${INITIAL_ADMIN_ACCOUNT}
- INITIAL_ADMIN_DOMAIN=${DOMAIN}
- INITIAL_ADMIN_PW=${INITIAL_ADMIN_PW}
- INITIAL_ADMIN_MODE=ifmissing
# Public DNSSEC-validating resolvers. The admin container refuses to start
# without DNSSEC validation, and Postfix needs it for outbound DANE. Service
# discovery still uses Docker's embedded DNS; these are only upstreams.
x-mailu-dns: &mailu-dns
- "1.1.1.1"
- "8.8.8.8"
services:
front:
image: ghcr.io/mailu/nginx:2024.06
restart: unless-stopped
environment: *mailu-env
dns: *mailu-dns
# Mail protocol ports are published directly on the host (same approach as
# the poste.io template). They will fail to bind if another mail server
# already uses them on this machine.
ports:
- "25:25" # SMTP (server to server)
- "465:465" # SMTPS submission
- "587:587" # Submission (STARTTLS)
- "993:993" # IMAPS
volumes:
- mailu-certs:/certs
# The image's built-in healthcheck also requires the mail (Dovecot) proxy,
# which only starts once TLS certificates exist. Behind Traefik that
# deadlocks: unhealthy -> Traefik drops the route -> the ACME challenge can
# never be answered. Check only nginx instead (health endpoint returns 204).
healthcheck:
test: ["CMD-SHELL", "curl -m3 -skfLo /dev/null http://127.0.0.1:10204/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
depends_on:
- admin
admin:
image: ghcr.io/mailu/admin:2024.06
restart: unless-stopped
environment: *mailu-env
dns: *mailu-dns
volumes:
- mailu-data:/data
- mailu-dkim:/dkim
depends_on:
- redis
imap:
image: ghcr.io/mailu/dovecot:2024.06
restart: unless-stopped
environment: *mailu-env
dns: *mailu-dns
volumes:
- mailu-mail:/mail
depends_on:
- front
smtp:
image: ghcr.io/mailu/postfix:2024.06
restart: unless-stopped
environment: *mailu-env
dns: *mailu-dns
volumes:
- mailu-mailqueue:/queue
depends_on:
- front
antispam:
image: ghcr.io/mailu/rspamd:2024.06
restart: unless-stopped
hostname: antispam
environment: *mailu-env
dns: *mailu-dns
volumes:
- mailu-filter:/var/lib/rspamd
depends_on:
- front
- redis
webmail:
image: ghcr.io/mailu/webmail:2024.06
restart: unless-stopped
environment: *mailu-env
dns: *mailu-dns
volumes:
- mailu-webmail:/data
depends_on:
- front
- imap
redis:
image: redis:alpine
restart: unless-stopped
volumes:
- mailu-redis:/data
volumes:
mailu-certs: {}
mailu-data: {}
mailu-dkim: {}
mailu-mail: {}
mailu-mailqueue: {}
mailu-filter: {}
mailu-webmail: {}
mailu-redis: {}[variables]
main_domain = "${domain}"
secret_key = "${password:16}"
admin_password = "${password:32}"
[config]
[[config.domains]]
serviceName = "front"
port = 80
host = "${main_domain}"
[config.env]
DOMAIN = "${main_domain}"
SECRET_KEY = "${secret_key}"
INITIAL_ADMIN_ACCOUNT = "admin"
INITIAL_ADMIN_PW = "${admin_password}"
SUBNET = "172.16.0.0/12"Base64
To import this template in Dokploy: create a Compose service → Advanced → Base64 import and paste the content below:
ewogICJjb21wb3NlIjogInZlcnNpb246IFwiMy44XCJcblxuIyBTaGFyZWQgTWFpbHUgY29uZmlndXJhdGlvbiAoZXF1aXZhbGVudCB0byB0aGUgdXBzdHJlYW0gbWFpbHUuZW52IGZpbGUpXG54LW1haWx1LWVudjogJm1haWx1LWVudlxuICAtIFNFQ1JFVF9LRVk9JHtTRUNSRVRfS0VZfVxuICAtIERPTUFJTj0ke0RPTUFJTn1cbiAgLSBIT1NUTkFNRVM9JHtET01BSU59XG4gIC0gUE9TVE1BU1RFUj1hZG1pblxuICAjIFRMUyBmb3IgdGhlIG1haWwgcHJvdG9jb2xzIGlzIG9idGFpbmVkIHRocm91Z2ggTGV0J3MgRW5jcnlwdCAoSFRUUC0wMVxuICAjIHRocm91Z2ggVHJhZWZpayBvbiBwb3J0IDgwKS4gVGhlIHdlYiBVSSBpdHNlbGYgaXMgc2VydmVkIHBsYWluIEhUVFAgdG9cbiAgIyBUcmFlZmlrLCB3aGljaCB0ZXJtaW5hdGVzIEhUVFBTLlxuICAtIFRMU19GTEFWT1I9bWFpbC1sZXRzZW5jcnlwdFxuICAjIE1haWx1IGdyYW50cyByZWxheS9YQ0xJRU5UIHRydXN0IHRvIHRoaXMgbmV0d29yayByYW5nZS4gSXQgbXVzdCBjb3ZlciB0aGVcbiAgIyBEb2NrZXIgbmV0d29ya3Mgb2YgdGhpcyBwcm9qZWN0IChEb2NrZXIgYWxsb2NhdGVzIHRoZW0gZnJvbSAxNzIuMTYuMC4wLzEyXG4gICMgYnkgZGVmYXVsdCkuIE5hcnJvdyBpdCBkb3duIGlmIHlvdSBrbm93IHlvdXIgZXhhY3Qgc3VibmV0LlxuICAtIFNVQk5FVD0ke1NVQk5FVH1cbiAgLSBBRE1JTj10cnVlXG4gIC0gV0VCTUFJTD1yb3VuZGN1YmVcbiAgLSBBUEk9ZmFsc2VcbiAgLSBXRUJEQVY9bm9uZVxuICAtIEFOVElWSVJVUz1ub25lXG4gIC0gU0NBTl9NQUNST1M9ZmFsc2VcbiAgIyBQb3J0cyBlbmFibGVkIGluc2lkZSB0aGUgZnJvbnQgY29udGFpbmVyLiA0MTkwIChzaWV2ZSkgc3RheXMgaW50ZXJuYWwgZm9yXG4gICMgdGhlIHdlYm1haWwgZmlsdGVycyBVSSBhbmQgaXMgbm90IHB1Ymxpc2hlZCBvbiB0aGUgaG9zdC5cbiAgLSBQT1JUUz0yNSw4MCw0NDMsNDY1LDU4Nyw5OTMsNDE5MFxuICAtIE1FU1NBR0VfU0laRV9MSU1JVD01MDAwMDAwMFxuICAtIEZVTExfVEVYVF9TRUFSQ0g9ZW5cbiAgLSBXRUJST09UX1JFRElSRUNUPS93ZWJtYWlsXG4gIC0gV0VCX0FETUlOPS9hZG1pblxuICAtIFdFQl9XRUJNQUlMPS93ZWJtYWlsXG4gIC0gU0lURU5BTUU9TWFpbHVcbiAgLSBXRUJTSVRFPWh0dHBzOi8vbWFpbHUuaW9cbiAgIyBUcnVzdCBUcmFlZmlrIChkb2twbG95LW5ldHdvcmspIHRvIGZvcndhcmQgdGhlIHJlYWwgY2xpZW50IElQIGZvciByYXRlIGxpbWl0aW5nXG4gIC0gUkVBTF9JUF9GUk9NPSR7U1VCTkVUfVxuICAtIFJFQUxfSVBfSEVBREVSPVgtRm9yd2FyZGVkLUZvclxuICAjIEluaXRpYWwgYWRtaW4gYWNjb3VudDogYWRtaW5AJHtET01BSU59IChjcmVhdGVkIG9ubHkgaWYgaXQgZG9lcyBub3QgZXhpc3QpXG4gIC0gSU5JVElBTF9BRE1JTl9BQ0NPVU5UPSR7SU5JVElBTF9BRE1JTl9BQ0NPVU5UfVxuICAtIElOSVRJQUxfQURNSU5fRE9NQUlOPSR7RE9NQUlOfVxuICAtIElOSVRJQUxfQURNSU5fUFc9JHtJTklUSUFMX0FETUlOX1BXfVxuICAtIElOSVRJQUxfQURNSU5fTU9ERT1pZm1pc3NpbmdcblxuIyBQdWJsaWMgRE5TU0VDLXZhbGlkYXRpbmcgcmVzb2x2ZXJzLiBUaGUgYWRtaW4gY29udGFpbmVyIHJlZnVzZXMgdG8gc3RhcnRcbiMgd2l0aG91dCBETlNTRUMgdmFsaWRhdGlvbiwgYW5kIFBvc3RmaXggbmVlZHMgaXQgZm9yIG91dGJvdW5kIERBTkUuIFNlcnZpY2VcbiMgZGlzY292ZXJ5IHN0aWxsIHVzZXMgRG9ja2VyJ3MgZW1iZWRkZWQgRE5TOyB0aGVzZSBhcmUgb25seSB1cHN0cmVhbXMuXG54LW1haWx1LWRuczogJm1haWx1LWRuc1xuICAtIFwiMS4xLjEuMVwiXG4gIC0gXCI4LjguOC44XCJcblxuc2VydmljZXM6XG4gIGZyb250OlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L25naW54OjIwMjQuMDZcbiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZFxuICAgIGVudmlyb25tZW50OiAqbWFpbHUtZW52XG4gICAgZG5zOiAqbWFpbHUtZG5zXG4gICAgIyBNYWlsIHByb3RvY29sIHBvcnRzIGFyZSBwdWJsaXNoZWQgZGlyZWN0bHkgb24gdGhlIGhvc3QgKHNhbWUgYXBwcm9hY2ggYXNcbiAgICAjIHRoZSBwb3N0ZS5pbyB0ZW1wbGF0ZSkuIFRoZXkgd2lsbCBmYWlsIHRvIGJpbmQgaWYgYW5vdGhlciBtYWlsIHNlcnZlclxuICAgICMgYWxyZWFkeSB1c2VzIHRoZW0gb24gdGhpcyBtYWNoaW5lLlxuICAgIHBvcnRzOlxuICAgICAgLSBcIjI1OjI1XCIgIyBTTVRQIChzZXJ2ZXIgdG8gc2VydmVyKVxuICAgICAgLSBcIjQ2NTo0NjVcIiAjIFNNVFBTIHN1Ym1pc3Npb25cbiAgICAgIC0gXCI1ODc6NTg3XCIgIyBTdWJtaXNzaW9uIChTVEFSVFRMUylcbiAgICAgIC0gXCI5OTM6OTkzXCIgIyBJTUFQU1xuICAgIHZvbHVtZXM6XG4gICAgICAtIG1haWx1LWNlcnRzOi9jZXJ0c1xuICAgICMgVGhlIGltYWdlJ3MgYnVpbHQtaW4gaGVhbHRoY2hlY2sgYWxzbyByZXF1aXJlcyB0aGUgbWFpbCAoRG92ZWNvdCkgcHJveHksXG4gICAgIyB3aGljaCBvbmx5IHN0YXJ0cyBvbmNlIFRMUyBjZXJ0aWZpY2F0ZXMgZXhpc3QuIEJlaGluZCBUcmFlZmlrIHRoYXRcbiAgICAjIGRlYWRsb2NrczogdW5oZWFsdGh5IC0+IFRyYWVmaWsgZHJvcHMgdGhlIHJvdXRlIC0+IHRoZSBBQ01FIGNoYWxsZW5nZSBjYW5cbiAgICAjIG5ldmVyIGJlIGFuc3dlcmVkLiBDaGVjayBvbmx5IG5naW54IGluc3RlYWQgKGhlYWx0aCBlbmRwb2ludCByZXR1cm5zIDIwNCkuXG4gICAgaGVhbHRoY2hlY2s6XG4gICAgICB0ZXN0OiBbXCJDTUQtU0hFTExcIiwgXCJjdXJsIC1tMyAtc2tmTG8gL2Rldi9udWxsIGh0dHA6Ly8xMjcuMC4wLjE6MTAyMDQvaGVhbHRoXCJdXG4gICAgICBpbnRlcnZhbDogMzBzXG4gICAgICB0aW1lb3V0OiA1c1xuICAgICAgcmV0cmllczogM1xuICAgICAgc3RhcnRfcGVyaW9kOiA2MHNcbiAgICBkZXBlbmRzX29uOlxuICAgICAgLSBhZG1pblxuXG4gIGFkbWluOlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L2FkbWluOjIwMjQuMDZcbiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZFxuICAgIGVudmlyb25tZW50OiAqbWFpbHUtZW52XG4gICAgZG5zOiAqbWFpbHUtZG5zXG4gICAgdm9sdW1lczpcbiAgICAgIC0gbWFpbHUtZGF0YTovZGF0YVxuICAgICAgLSBtYWlsdS1ka2ltOi9ka2ltXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gcmVkaXNcblxuICBpbWFwOlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L2RvdmVjb3Q6MjAyNC4wNlxuICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkXG4gICAgZW52aXJvbm1lbnQ6ICptYWlsdS1lbnZcbiAgICBkbnM6ICptYWlsdS1kbnNcbiAgICB2b2x1bWVzOlxuICAgICAgLSBtYWlsdS1tYWlsOi9tYWlsXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gZnJvbnRcblxuICBzbXRwOlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L3Bvc3RmaXg6MjAyNC4wNlxuICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkXG4gICAgZW52aXJvbm1lbnQ6ICptYWlsdS1lbnZcbiAgICBkbnM6ICptYWlsdS1kbnNcbiAgICB2b2x1bWVzOlxuICAgICAgLSBtYWlsdS1tYWlscXVldWU6L3F1ZXVlXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gZnJvbnRcblxuICBhbnRpc3BhbTpcbiAgICBpbWFnZTogZ2hjci5pby9tYWlsdS9yc3BhbWQ6MjAyNC4wNlxuICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkXG4gICAgaG9zdG5hbWU6IGFudGlzcGFtXG4gICAgZW52aXJvbm1lbnQ6ICptYWlsdS1lbnZcbiAgICBkbnM6ICptYWlsdS1kbnNcbiAgICB2b2x1bWVzOlxuICAgICAgLSBtYWlsdS1maWx0ZXI6L3Zhci9saWIvcnNwYW1kXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gZnJvbnRcbiAgICAgIC0gcmVkaXNcblxuICB3ZWJtYWlsOlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L3dlYm1haWw6MjAyNC4wNlxuICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkXG4gICAgZW52aXJvbm1lbnQ6ICptYWlsdS1lbnZcbiAgICBkbnM6ICptYWlsdS1kbnNcbiAgICB2b2x1bWVzOlxuICAgICAgLSBtYWlsdS13ZWJtYWlsOi9kYXRhXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gZnJvbnRcbiAgICAgIC0gaW1hcFxuXG4gIHJlZGlzOlxuICAgIGltYWdlOiByZWRpczphbHBpbmVcbiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZFxuICAgIHZvbHVtZXM6XG4gICAgICAtIG1haWx1LXJlZGlzOi9kYXRhXG5cbnZvbHVtZXM6XG4gIG1haWx1LWNlcnRzOiB7fVxuICBtYWlsdS1kYXRhOiB7fVxuICBtYWlsdS1ka2ltOiB7fVxuICBtYWlsdS1tYWlsOiB7fVxuICBtYWlsdS1tYWlscXVldWU6IHt9XG4gIG1haWx1LWZpbHRlcjoge31cbiAgbWFpbHUtd2VibWFpbDoge31cbiAgbWFpbHUtcmVkaXM6IHt9XG4iLAogICJjb25maWciOiAiW3ZhcmlhYmxlc11cbm1haW5fZG9tYWluID0gXCIke2RvbWFpbn1cIlxuc2VjcmV0X2tleSA9IFwiJHtwYXNzd29yZDoxNn1cIlxuYWRtaW5fcGFzc3dvcmQgPSBcIiR7cGFzc3dvcmQ6MzJ9XCJcblxuW2NvbmZpZ11cbltbY29uZmlnLmRvbWFpbnNdXVxuc2VydmljZU5hbWUgPSBcImZyb250XCJcbnBvcnQgPSA4MFxuaG9zdCA9IFwiJHttYWluX2RvbWFpbn1cIlxuXG5bY29uZmlnLmVudl1cbkRPTUFJTiA9IFwiJHttYWluX2RvbWFpbn1cIlxuU0VDUkVUX0tFWSA9IFwiJHtzZWNyZXRfa2V5fVwiXG5JTklUSUFMX0FETUlOX0FDQ09VTlQgPSBcImFkbWluXCJcbklOSVRJQUxfQURNSU5fUFcgPSBcIiR7YWRtaW5fcGFzc3dvcmR9XCJcblNVQk5FVCA9IFwiMTcyLjE2LjAuMC8xMlwiXG4iCn0=Instructions
Mailu
Getting started
- Point the domain you assign in Dokploy (for example
mail.example.com) at your server before deploying: anArecord, plus anMXrecord for your mail domain targeting it. - Deploy the template and open the domain:
/webmailis Roundcube,/adminis the admin UI. - Log in at
/adminwithadmin@<your domain>and the auto-generatedINITIAL_ADMIN_PW(Dokploy → your service → Environment). The account is created only on first boot (INITIAL_ADMIN_MODE=ifmissing); change the password from the admin UI afterwards. - In the admin UI, open Mail domains → your domain → Details and create the DNS records it shows (SPF, DKIM, DMARC). Also set the PTR/reverse DNS record of your server IP to your mail hostname — most providers require this to accept your mail.
Ports
The mail protocol ports are published directly on the host: 25 (SMTP), 465 (SMTPS), 587 (submission) and 993 (IMAPS). The deployment fails to start if another mail server (or a previous Mailu deployment) already binds them on the same machine. Many VPS providers block outbound port 25 by default — ask your provider to unblock it, or configure a relay host in Mailu.
TLS
- The web UI is served through Traefik like any other Dokploy app. Enable HTTPS with Let's Encrypt on the Dokploy domain: the web login cookie requires HTTPS, and Mailu's internal certbot self-check follows Traefik's HTTP→HTTPS redirect and needs a valid certificate there.
- The mail ports get their own Let's Encrypt certificate: the
frontcontainer runs certbot internally and answers the HTTP-01 challenge through Traefik on port 80. This only succeeds once the DNS record of your domain points at the server. If the certificate was obtained after the first boot, restart thefrontservice once so the TLS mail listeners (465/587/993) come up.
Notes
SUBNET(default172.16.0.0/12) is the network range Mailu trusts for its internal traffic (Postfix relay/XCLIENT, Dovecot proxying, Rspamd). It covers Docker's default address pools; if your Docker daemon uses custom pools, adjust it to match. Note this means other containers on the same Docker host are treated as trusted senders.- The containers use public DNSSEC-validating resolvers (
1.1.1.1,8.8.8.8) as upstream DNS: the admin container requires DNSSEC validation and Postfix uses it for DANE. Heavy production use benefits from a dedicated local resolver instead, because public resolvers are rate-limited by DNSBLs (see the Mailu DNS FAQ). - Additional mail domains, users, aliases and fetchmail can be managed in the admin UI. ClamAV antivirus is not included in this template to keep memory usage low (it needs >1 GB RAM); see the Mailu docs to add it.
Links
Tags
email, mail-server, smtp, imap, antispam, webmail
Version: 2024.06