Skip to content

IPv6

Purpose. Let clients and other mail servers reach this server over IPv6 as well as IPv4, with the same trust, ban and relay rules. It is off by default: an install publishes IPv4 only until you switch IPv6 on.

Behaviour.

  • Switched on per server, with IPV6_ENABLED=true in .env — written by mailctl install --ipv6, or set by hand followed by mailctl upgrade — with mailctl itself at 0.8.0 or later first (npm i -g @maildock/mailctl@<tag>), because an upgrade writes the compose files bundled with the installed CLI, and an older one has no compose.ipv6.yaml. Every port the server publishes on IPv4 (25, 465, 587, 143, 993, 110, 995, 4190, 80, 443, 8081) is then published on IPv6 too, on PUBLISH_BIND6 (default ::, every IPv6 address the host has).
  • Clients keep their own address. Docker forwards IPv6 to the containers by address translation, so Postfix, Dovecot and the control plane see each client’s real IPv6 address: authentication bans, rate limits and relay rules apply to it exactly as to IPv4. A ban on an IPv6 client covers its whole /64 (Login rate limiting & brute-force lockout).
  • Outbound stays IPv4 unless you turn it on. The server receives mail over IPv6 and serves IMAP, POP3, ManageSieve, submission and the web apps on it, but sends outbound mail over IPv4 until outboundIpv6 is set to true in the server configuration (PUT /api/v1/server/config; Settings has no switch for it yet). Then Postfix delivers over IPv6 to destinations that offer it.
  • Turning outbound IPv6 on is refused unless it can work: IPv6 must be on, PUBLIC_IP6 known, and that address must have a PTR naming the mail hostname whose AAAA points back at it — the forward-confirmed reverse DNS the large receivers require of an IPv6 sender (400 outbound_ipv6_refused, naming what is missing). Keeping it on is never refused; mailctl doctor reports a PTR that breaks later.
  • Switching it on or off recreates every container once, on the next mailctl upgrade. Mail queued in Postfix is kept (it lives on its own volume).

Configuration. .env: IPV6_ENABLED (false), PUBLISH_BIND6 (not written; Compose defaults it to ::), PUBLIC_IP6 (the host’s IPv6 address, detected). Requirements:

  • Docker 27 or later. Older engines carry IPv6 through a proxy that hides every client behind one internal address, which the server would trust as itself; mailctl install --ipv6 refuses them, and so does mailctl upgrade once .env has IPv6 on.
  • A routable IPv6 address on the host.
  • mailctl doctor reading ✔ on its ipv6 row before anything else: it confirms every published port answers on IPv6 and that Docker forwards :25 by address translation. A ✖ there means clients would arrive under an internal address — switch IPv6 off until it is fixed.

Then DNS. Once doctor is ✔, publish an AAAA record for the mail hostname (and for postmaster. and webmail. of the server’s domain if you use them). Certificates keep renewing, because ports 80 and 443 now answer on IPv6. The domain page’s DNS & routing section lists the AAAA record and the IPv6 PTR while IPv6 is on, from PUBLIC_IP6 in .env (detected at install, or by mailctl upgrade when you switch IPv6 on), and verifies them like the others; mailctl doctor checks them too.

Limits & edge cases.

  • tls.mode: none counts the IPv6 address as public. With IPv6 on, the mail ports are published on PUBLISH_BIND6 too, so binding IPv4 to 127.0.0.1 no longer makes plaintext logins acceptable without the explicit escape; set PUBLISH_BIND6=::1 as well for a loopback-only server.

  • PUBLIC_IP still holds one address; while IPv6 is on, blocklist and reverse-DNS checks also look at PUBLIC_IP6 (Blocklist, reverse-DNS and certificate monitoring (alerts)).

  • Do not publish an AAAA record while IPv6 is off. Senders and certificate authorities that prefer IPv6 would find nothing listening.

  • The server’s own containers use a fixed private IPv6 range, fd00:6d61:696c::/64. Do not use it for anything else on the host.

  • trustedNetworks accepts IPv6 networks (Relaying & trusted networks).