Infrastructure

How to Self-Host Maddy Mail Server Using Docker Compose Behind Caddy

Maddy is a modern, all-in-one open-source mail server written in Go. Learn how to deploy Maddy with Docker Compose, leverage Caddy for automatic SSL certificates, and configure DKIM, SPF, and DMARC.

Rajasekhar Gundala··8 min read

Operating an email server has historically required orchestrating multiple decoupled utilities: Postfix for message transfer (MTA), Dovecot for IMAP storage (MDA), OpenDKIM for signing, and OpenDMARC for policy enforcement. Each service relies on legacy configuration semantics, distinct log streams, and complex inter-process socket communication.

Maddy Mail Server consolidates the entire mail pipeline into a single, composable binary written in Go. It natively manages SMTP, IMAP4, DKIM signing, SPF verification, and DMARC enforcement using an embedded SQLite database by default.

This guide walks through deploying Maddy in Docker behind Caddy, configuring automatic TLS certificate synchronization, and provisioning user mailboxes.


Prerequisites

Ensure the host environment meets the following conditions before proceeding:

  1. A cloud server running Ubuntu 22.04/24.04 LTS (or Debian 12) with a dedicated public IPv4 address.
  2. Docker Engine and Docker Compose v2 installed.
  3. A domain name with access to edit DNS records (e.g., example.com).
  4. An assigned Reverse DNS (PTR record) configured at your hosting provider pointing your IP to mail.yourdomain.com.

Core Architecture & Features

Maddy’s internal pipeline processes inbound and outbound mail through modular message pipelines defined in a single configuration file (maddy.conf).

All-in-One Protocol Support
  • SMTP & Submission: Inbound MX (port 25), Submission with STARTTLS (port 587), and Implicit TLS Submission (port 465).
  • IMAP4rev1 Storage: Built-in IMAP server supporting STARTTLS (port 143) and Implicit TLS (port 993).
  • Zero External Dependencies: Uses SQLite for credential storage, mail indexing, and delivery queues without requiring separate database containers.
Native Authentication & Delivery Controls
  • Automated DKIM: Generates RSA/Ed25519 cryptographic keys on first boot and automatically signs outgoing messages.
  • SPF Verification: Evaluates inbound sender IP authorizations against published domain policies.
  • DMARC Enforcement: Built-in parser checks alignment and applies none, quarantine, or reject policies directly at the SMTP transaction stage.
Security & Isolation
  • Runs as an unprivileged system user (maddy:maddy, UID 1000) inside containerized namespaces.
  • Supports strict TLS configurations, ALPN challenge routing, and modern cipher suites.

Preparing the Deployment Environment

Step 1: Verify Inbound and Outbound Port 25

Residential ISPs and several cloud hosts (such as AWS, DigitalOcean, and Linode) block outbound traffic on TCP port 25 to mitigate spam abuse.

Run this check from your server CLI:

telnet gmail-smtp-in.l.google.com 25

If the connection is allowed, the remote server returns an SMTP handshake banner:

Trying 142.251.163.26...
Connected to gmail-smtp-in.l.google.com.
Escape character is '^]'.
220 mx.google.com ESMTP

Type QUIT to disconnect. If the connection stalls or drops with Connection timed out, request an unblock on outbound port 25 through your host provider’s support ticket system.

Step 2: Establish the Directory Layout

Create persistent directories to preserve database files, DKIM keys, and Caddy’s certificates across container restarts:

sudo mkdir -p /opt/maddy/{maddydata,caddydata,caddyconfig}
cd /opt/maddy

Required DNS Records

Before initiating certificate challenges, publish the necessary DNS records through your registrar or DNS provider (e.g., Cloudflare, Route53):

Type Name Value Priority / TTL
A mail YOUR_SERVER_IPV4 Auto / 300s
MX @ mail.yourdomain.com. Priority 10
TXT @ v=spf1 mx ~all Auto / 300s
TXT _dmarc v=DMARC1; p=quarantine; adkim=r; aspf=r; Auto / 300s

If you route web traffic through Cloudflare, set the Proxy status for the mail A-record to DNS Only (Grey Cloud). Cloudflare’s HTTP proxy does not proxy raw SMTP/IMAP traffic on standard mail ports.


Caddy Configuration

Caddy handles ACME HTTP-01 challenges and provisions wildcard or dedicated Let’s Encrypt certificates. By mounting Caddy’s data storage into the Maddy container, Maddy reads valid certificates directly without duplicate renewal scripts or certbot hooks.

