Tech

Self-Host Your Own Email Server (Stalwart with Roundcube)

Introduction

In this article, I share my journey learning to self-host email server. Having a self-hosted mail server is really good if you want complete control & customization over your data, privacy & cost. However, having to create one require alot of technical expertise & maintenance to ensure its secure & reliable.


PART 1 : Choosing an open-source mail server

There is a lot of open-source mail out there. I do quick research to see what best for me and found out few that i interested:

  • Mail-in-a-Box (https://github.com/mail-in-a-box/mailinabox): turnkey mail suite with Postfix, Dovecot, SpamAssassin, Roundcube, DNS automation, Let’s Encrypt, monitoring. Drawback: not compatible with Ubuntu 24.04.
  • Mailcow:dockerized (https://github.com/mailcow/mailcow-dockerized): a full Docker stack with Postfix, Dovecot, Rspamd, ClamAV, MariaDB, Roundcube/SOGo, polished UI. Drawback: heavyweight, higher resource needs.
  • Mox (https://github.com/mjl-/mox): a single-binary Go mail server with SMTP, IMAP4, JMAP, automatic TLS, basic filtering. Drawback: sparse documentation, smaller community.
  • Stalwart (https://github.com/stalwartlabs/stalwart): modern Rust-based mail & collaboration server offering SMTP, IMAP, JMAP, WebDAV (calendars/contacts), auto-TLS, spam defenses, built-in admin UI. Advantage: excellent docs, minimal requirements, actively maintained.

At first, i wanted to use mail-in-a-box, but due to it does not officially support Ubuntu 24.04, i ended up using Stalwart due to its lightweight & great documentation.


PART 2: Setup Stalwart

Stalwart has a great documentation. You may read the official docs here (for linux). Once i've installed stalwart, i can configure all the setting via admin dashboard.

1. Install stalwart

$ sudo sh install.sh
✅ Configuration file written to /opt/stalwart/etc/config.toml
🔑 Your administrator account is 'admin' with password 'w95Yuiu36E'.
🎉 Installation complete! Continue the setup at http://yourserver.org:8080/login

Take note the configuration file & the credential. If you miss it, you can run the install.sh again.

2. Once installed, log in to the admin panel using the credential given. You can leave the default setting as it be or follow from the official docs to configure the storage setting.

3. Navigate to Directories -> Domains & create your domain. Once created, view the dns record, & copy the dns records on the zonefile put it inside a .txt file on your local machine.

Screenshot 2025-06-22 at 2.28.41 PM.pngScreenshot 2025-06-22 at 3.52.05 PM.png

4. Go to your Cloudflare dashboard. Add A record with a mail subdomain in your cloudflare DNS record. Ensure the proxy is disabled.

Screenshot 2025-06-22 at 2.44.34 PM.png

5. Next, import the .txt file. Ensure you dont have any existing MX record first on your cloudflare dns record. If you have, you need to remove it first.

Screenshot 2025-06-22 at 2.44.43 PM.png


6. Navigate to Settings & update the hostname accordingly. (Some stricter MTAs will temporarily defer or mark your mail as spam, if hostname is differ from your A record.).

Screenshot 2025-06-22 at 2.31.13 PM.png



7. Navigate to Settings -> TLS -> ACME Providers. Create a new certificate provider for TLS connection.

Screenshot 2025-06-22 at 2.49.38 PM.png



8. Now you may add your mail account. Navigate to Directory -> Accounts. Create your email account & set the authentication password accordingly.

Screenshot 2025-06-22 at 2.52.18 PM.pngScreenshot 2025-06-22 at 2.52.22 PM.png

9. That it! You can test your email setup by using any mail testing tool available. Im using mail-tester & SMTP tools to test my configuration.

Screenshot 2025-06-21 at 9.29.47 PM.pngScreenshot 2025-06-21 at 9.29.43 PM.png
NOTE: Troubleshooting

1. Ensure your server allow the required port. You may check by running ufw status
to see the list of allowed port. Check if this port is in the list: 25,465,587,993,80,443.

  • 25/tcp SMTP (inbound mail)
  • 465/tcp SMTPS (legacy SSL submission; optional if you use 587 + STARTTLS only)
  • 587/tcp SMTP submission (clients)
  • 993/tcp IMAPS (clients)
  • 80/tcp HTTP
  • 443/tcp HTTPS

You can enable the rqeuired port by running this command:

sudo ufw allow 25,465,587,993,80,443/tcp
sudo ufw reload

2. Stalwart use 8080 port for web admin panel. As i have jenkins dashboard that already use the port, i need to use different port for stalwart. To modify it, edit the file /etc/stalwart/config.toml and update the 8080 port to use different port.


PART 3: Setup Roundcube

Roundcube have alot of dependency to be installed such as php, php-fpm and more. Therefore, i choose to use the docker version to consolidate everything. This way, it easier to install without having to manually install each dependency.


1. Creating docker-compose.yml.

sudo nano /opt/roundcube/docker-compose.yml

Here my docker-compose content:

services:
roundcube:
image: roundcube/roundcubemail:latest
container_name: roundcube
restart: unless-stopped

ports:
- "127.0.0.1:8080:80"

environment:
# IMAP (default_host/port, not imap_host)
ROUNDCUBEMAIL_DEFAULT_HOST: "ssl://mail.yourdomain.com"
ROUNDCUBEMAIL_DEFAULT_PORT: 993

# SMTP
ROUNDCUBEMAIL_SMTP_SERVER: "tls://mail.yourdomain.com"
ROUNDCUBEMAIL_SMTP_PORT: 587

# Database (example SQLite)
ROUNDCUBEMAIL_DBTYPE: sqlite
ROUNDCUBEMAIL_DSN: sqlite:////var/www/html/config/roundcube.db?mode=rwc

# Other settings…
ROUNDCUBEMAIL_DEFAULT_LANGUAGE: en_US
ROUNDCUBEMAIL_PLUGINS: archive,zipdownload,managesieve

volumes:
- ./data/config:/var/www/html/config
- ./data/logs:/var/www/html/logs
- ./data/temp:/var/www/html/temp
- ./data/db:/var/roundcube/db

Update the domain accordingly.

2. Next , if you dont have docker installed yet, you can run this command to install docker.

curl -sSL https://get.docker.com/ | CHANNEL=stable bash

3. Now, start the roundcube container by running this command:

docker compose up -d

4. Next, we need to create nginx reverse proxy configuration for our mail.yourdomain.com to forward all request to the roundcube. First, create the conf file.

sudo nano /etc/nginx/sites-available/mail.yourdomain.com

Here a sample conf for your reference.

# 1) Redirect HTTP → HTTPS

server {

listen 80;

listen [::]:80;

server_name mail.nizamsaidin.com;


return 301 https://$host$request_uri;

}


# 2) HTTPS site

server {

listen 443 ssl http2;

listen [::]:443 ssl http2;

server_name mail.nizamsaidin.com;


ssl_certificate /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem;

ssl_certificate_key /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem;


# Security headers

add_header X-Content-Type-Options nosniff;

add_header X-Frame-Options "SAMEORIGIN";


# Main proxy

location / {

proxy_pass http://127.0.0.1:8080;

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_set_header X-Forwarded-Proto $scheme;


proxy_hide_header X-Frame-Options;

add_header X-Frame-Options "SAMEORIGIN";

}


# Static assets (regex location)

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {

proxy_pass http://127.0.0.1:8080;

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_set_header X-Forwarded-Proto $scheme;


proxy_hide_header X-Frame-Options;

add_header X-Frame-Options "SAMEORIGIN";


expires max;

access_log off;

}


# Optional health check

location = /healthz {

return 200 "OK";

add_header Content-Type text/plain;

}

}



5. Obtain or verify your TLS certificate for mail.yourdomain.com (using Certbot) so that Nginx can serve HTTPS. For example:

sudo apt update
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d mail.yourdomain.com

6. Create a symlink of the file to /etc/nginx/sites-enabled to enable the configuration.

sudo ln -s /etc/nginx/sites-available/mail.yourdomain.com /etc/nginx/sites-enabled/

7. Reload nginx

sudo nginx -t && sudo systemctl reload nginx

8. Now Visit https://mail.yourdomain.com and log in with your email account credentials that you just created via stalwart.

Screenshot 2025-06-22 at 3.39.33 PM.png