· Rajasekhar Gundala · Mailserver  · 7 min read

How to selfhost Stalwart mail server using docker compose behind Caddy v2.8.4

Stalwart is an open-source mail server solution with JMAP, IMAP4, POP3 & SMTP support; a wide range of modern features. It is written in Rust and aims to be secure, fast, robust and scalable.

Stalwart is an open-source mail server solution with JMAP, IMAP4, POP3 & SMTP support; a wide range of modern features. It is written in Rust and aims to be secure, fast, robust and scalable.

Stalwart is an open-source mail server solution with JMAP, IMAP4, POP3 & SMTP support; a wide range of modern features. It is written in Rust and aims to be secure, fast, robust and scalable.

Today I am going to show you how to deploy Stalwart mail server using docker compose behind Caddy proxy.

Lets start with the actual deployment…

Prerequisite

Please make sure you should fulfill the below requirements before proceeding to the actual deployment.

  1. Ubuntu latest server (any Linux flavor works but I am using Ubuntu here).
  2. Docker and Docker Compose installed on the Ubuntu Server.

Introduction to Stalwart Mail Server

Stalwart is an open-source mail server solution with JMAP, IMAP4, POP3 & SMTP support; a wide range of modern features. It is written in Rust and aims to be secure, fast, robust and scalable.

Unlike any other solution on the market, Stalwart redefines the email server landscape. Traditionally, operating a mail server required piecing together multiple components—such as MTA, message store, and spam filter—each developed over 20 years ago with their own configuration formats.

Stalwart streamlines this process into a single, efficient binary with a unified configuration. Furthermore, it is crafted in Rust, a language celebrated for its memory safety, ensuring a robust and secure email infrastructure.

Secure & Modern All-in-One Mail Server (IMAP, JMAP, POP3, SMTP).

Stalwart Mail Server Core Features

Stalwart is an integrated email infrastructure for secure and efficient messaging. It has the following key features.

JMAP Server

  • JMAP Core and JMAP Mail full compliance.
  • JMAP for Sieve Scripts extension for managing Sieve scripts.
  • JMAP for WebSocket, JMAP Blob Management and JMAP for Quotas extensions.

IMAP4, POP3 and ManageSieve Server

  • IMAP4rev2 and IMAP4rev1 server with support for numerous extensions.
  • POP3 server with extensions, STLS and SASL support.
  • ManageSieve server for managing Sieve scripts.

SMTP Server

  • Built-in DMARC, DKIM, SPF and ARC support for message authentication.
  • Strong transport security through DANE, MTA-STS and SMTP TLS reporting.
  • Inbound throttling and filtering with granular configuration rules, sieve scripting, MTA hooks and milter integration.
  • Distributed virtual queues with delayed delivery, priority delivery, quotas, routing rules and throttling support.
  • Envelope rewriting and message modification.

Spam Phishing Filter

  • Comprehensive set of filtering rules on par with popular solutions.
  • Statistical spam classifier with automatic training capabilities.
  • DNS Blocklists (DNSBLs) checking of IP addresses, domains, and hashes.
  • Collaborative digest-based spam filtering with Pyzor.
  • Phishing protection against homographic URL attacks, sender spoofing and other techniques.
  • Trusted reply tracking to recognize and prioritize genuine e-mail replies.
  • Sender reputation monitoring by IP address, ASN, domain and email address.
  • Greylisting to temporarily defer unknown senders.
  • Spam traps to set up decoy email addresses that catch and analyze spam.

Flexible and Scalable

  • Pluggable storage backends with RocksDB, FoundationDB, PostgreSQL, mySQL, SQLite, S3-Compatible, Redis and ElasticSearch support.
  • Clustering support with node autodiscovery and partition-tolerant failure detection.
  • Built-in, LDAP or SQL authentication backend support.
  • Full-text search available in 17 languages.
  • Sieve scripting language with support for all registered extensions.
  • Email aliases, mailing lists, subaddressing and catch-all addresses support.
  • Automatic account configuration and discovery with autoconfig and autodiscover.
  • Integration with OpenTelemetry to enable monitoring, tracing, and performance analysis.
  • Webhooks for event-driven automation.
  • Disk quotas.

Web-based Administration

  • Account, domain, group and mailing list management.
  • SMTP queue management for messages and outbound DMARC and TLS reports.
  • Report visualization interface for received DMARC, TLS-RPT and Failure (ARF) reports.
  • Configuration of every aspect of the mail server.
  • Log viewer with search and filtering capabilities.
  • Self-service portal for password reset and encryption-at-rest key management.

Secure and Robust

  • Encryption at rest with S/MIME or OpenPGP.
  • Automatic TLS certificate provisioning with ACME using TLS-ALPN-01, DNS-01 or HTTP-01 challenges.
  • OAuth 2.0 authorization code and device authorization flows.
  • Two-factor authentication with Time-based One-Time Passwords.
  • Application passwords (App Passwords).
  • Automated blocking of hosts that cause multiple authentication errors.
  • Access Control Lists.
  • Rate limiting.
  • Security audited.
  • Memory safe.

Prepare Stalwart Mail Server Environment

To proceed further make sure that the port 25 is open on the server

Your ISP or hosting provider won’t block incoming connection to port 25, means you can receive emails from other mail servers. However, many ISP/hosting providers block outgoing connection to port 25 of other mail servers, means you can’t send emails.