Create /opt/maddy/Caddyfile:

sudo nano Caddyfile

Paste the following configuration, replacing the email and domain with your own:

{
    email admin@yourdomain.com
}

mail.yourdomain.com {
    # Handles HTTP-01 ACME challenge verification
    respond "Maddy Mail Endpoint Active" 200
}

Docker Compose Stack

Maddy publishes standard mail ports directly on the host interface, while mounting Caddy’s storage volume as read-only.

Create /opt/maddy/docker-compose.yml:

sudo nano docker-compose.yml

Paste the following stack configuration:

version: "3.8"

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - ./caddyconfig:/config

  maddy:
    image: foxcpp/maddy:latest
    container_name: maddy
    restart: unless-stopped
    depends_on:
      - caddy
    ports:
      - "25:25"       # SMTP Inbound Transfer
      - "143:143"     # IMAP (STARTTLS)
      - "465:465"     # SMTPS (Implicit TLS Submission)
      - "587:587"     # Submission (STARTTLS)
      - "993:993"     # IMAPS (Implicit TLS)
    environment:
      - MADDY_HOSTNAME=mail.yourdomain.com
      - MADDY_DOMAIN=yourdomain.com
    volumes:
      - ./maddydata:/data
      - caddy_data:/caddy_certs:ro

volumes:
  caddy_data:

Configuring Maddy

Start Caddy once to generate certificates and allow Maddy to write its baseline configuration:

docker compose up -d

Check Caddy logs to verify the certificate issued successfully:

docker compose logs caddy

Stop the stack to adjust maddy.conf:

docker compose down

Open /opt/maddy/maddydata/maddy.conf in your editor. Locate the primary tls directive and update the certificate paths to point to Caddy’s shared volume mount:

$(primary_domain) = yourdomain.com
$(local_domains) = $(primary_domain)
$(hostname) = mail.yourdomain.com

tls file /caddy_certs/caddy/certificates/acme-v02.api.letsencrypt.org-directory/[mail.yourdomain.com/mail.yourdomain.com.crt](https://mail.yourdomain.com/mail.yourdomain.com.crt) /caddy_certs/caddy/certificates/acme-v02.api.letsencrypt.org-directory/[mail.yourdomain.com/mail.yourdomain.com.key](https://mail.yourdomain.com/mail.yourdomain.com.key) {
    protocols tls1.2 tls1.3
}

Relaunch the entire mail stack:

docker compose up -d

Publishing the DKIM Record

During initial startup, Maddy automatically generates an RSA DKIM key located in /opt/maddy/maddydata/dkim_keys/.

Display the generated public DNS record:

cat /opt/maddy/maddydata/dkim_keys/yourdomain.com_default.dns

The output provides the TXT record format required by your DNS manager:

v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0r1...

Add this as a TXT record in your DNS console:

  • Name: default._domainkey
  • Value: Paste the full string output by Maddy.

Mailbox Provisioning

Account management is executed directly via Maddy’s CLI within the running container.

Step 1: Create Account Authentication Credentials
docker exec -it maddy maddy creds create admin@yourdomain.com

Prompt: Enter a secure password and confirm.

Step 2: Initialize IMAP Mailbox Storage
docker exec -it maddy maddy imap-acct create admin@yourdomain.com
Step 3: Verify Provisioned Credentials
docker exec -it maddy maddy creds list

Client Configuration Details

Use the following parameters to connect desktop and mobile mail clients (Thunderbird, Apple Mail, Outlook):

Incoming Mail (IMAP)
  • Server: mail.yourdomain.com
  • Port: 993
  • Security: SSL/TLS (Implicit TLS)
  • Username: admin@yourdomain.com
  • Authentication: Normal Password
Outgoing Mail (SMTP)
  • Server: mail.yourdomain.com
  • Port: 465 (Implicit TLS) or 587 (STARTTLS)
  • Security: SSL/TLS or STARTTLS
  • Username: admin@yourdomain.com
  • Authentication: Normal Password

Verification and Delivery Health

Verify inbound and outbound delivery health by sending a test message to mail-tester.com.

Inspect active mail transaction logs in real time:

docker compose logs -f maddy

A properly configured server will report passing checks across SPF, DKIM, and DMARC alignment, establishing high deliverability for outbound emails.

Share
Written by
Rajasekhar Gundala

Senior Infrastructure & Web Platform Leader.

Continue reading

Weekly Engineering Notes.

A weekly digest on infrastructure, observability, Rust, and the open web. No spam, just technical signals.

Free. Unsubscribe in one click.