Skip to content
Go back

Self-Hosted Email Aliasing on Your Own Domain

· Updated:
By SumGuy 13 min read
Self-Hosted Email Aliasing on Your Own Domain
Contents

The Dream vs. The Reality

Someone in every self-hosting forum, at least once a week, says it: “Just host your own email server.” And they’re not wrong in spirit. But they are deeply, tragically wrong about how much pain that involves.

Running a full mail server, Postfix, Dovecot, SpamAssassin, OpenDKIM, the works, means spending your weekend configuring PTR records, begging your VPS provider to unblock port 25, convincing Gmail that you’re not a bot farm, and watching your carefully crafted newsletters land directly in spam because your IP used to belong to someone who sold Viagra in 2009.

You probably don’t actually need to run your own mail server. What you actually want is to give a unique email address to every website you sign up for, on your own domain, so when DataBreach #4,729 happens you can trace exactly who sold your email and nuke that alias without touching your real inbox.

That’s email aliasing: a completely different (and saner) problem.

Why Aliasing Beats the Full Server

Full self-hosted email is a two-part problem: receiving mail and sending mail. Receiving is honestly fine. Your server gets an MX record, accepts connections on port 25, done. Not trivial, but doable.

Sending is where everything catches fire.

Deliverability to Gmail, Outlook, and Yahoo is gated behind a wall of reputation signals: IP warmup history, DMARC/DKIM/SPF alignment, reverse PTR records that match your hostname, and the general goodwill of major providers who can reject your mail for vibes alone. Commercial email services like Postmark and AWS SES exist specifically because building and maintaining sender reputation is a full-time job.

Aliasing sidesteps all of this. The aliasing server receives mail on your domain and forwards it to your real account (Gmail, Proton, Fastmail, whatever you already trust). You never need to send cold mail from your own IP. When you reply through an alias, the aliasing layer rewrites headers so your real address stays hidden, then routes the outbound message through a configured SMTP relay with proper reputation.

Your job: set up MX records, configure SPF/DKIM/DMARC, and point a working SMTP relay at your aliasing server. That’s it.

The Two Contenders

Two projects dominate the self-hostable aliasing space.

SimpleLogin: Proton acquired this in 2022, which either reassures you or makes you suspicious depending on your threat model. The good news: it’s still open source and fully self-hostable. It has polished mobile apps, browser extensions for every major browser, and a web UI that doesn’t look like it was designed by a sysadmin on their lunch break. SimpleLogin supports sending through aliases (reply and compose), custom domains, PGP encryption between server and registered users, and multiple inboxes per account. The self-hosted path uses Docker and is reasonably well-documented.

addy.io (formerly AnonAddy): pure open source, community-driven, no acquisition story to worry about. The self-hosted setup is similarly Docker-based. The web UI is solid; mobile apps exist but are less polished than SimpleLogin’s. addy.io supports multiple aliases per recipient, custom domains, bandwidth-based limits (configurable when self-hosting), and similar reply-through-alias behavior. If you’re already paying for Proton services and want SimpleLogin’s hosted tier “for free,” you get it. If you want no corporate parent anywhere near your privacy stack, addy.io is your call.

Honestly? Both are good. The architectural difference is mostly vibes and ecosystem preferences.

Architecture in 30 Seconds

Spammy Website → sends to [email protected]
MX record → your server running SimpleLogin or addy.io
Aliasing layer looks up alias → routes to [email protected]
Gmail receives forwarded mail (sender: [email protected])
You reply → aliasing layer rewrites From → Spammy Website sees alias, not Gmail

Your real address never leaves the aliasing server. When a site gets breached, you disable that one alias. The rest of your mail is unaffected.

Self-Hosting SimpleLogin

DNS First

Before you touch Docker, get DNS right. You need:

MX record, points your domain’s mail to your server:

yourdomain.com. MX 10 mail.yourdomain.com.
mail.yourdomain.com. A YOUR_SERVER_IP

SPF, tells receivers who’s allowed to send mail from your domain:

