Self-hosting
This guide takes you from an empty server to mailrelay receiving mail for your domain. If you just want to try it out, use the quick start instead.
Before you start
Section titled “Before you start”You need inbound port 25. Other mail servers only deliver to port 25, and many providers block it:
- Home ISPs often block it, and residential IPs are fine for receiving but may change.
- Some cloud providers block port 25 or ask you to request access first (AWS, Google Cloud and Azure restrict it; DigitalOcean blocks it on new accounts). Most of these restrictions are on outbound 25, which mailrelay doesn’t need, but check your provider’s docs and firewall.
- Small VPS providers (Hetzner, OVH, Vultr, a Raspberry Pi behind a port-forward) usually work.
Check from another machine once mailrelay is running:
nc -vz mx.example.com 25Can’t open port 25? Keep your existing mailbox and forward to mailrelay instead. You still need some host that accepts port 25, but it can be one cheap VPS shared by everything. See Forwarding instead of MX.
SMTP is unencrypted. mailrelay doesn’t support STARTTLS yet, so senders deliver in plain text. Mail between servers on the internet is often unencrypted anyway, but some senders flag it (Gmail shows a red “did not encrypt” warning on mail it sends to such servers). Don’t route mail through mailrelay that you wouldn’t send over plain SMTP.
Pick a hostname for the server, for example mx.example.com, and the domain you want to receive mail for, for example alerts.example.com.
| Record | Name | Value |
|---|---|---|
A (and AAAA if you have IPv6) |
mx.example.com |
your server’s IP |
MX |
alerts.example.com |
10 mx.example.com |
Set smtp.domain to the hostname (mx.example.com). It’s what mailrelay announces in its SMTP greeting.
Check it:
dig +short MX alerts.example.comYou don’t need SPF, DKIM or a PTR record for mailrelay itself. Those matter for sending mail, and mailrelay only receives.
Forwarding instead of MX
Section titled “Forwarding instead of MX”If your mail already lives somewhere else, set up a forwarding rule there that sends the mails you care about to an address mailrelay accepts:
- Gmail: Settings → Forwarding and POP/IMAP → add a forwarding address, then create a filter that forwards matching mail to it. Gmail sends a confirmation email to that address first. It shows up in the mailrelay UI as Dropped, so open it there to copy the code.
- Fastmail: Settings → Filters & Rules → new rule → “Send a copy to”.
- Cloudflare Email Routing: add a custom address that routes to a destination address. You’ll have to verify the destination the same way.
Forwarded mail keeps the original From/To headers but has a different envelope. mailrelay shows both, and rules can match either. If you set the auth checks to enforce, read Auth checks and forwarded mail first.
Install with Docker
Section titled “Install with Docker”Start from docker-compose.example.yaml:
mkdir mailrelay && cd mailrelaycurl -fsSL -o docker-compose.yaml https://raw.githubusercontent.com/gowthamgts/mailrelay/main/docker-compose.example.yamlcurl -fsSL -o config.yaml https://raw.githubusercontent.com/gowthamgts/mailrelay/main/config.example.yamlEdit config.yaml. At minimum set:
smtp.domainto your hostnamesmtp.allowed_recipientsto the addresses you want, such as"*@alerts.example.com". Without it mailrelay accepts mail for any address.
The image sets http.addr, webui.db_path and webui.raw_email_dir through environment variables, and env vars beat the config file. So those three keys in config.yaml are ignored in Docker; to change one, set its MAILRELAY_ variable in the compose file instead. The UI listens on all interfaces inside the container, and the compose file publishes it only on the host’s 127.0.0.1.
Then uncomment the ./config.yaml:/data/config.yaml:ro volume in docker-compose.yaml, and start it:
docker compose up -ddocker compose logs -fPin a version (ghcr.io/gowthamgts/mailrelay:1.2.3) instead of latest so upgrades happen when you choose. Releases are listed on the releases page.
The container runs as a non-root mailrelay user and can still bind port 25, because Docker allows that inside the container by default.
Install the binary
Section titled “Install the binary”Release archives for Linux and macOS (amd64 and arm64) are on the releases page.
tar xzf mailrelay_*_linux_amd64.tar.gzsudo install mailrelay /usr/local/bin/sudo useradd --system --home /var/lib/mailrelay --create-home mailrelaysudo mkdir -p /etc/mailrelaysudo cp config.example.yaml /etc/mailrelay/config.yamlBy default the database is created as mailrelay.db in the working directory. Point it at the data directory in /etc/mailrelay/config.yaml:
webui: db_path: "/var/lib/mailrelay/mailrelay.db" raw_email_dir: "/var/lib/mailrelay/emails"Leave webui.enabled on. Rules are stored in the web UI’s database, so with it off mailrelay has no rules and drops every email.
Run it with systemd. AmbientCapabilities lets the unprivileged user bind port 25:
[Unit]Description=mailrelayAfter=network-online.targetWants=network-online.target
[Service]User=mailrelayGroup=mailrelayExecStart=/usr/local/bin/mailrelay -config /etc/mailrelay/config.yamlRestart=on-failureAmbientCapabilities=CAP_NET_BIND_SERVICECapabilityBoundingSet=CAP_NET_BIND_SERVICENoNewPrivileges=trueProtectSystem=strictProtectHome=trueReadWritePaths=/var/lib/mailrelayPrivateTmp=true
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable --now mailrelayjournalctl -u mailrelay -fFirst checks
Section titled “First checks”- Open the UI. On a remote server without a reverse proxy, use an SSH tunnel:
ssh -L 2623:127.0.0.1:2623 you@server, then visit http://localhost:2623. - Send a test email from your normal mailbox to an allowed address. It should appear as Dropped (no rules yet).
- Create a rule and click Replay on that email to try the webhook without sending another email.
Look at the startup logs too. mailrelay warns if smtp.allowed_recipients is empty or the web UI is reachable from other hosts without a login.
Security checklist
Section titled “Security checklist”-
smtp.allowed_recipientslists only your addresses. - The web UI is on
127.0.0.1, or behind a reverse proxy with a login. -
webhook.allow_private_networksis on only if your webhooks really point at your LAN, Docker or localhost. While it’s on, anyone who can edit rules can make requests to your internal network. - Your webhook receivers check a secret (see Securing your receivers).
-
smtp.max_message_bytessuits your mail (default 25 MB). - The auth checks are
loguntil you’ve watched real mail pass, thenenforceif you want to reject forgeries. - Port 2623 isn’t open in your firewall unless you mean it to be.
Data, backups and upgrades
Section titled “Data, backups and upgrades”Everything lives in the data directory: /data in Docker, or wherever webui.db_path and webui.raw_email_dir point.
| Path | What it holds |
|---|---|
mailrelay.db (plus -wal and -shm) |
SQLite: rules, emails, delivery attempts, saved settings |
emails/<id>.eml |
Raw copy of each email, used by Replay and Download |
config.yaml |
Optional config file |
Retention. Emails, their delivery attempts and their .eml files are deleted after webui.retention_days (default 7), checked hourly. Rules and settings are never deleted. Disk use is roughly your daily mail volume times the retention days. If you start with retention_days: 0 (keep forever), cleanup doesn’t run until you restart with a value above 0.
Saved settings win. Once you click Save on the Settings page, the values stored there (log level, auth checks, retry and retention) override the config file and env vars on every start. Change them in the UI, not the config file.
Backups. The database is in WAL mode, so copying only mailrelay.db while it’s running can give you a stale or broken copy. Use SQLite’s online backup instead:
# Dockerdocker run --rm -v mailrelay_data:/data -v "$PWD":/backup alpine \ sh -c 'apk add -q sqlite && sqlite3 /data/mailrelay.db ".backup /backup/mailrelay.db"'
# binarysqlite3 /var/lib/mailrelay/mailrelay.db ".backup /var/backups/mailrelay.db"Back up emails/ too if you want to be able to replay old mail. Rules are the part that’s hard to recreate, and they’re in the database.
Upgrades. Back up, change the image tag (or replace the binary), and restart. The database schema updates itself on startup. Read the release notes before jumping several versions.
