Dokploy

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.

Mailu logo

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 → AdvancedBase64 import and paste the content below:

ewogICJjb21wb3NlIjogInZlcnNpb246IFwiMy44XCJcblxuIyBTaGFyZWQgTWFpbHUgY29uZmlndXJhdGlvbiAoZXF1aXZhbGVudCB0byB0aGUgdXBzdHJlYW0gbWFpbHUuZW52IGZpbGUpXG54LW1haWx1LWVudjogJm1haWx1LWVudlxuICAtIFNFQ1JFVF9LRVk9JHtTRUNSRVRfS0VZfVxuICAtIERPTUFJTj0ke0RPTUFJTn1cbiAgLSBIT1NUTkFNRVM9JHtET01BSU59XG4gIC0gUE9TVE1BU1RFUj1hZG1pblxuICAjIFRMUyBmb3IgdGhlIG1haWwgcHJvdG9jb2xzIGlzIG9idGFpbmVkIHRocm91Z2ggTGV0J3MgRW5jcnlwdCAoSFRUUC0wMVxuICAjIHRocm91Z2ggVHJhZWZpayBvbiBwb3J0IDgwKS4gVGhlIHdlYiBVSSBpdHNlbGYgaXMgc2VydmVkIHBsYWluIEhUVFAgdG9cbiAgIyBUcmFlZmlrLCB3aGljaCB0ZXJtaW5hdGVzIEhUVFBTLlxuICAtIFRMU19GTEFWT1I9bWFpbC1sZXRzZW5jcnlwdFxuICAjIE1haWx1IGdyYW50cyByZWxheS9YQ0xJRU5UIHRydXN0IHRvIHRoaXMgbmV0d29yayByYW5nZS4gSXQgbXVzdCBjb3ZlciB0aGVcbiAgIyBEb2NrZXIgbmV0d29ya3Mgb2YgdGhpcyBwcm9qZWN0IChEb2NrZXIgYWxsb2NhdGVzIHRoZW0gZnJvbSAxNzIuMTYuMC4wLzEyXG4gICMgYnkgZGVmYXVsdCkuIE5hcnJvdyBpdCBkb3duIGlmIHlvdSBrbm93IHlvdXIgZXhhY3Qgc3VibmV0LlxuICAtIFNVQk5FVD0ke1NVQk5FVH1cbiAgLSBBRE1JTj10cnVlXG4gIC0gV0VCTUFJTD1yb3VuZGN1YmVcbiAgLSBBUEk9ZmFsc2VcbiAgLSBXRUJEQVY9bm9uZVxuICAtIEFOVElWSVJVUz1ub25lXG4gIC0gU0NBTl9NQUNST1M9ZmFsc2VcbiAgIyBQb3J0cyBlbmFibGVkIGluc2lkZSB0aGUgZnJvbnQgY29udGFpbmVyLiA0MTkwIChzaWV2ZSkgc3RheXMgaW50ZXJuYWwgZm9yXG4gICMgdGhlIHdlYm1haWwgZmlsdGVycyBVSSBhbmQgaXMgbm90IHB1Ymxpc2hlZCBvbiB0aGUgaG9zdC5cbiAgLSBQT1JUUz0yNSw4MCw0NDMsNDY1LDU4Nyw5OTMsNDE5MFxuICAtIE1FU1NBR0VfU0laRV9MSU1JVD01MDAwMDAwMFxuICAtIEZVTExfVEVYVF9TRUFSQ0g9ZW5cbiAgLSBXRUJST09UX1JFRElSRUNUPS93ZWJtYWlsXG4gIC0gV0VCX0FETUlOPS9hZG1pblxuICAtIFdFQl9XRUJNQUlMPS93ZWJtYWlsXG4gIC0gU0lURU5BTUU9TWFpbHVcbiAgLSBXRUJTSVRFPWh0dHBzOi8vbWFpbHUuaW9cbiAgIyBUcnVzdCBUcmFlZmlrIChkb2twbG95LW5ldHdvcmspIHRvIGZvcndhcmQgdGhlIHJlYWwgY2xpZW50IElQIGZvciByYXRlIGxpbWl0aW5nXG4gIC0gUkVBTF9JUF9GUk9NPSR7U1VCTkVUfVxuICAtIFJFQUxfSVBfSEVBREVSPVgtRm9yd2FyZGVkLUZvclxuICAjIEluaXRpYWwgYWRtaW4gYWNjb3VudDogYWRtaW5AJHtET01BSU59IChjcmVhdGVkIG9ubHkgaWYgaXQgZG9lcyBub3QgZXhpc3QpXG4gIC0gSU5JVElBTF9BRE1JTl9BQ0NPVU5UPSR7SU5JVElBTF9BRE1JTl9BQ0NPVU5UfVxuICAtIElOSVRJQUxfQURNSU5fRE9NQUlOPSR7RE9NQUlOfVxuICAtIElOSVRJQUxfQURNSU5fUFc9JHtJTklUSUFMX0FETUlOX1BXfVxuICAtIElOSVRJQUxfQURNSU5fTU9ERT1pZm1pc3NpbmdcblxuIyBQdWJsaWMgRE5TU0VDLXZhbGlkYXRpbmcgcmVzb2x2ZXJzLiBUaGUgYWRtaW4gY29udGFpbmVyIHJlZnVzZXMgdG8gc3RhcnRcbiMgd2l0aG91dCBETlNTRUMgdmFsaWRhdGlvbiwgYW5kIFBvc3RmaXggbmVlZHMgaXQgZm9yIG91dGJvdW5kIERBTkUuIFNlcnZpY2VcbiMgZGlzY292ZXJ5IHN0aWxsIHVzZXMgRG9ja2VyJ3MgZW1iZWRkZWQgRE5TOyB0aGVzZSBhcmUgb25seSB1cHN0cmVhbXMuXG54LW1haWx1LWRuczogJm1haWx1LWRuc1xuICAtIFwiMS4xLjEuMVwiXG4gIC0gXCI4LjguOC44XCJcblxuc2VydmljZXM6XG4gIGZyb250OlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L25naW54OjIwMjQuMDZcbiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZFxuICAgIGVudmlyb25tZW50OiAqbWFpbHUtZW52XG4gICAgZG5zOiAqbWFpbHUtZG5zXG4gICAgIyBNYWlsIHByb3RvY29sIHBvcnRzIGFyZSBwdWJsaXNoZWQgZGlyZWN0bHkgb24gdGhlIGhvc3QgKHNhbWUgYXBwcm9hY2ggYXNcbiAgICAjIHRoZSBwb3N0ZS5pbyB0ZW1wbGF0ZSkuIFRoZXkgd2lsbCBmYWlsIHRvIGJpbmQgaWYgYW5vdGhlciBtYWlsIHNlcnZlclxuICAgICMgYWxyZWFkeSB1c2VzIHRoZW0gb24gdGhpcyBtYWNoaW5lLlxuICAgIHBvcnRzOlxuICAgICAgLSBcIjI1OjI1XCIgIyBTTVRQIChzZXJ2ZXIgdG8gc2VydmVyKVxuICAgICAgLSBcIjQ2NTo0NjVcIiAjIFNNVFBTIHN1Ym1pc3Npb25cbiAgICAgIC0gXCI1ODc6NTg3XCIgIyBTdWJtaXNzaW9uIChTVEFSVFRMUylcbiAgICAgIC0gXCI5OTM6OTkzXCIgIyBJTUFQU1xuICAgIHZvbHVtZXM6XG4gICAgICAtIG1haWx1LWNlcnRzOi9jZXJ0c1xuICAgICMgVGhlIGltYWdlJ3MgYnVpbHQtaW4gaGVhbHRoY2hlY2sgYWxzbyByZXF1aXJlcyB0aGUgbWFpbCAoRG92ZWNvdCkgcHJveHksXG4gICAgIyB3aGljaCBvbmx5IHN0YXJ0cyBvbmNlIFRMUyBjZXJ0aWZpY2F0ZXMgZXhpc3QuIEJlaGluZCBUcmFlZmlrIHRoYXRcbiAgICAjIGRlYWRsb2NrczogdW5oZWFsdGh5IC0+IFRyYWVmaWsgZHJvcHMgdGhlIHJvdXRlIC0+IHRoZSBBQ01FIGNoYWxsZW5nZSBjYW5cbiAgICAjIG5ldmVyIGJlIGFuc3dlcmVkLiBDaGVjayBvbmx5IG5naW54IGluc3RlYWQgKGhlYWx0aCBlbmRwb2ludCByZXR1cm5zIDIwNCkuXG4gICAgaGVhbHRoY2hlY2s6XG4gICAgICB0ZXN0OiBbXCJDTUQtU0hFTExcIiwgXCJjdXJsIC1tMyAtc2tmTG8gL2Rldi9udWxsIGh0dHA6Ly8xMjcuMC4wLjE6MTAyMDQvaGVhbHRoXCJdXG4gICAgICBpbnRlcnZhbDogMzBzXG4gICAgICB0aW1lb3V0OiA1c1xuICAgICAgcmV0cmllczogM1xuICAgICAgc3RhcnRfcGVyaW9kOiA2MHNcbiAgICBkZXBlbmRzX29uOlxuICAgICAgLSBhZG1pblxuXG4gIGFkbWluOlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L2FkbWluOjIwMjQuMDZcbiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZFxuICAgIGVudmlyb25tZW50OiAqbWFpbHUtZW52XG4gICAgZG5zOiAqbWFpbHUtZG5zXG4gICAgdm9sdW1lczpcbiAgICAgIC0gbWFpbHUtZGF0YTovZGF0YVxuICAgICAgLSBtYWlsdS1ka2ltOi9ka2ltXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gcmVkaXNcblxuICBpbWFwOlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L2RvdmVjb3Q6MjAyNC4wNlxuICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkXG4gICAgZW52aXJvbm1lbnQ6ICptYWlsdS1lbnZcbiAgICBkbnM6ICptYWlsdS1kbnNcbiAgICB2b2x1bWVzOlxuICAgICAgLSBtYWlsdS1tYWlsOi9tYWlsXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gZnJvbnRcblxuICBzbXRwOlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L3Bvc3RmaXg6MjAyNC4wNlxuICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkXG4gICAgZW52aXJvbm1lbnQ6ICptYWlsdS1lbnZcbiAgICBkbnM6ICptYWlsdS1kbnNcbiAgICB2b2x1bWVzOlxuICAgICAgLSBtYWlsdS1tYWlscXVldWU6L3F1ZXVlXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gZnJvbnRcblxuICBhbnRpc3BhbTpcbiAgICBpbWFnZTogZ2hjci5pby9tYWlsdS9yc3BhbWQ6MjAyNC4wNlxuICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkXG4gICAgaG9zdG5hbWU6IGFudGlzcGFtXG4gICAgZW52aXJvbm1lbnQ6ICptYWlsdS1lbnZcbiAgICBkbnM6ICptYWlsdS1kbnNcbiAgICB2b2x1bWVzOlxuICAgICAgLSBtYWlsdS1maWx0ZXI6L3Zhci9saWIvcnNwYW1kXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gZnJvbnRcbiAgICAgIC0gcmVkaXNcblxuICB3ZWJtYWlsOlxuICAgIGltYWdlOiBnaGNyLmlvL21haWx1L3dlYm1haWw6MjAyNC4wNlxuICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkXG4gICAgZW52aXJvbm1lbnQ6ICptYWlsdS1lbnZcbiAgICBkbnM6ICptYWlsdS1kbnNcbiAgICB2b2x1bWVzOlxuICAgICAgLSBtYWlsdS13ZWJtYWlsOi9kYXRhXG4gICAgZGVwZW5kc19vbjpcbiAgICAgIC0gZnJvbnRcbiAgICAgIC0gaW1hcFxuXG4gIHJlZGlzOlxuICAgIGltYWdlOiByZWRpczphbHBpbmVcbiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZFxuICAgIHZvbHVtZXM6XG4gICAgICAtIG1haWx1LXJlZGlzOi9kYXRhXG5cbnZvbHVtZXM6XG4gIG1haWx1LWNlcnRzOiB7fVxuICBtYWlsdS1kYXRhOiB7fVxuICBtYWlsdS1ka2ltOiB7fVxuICBtYWlsdS1tYWlsOiB7fVxuICBtYWlsdS1tYWlscXVldWU6IHt9XG4gIG1haWx1LWZpbHRlcjoge31cbiAgbWFpbHUtd2VibWFpbDoge31cbiAgbWFpbHUtcmVkaXM6IHt9XG4iLAogICJjb25maWciOiAiW3ZhcmlhYmxlc11cbm1haW5fZG9tYWluID0gXCIke2RvbWFpbn1cIlxuc2VjcmV0X2tleSA9IFwiJHtwYXNzd29yZDoxNn1cIlxuYWRtaW5fcGFzc3dvcmQgPSBcIiR7cGFzc3dvcmQ6MzJ9XCJcblxuW2NvbmZpZ11cbltbY29uZmlnLmRvbWFpbnNdXVxuc2VydmljZU5hbWUgPSBcImZyb250XCJcbnBvcnQgPSA4MFxuaG9zdCA9IFwiJHttYWluX2RvbWFpbn1cIlxuXG5bY29uZmlnLmVudl1cbkRPTUFJTiA9IFwiJHttYWluX2RvbWFpbn1cIlxuU0VDUkVUX0tFWSA9IFwiJHtzZWNyZXRfa2V5fVwiXG5JTklUSUFMX0FETUlOX0FDQ09VTlQgPSBcImFkbWluXCJcbklOSVRJQUxfQURNSU5fUFcgPSBcIiR7YWRtaW5fcGFzc3dvcmR9XCJcblNVQk5FVCA9IFwiMTcyLjE2LjAuMC8xMlwiXG4iCn0=

Instructions

Mailu

Getting started

  1. Point the domain you assign in Dokploy (for example mail.example.com) at your server before deploying: an A record, plus an MX record for your mail domain targeting it.
  2. Deploy the template and open the domain: /webmail is Roundcube, /admin is the admin UI.
  3. Log in at /admin with admin@<your domain> and the auto-generated INITIAL_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.
  4. 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 front container 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 the front service once so the TLS mail listeners (465/587/993) come up.

Notes

  • SUBNET (default 172.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.

Tags

email, mail-server, smtp, imap, antispam, webmail


Version: 2024.06

On this page