yourdomain.com. TXT "v=spf1 ip4:YOUR_SERVER_IP -all"

If you’re routing outbound through Postmark, add their include:

yourdomain.com. TXT "v=spf1 ip4:YOUR_SERVER_IP include:spf.mtasv.net -all"

DKIM, cryptographic signature proving the mail came from your server. SimpleLogin generates a keypair on first run. You’ll grab the public key from the container and create a TXT record:

dkim._domainkey.yourdomain.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCS..."

DMARC, tells receivers what to do with mail that fails SPF/DKIM checks:

_dmarc.yourdomain.com. TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]"

Start with p=quarantine instead of p=reject until you’re confident everything is aligned.

PTR record, the reverse DNS entry mapping your server IP back to your hostname. This one’s on your VPS provider’s control panel, not your DNS host. Set YOUR_SERVER_IP → mail.yourdomain.com. Most providers let you do this from the dashboard. Some budget providers don’t. If your provider won’t give you PTR control, your deliverability will be limited, use a proper SMTP relay for outbound.

DKIM Key Generation

Before writing your compose file, generate the keypair SimpleLogin expects:

Terminal window
mkdir -p /opt/simplelogin/dkim
openssl genrsa -out /opt/simplelogin/dkim/dkim.key 2048
openssl rsa -in /opt/simplelogin/dkim/dkim.key -pubout -out /opt/simplelogin/dkim/dkim.pub.key

Grab the public key content (strip the header/footer lines, join into one line), that’s what goes in your DKIM TXT record.

Docker Compose

docker-compose.yml
services:
postgres:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_USER: simplelogin
POSTGRES_PASSWORD: changeme_strong_password
POSTGRES_DB: simplelogin
volumes:
- ./postgres-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- ./redis-data:/data
app:
image: simplelogin/app:latest
restart: unless-stopped
depends_on:
- postgres
- redis
env_file:
- .env
ports:
- "7777:7777"
volumes:
- ./dkim:/dkim
- ./upload:/code/static/upload
email-handler:
image: simplelogin/app:latest
restart: unless-stopped
command: ["python", "email_handler.py"]
depends_on:
- postgres
- redis
env_file:
- .env
ports:
- "20381:20381"
volumes:
- ./dkim:/dkim

Environment Config

.env
# Domain config
URL=https://app.yourdomain.com
EMAIL_DOMAIN=yourdomain.com
ALIAS_DOMAINS=yourdomain.com
SUPPORT_EMAIL=[email protected]
# Database
DB_URI=postgresql://simplelogin:changeme_strong_password@postgres/simplelogin
# Redis
REDIS_URL=redis://redis:6379/0
# Secret key. Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=your_generated_secret_key_here
# DKIM
DKIM_PRIVATE_KEY_PATH=/dkim/dkim.key
DKIM_DNS_VALUE=dkim._domainkey.yourdomain.com
# Email handler
EMAIL_HANDLER_HOST=email-handler
EMAIL_HANDLER_PORT=20381
# Outbound SMTP. Use Postmark, Mailgun, or similar
POSTFIX_SERVER=smtp.postmarkapp.com
POSTFIX_PORT=587
POSTFIX_USERNAME=your-postmark-api-token
POSTFIX_PASSWORD=your-postmark-api-token
# App settings
DISABLE_REGISTRATION=false
DISABLE_ONBOARDING=false

One reality check: POSTFIX_SERVER here isn’t actually Postfix. SimpleLogin’s naming is a bit loose, it’s just an SMTP relay endpoint. Point it at Postmark, Mailgun, AWS SES, or whatever you’re using for legitimate outbound. Plan for about $1-2/month for light usage on most providers.

Initialize and Run

Terminal window
docker compose up -d postgres redis
# Wait a few seconds for postgres to initialize
docker compose run --rm app flask db upgrade
docker compose up -d

Point your reverse proxy (Caddy, Nginx, Traefik) at port 7777 for the web UI. Make sure your MX record’s port 25 reaches the email-handler container on port 20381, most reverse proxies don’t handle SMTP, so you’ll need to forward port 25 → 20381 directly at the host level or via your firewall.