Run the following command on your server where you are going to deploy Stalwart Mail Server to check if port 25 (outbound) is blocked.

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

You would see below message, which indicates a connection is successfully established.

Trying your-server-ip-address...
Connected to gmail-smtp-in.l.google.com.
Escape character is '^]'.
220 mx.google.com ESMTP 7fbef4ecsi44cewcfewdfew.135 - gsmtp

Type quit and press Enter to close the connection.

Proceed further after confirming the port 25 is not blocked

Let’s start creating a local folder stalwart to persist Stalwart docker container data, /opt/stalwart-mail for disaster recovery purpose.

Create folder in /opt directory to persistent Stalwart data folder, /opt/stalwart-mail

cd /opt
sudo mkdir -p stalwart

Caddyfile – Stalwart Mail Server

The Caddyfile is a convenient Caddy configuration format for humans.

Caddyfile is easy to write, easy to understand, and expressive enough for most use cases.

Production-ready Caddyfile for Jekyll as Static File Server. You can use it for other static site generators as well, like Hugo.

Learn more about Caddyfile here to get familiar with it.

Use the below commands to create the Caddy configuration file,

cd /opt
sudo touch Caddyfile

Open Caddyfile with nano editor using sudo nano Caddyfile

Copy and paste the below code in the Caddyfile configuration file.

{
    email you@example.com
    default_sni stalwart
    cert_issuer acme
    # Production acme directory
    acme_ca https://acme-v02.api.letsencrypt.org/directory
    # Staging acme directory
    #acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
    servers {
        metrics
        protocol h1 h2c h3
        strict_sni_host on
        trusted_proxies cloudflare {
            interval 12h
            timeout 15s  
        }
    }
    admin 0.0.0.0:2019
}

mail.example.com {
    log {
        output file /var/log/caddy/jekyll.log {
            roll_size 20mb
            roll_keep 2
            roll_keep_for 6h
        }
        format console
        level error
    }

    reverse_proxy stalwart:8080 {
        transport http {
            keepalive 300s
        }
    }
}

Please go to Caddy Post to get more insight to deploy it in the docker swarm cluster.

Prepare Docker Compose File

Now it’s time to create a docker compose file that contains the Caddy Reverse Proxy service and Stalwart service in /opt directory. docker compose is the configuration file in .yml (Yet Another Markup Language).

Create a docker network caddy, we will use it to expose application containers or internal services to outside world.

docker network create caddy

Use the below commands to create the configuration file,

cd /opt
sudo touch docker-compose.yml

Open docker-compose.yml with nano editor using sudo nano docker-compose.yml

Copy and paste the below code in the docker-compose.yml configuration file.

Here is the full docker-compose file.

services:
  caddy:
    image: rajaseg/caddy
    restart: unless-stopped
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
      - "2019:2019"
    networks:
      - caddy
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./caddydata:/data
      - ./caddyconfig:/config
      - ./caddylogs:/var/log/caddy
      - ./stalwart:/opt/stalwart-mail
  stalwart:
    image: stalwartlabs/mail-server:latest
    container_name: stalwart
    restart: unless-stopped
    depends_on:
      caddy:
        condition: service_started
    volumes:
      - ./stalwart:/opt/stalwart-mail
    ports:
      - "4190:4190"
      - "993:993"
      - "143:143"
      - "465:465"
      - "587:587"
      - "25:25"
      - "8443:443"
    networks:
      - caddy
    stdin_open: true
    tty: true
volumes:
  caddydata:
  caddyconfig:
  caddylogs:
  stalwart:
  beszel:
networks:
  caddy:
    external: true

Lets discuss about some of the configuration options above.

depends_on

depends_on expresses startup and shutdown dependencies between services.

short syntax

The short syntax variant only specifies service names of the dependencies. Service dependencies cause the following behaviors:

  • Compose creates services in dependency order. In the above scenario, caddy is created before stalwart.
  • Compose removes services in dependency order. In the above scenario, stalwart is removed before caddy.
  • restart_policy is to configure whether and how to restart containers when they exit. condition: One of none, on-failure or any. Default is any*.*

Compose guarantees dependency services have been started before starting a dependent service. Compose waits for dependency services to be ready before starting a dependent service.

Deploy the Docker Compose

Now it’s time to deploy our docker-compose file above using the below command

docker compose up -d

Make sure that you have DNS entry for your application (mail.example.com) in your DNS Management Application.

Log in to Stalwart Mail Server Web Interface

In order to obtain the administrator account and password details execute the below command.

docker logs stalwart-container-id

Type docker ps and press Enter to get the container id's of the running services.

After getting the Stalwart container id, use below command to get the log in details

docker logs stalwart-container-id
 Configuration file written to /opt/stalwart-mail/etc/config.toml
🔑 Your administrator account is 'admin' with password 'password'.

With this information, you can log in to the web interface at mail.example.com.

Make sure that you have DNS entry for your application (wordpress.example.com) in your DNS Management Application.

Please find below images for your reference.

Below screen shots are for reference purpose.

Stalwart Web Interface Stalwart Dashboard

I hope you enjoyed this post. Please share your thoughts or feedback on it by commenting.

Stay tuned for other deployments 🙂

Back to Blog