Terminal window
# On the host, if using ufw or iptables:
iptables -t nat -A PREROUTING -p tcp --dport 25 -j REDIRECT --to-port 20381

Or just expose port 25 in Docker and map it to 20381 in the compose file if SimpleLogin is the only mail service on the host.

Self-Hosting addy.io

Compose Setup

addy.io is MariaDB or MySQL only. The Laravel config hardcodes DB_CONNECTION=mysql at container init, so pointing it at PostgreSQL fails on the first migration. Unlike SimpleLogin, everything else lives in one container: nginx, php-fpm, Postfix and Rspamd all run under s6 inside the addy image, which is why port 25 is published on it directly.

docker-compose.yml
services:
addy:
image: anonaddy/anonaddy:1.7.1
restart: unless-stopped
depends_on:
- db
- redis
env_file:
- .env
ports:
# SMTP. Your MX record resolves here.
- "25:25"
# Web UI. Put your reverse proxy in front of this one.
- "8000:8000"
environment:
DB_HOST: db
REDIS_HOST: redis
volumes:
- ./data:/data
db:
image: mariadb:12
restart: unless-stopped
command:
- "mariadbd"
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_unicode_ci"
environment:
MARIADB_RANDOM_ROOT_PASSWORD: "yes"
MYSQL_DATABASE: anonaddy
MYSQL_USER: anonaddy
MYSQL_PASSWORD: changeme
volumes:
- ./db:/var/lib/mysql
redis:
image: redis:8-alpine
restart: unless-stopped

There is no separate queue worker. The image pins QUEUE_CONNECTION=sync and runs jobs in-process, so a second container running artisan queue:work just burns RAM. The single ./data volume holds storage, DKIM keys and the GPG keyring, and it has to be writable by the PUID/PGID you set or the container refuses to start.

addy.io Environment

.env
APP_NAME=addy
APP_KEY=base64:your_generated_key_here
APP_URL=https://addy.yourdomain.com
# Must match the MariaDB service above. DB_HOST comes from compose.
DB_DATABASE=anonaddy
DB_USERNAME=anonaddy
DB_PASSWORD=changeme
# Long random string used to hash anonymous reply addresses. Required,
# and changing it later breaks every reply alias already handed out.
ANONADDY_SECRET=your_long_random_string_here
# Mail receiving
ANONADDY_DOMAIN=yourdomain.com
ANONADDY_HOSTNAME=mail.yourdomain.com
ANONADDY_ADMIN_USERNAME=yourname
# Outbound relay. Mail leaves through the container's own Postfix,
# so these are POSTFIX_ vars, not Laravel MAIL_ vars.
MAIL_FROM_NAME=addy
MAIL_FROM_ADDRESS=[email protected]
POSTFIX_RELAYHOST=[smtp.postmarkapp.com]:587
POSTFIX_RELAYHOST_AUTH_ENABLE=true
POSTFIX_RELAYHOST_USERNAME=your-postmark-token
POSTFIX_RELAYHOST_PASSWORD=your-postmark-token
POSTFIX_SMTP_TLS=true
# Limits. Bandwidth is a byte count, not a human-readable string.
# 10737418240 is 10GB. New aliases per hour, not a total cap.
ANONADDY_BANDWIDTH_LIMIT=10737418240
ANONADDY_NEW_ALIAS_LIMIT=100

Two of those are hard requirements: the container init script aborts with an error if APP_KEY or ANONADDY_SECRET is missing. Setting MAIL_HOST here does nothing, because the image overwrites it with 127.0.0.1 to route outbound mail through the local Postfix.

DNS setup for addy.io is identical to SimpleLogin, same MX, SPF, DKIM, DMARC structure. Generate both secrets into .env before the first start. The container runs its own migrations on boot, so there is no separate migrate step:

Terminal window
# APP_KEY
echo "base64:$(openssl rand -base64 32)"
# ANONADDY_SECRET
openssl rand -hex 32
docker compose up -d
docker compose logs -f addy
# First account. The admin username set in .env is the one
# that receives mail at the bare root domain.
docker compose exec addy anonaddy anonaddy:create-user "yourname" "[email protected]"

DKIM keys live on the same /data volume. Generate the pair with docker compose run --entrypoint "" addy gen-dkim, then publish the TXT record it writes to data/dkim/yourdomain.com.txt. Rspamd stays disabled until that private key exists, and anonymous replies need Rspamd, so do this before you test the reply path.

Adding Your Custom Domain in addy.io

Once the app is running, log in, go to Domains, add yourdomain.com. addy.io will show you the exact DNS records it needs and verify them. This is where you set up the catch-all behavior: any address *@yourdomain.com routes through addy’s alias table and forwards to your registered inbox.

Replying Through Your Alias

This is the feature that makes the whole thing actually usable. Without it, you could receive mail through an alias but you’d have to reply from your real address, instantly exposing it.

Both SimpleLogin and addy.io handle this with a header-rewriting trick. When a forwarded email arrives in your real inbox, the Reply-To header is set to a special bounce address on your aliasing server (something like [email protected]). When you reply, your real mail client sends to that address. The aliasing server receives it, decodes the original sender, rewrites the From header to your alias, and delivers the reply through your SMTP relay.

From the recipient’s side: they see a reply from [email protected]. Your Gmail address is never in the headers. It’s not magic; it’s just header manipulation. But it works.

Threat Model: When Does Aliasing Actually Help?

Aliasing is good at one specific thing: isolating your real email address from third parties you don’t fully trust.

Aliasing is not a full anonymity solution. Your aliasing server knows your real address. If someone gets access to it, they see everything. And metadata, timing, volume, IP in the received headers, can still leak information even when the address itself is hidden.

If your threat model involves “keep my email private from sophisticated adversaries with legal authority,” you need Tor-accessed Proton with no recovery options, not a self-hosted forwarder. But for “I don’t want Booking.com knowing my real address,” aliasing is exactly right.

The Backup Problem (And Why It Matters More Than You Think)

Here’s the failure mode nobody talks about when they’re setting up a cool self-hosted aliasing server: your server dies.

If you’re using hosted SimpleLogin via Proton ($4/month or free with a Proton plan), your aliases survive because they’re on Proton’s infrastructure. If you’re self-hosting and your VPS gets the boot, your database is unrecoverable, or you forget to renew your domain, every alias you’ve ever given out stops working simultaneously.

You’ve just effectively deleted your email address for every account that uses those aliases.

The mitigation:

Terminal window
# SimpleLogin: backup postgres
docker compose exec postgres pg_dump -U simplelogin simplelogin | gzip > /backup/sl-$(date +%Y%m%d).sql.gz
# addy.io: backup postgres
docker compose exec postgres pg_dump -U addy addy | gzip > /backup/addy-$(date +%Y%m%d).sql.gz

Schedule that. Automate it. Test your restore process at least once. Your 2 AM self who just lost their VPS will appreciate it.

Alternatively: if reliability matters more than control, just pay Proton for hosted SimpleLogin and skip the ops burden. Four dollars a month buys you someone else’s uptime guarantee and a team of people who care about DKIM alignment more than you want to.

Should You Bother?

Yes, if:

Probably not if:

The honest take: aliasing on your own domain is one of the most practical privacy tools available to a home lab person. It’s not glamorous, it doesn’t require explaining threat models to normies, and it just works once it’s set up. SimpleLogin and addy.io are both solid choices. The difference is whether you care more about polish and apps (SimpleLogin) or pure open-source independence (addy.io).

Pick one, set it up on a Saturday afternoon, and next time some website requires your email to download a whitepaper, give them [email protected]. When the spam starts, you’ll know exactly who to blame, and you’ll be one click away from making it stop.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
LoRa Mesh Hardware Buying Guide
Next Post
CryptPad vs EtherCalc: Privacy Collaboration

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts