addy.io on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of addy.io on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. addy.io (formerly AnonAddy) is a self hosted email aliasing service: instead of handing your real address to every shop, forum, newsletter and app that asks for one, you give each of them a different alias on your own domain.
Mail sent to an alias is forwarded to your real inbox, and you can reply from the alias so the other side never learns where it actually lands. When an alias starts attracting spam you switch it off or delete it, and only that one correspondent is affected. Because every alias is unique to one recipient, an address that suddenly starts receiving spam tells you exactly who leaked or sold it.
Aliases can be created from the web interface, from the official browser extensions and mobile apps, or through the API. Each one carries a description so you remember what it was for, can be enabled and disabled at any time, and keeps a count of what has been forwarded, blocked and replied through it. You can add several recipient mailboxes and point different aliases at different ones, block individual senders, build rules that act on incoming mail, and have forwards signed and encrypted with your own PGP key.
The cloudimg image ships the free and open source, AGPL-3.0 licensed addy.io 1.7.0, run unmodified from the upstream release tag. The Laravel application runs under PHP 8.3-FPM with MariaDB and Redis on the loopback interface only; nginx on ports 80 and 443 is the single public web listener and Postfix on port 25 is the single public mail listener. Rspamd signs your outgoing alias mail with DKIM and ARC and checks SPF, DKIM and DMARC on the way in. Backed by 24/7 cloudimg support.
addy.io is an independent open source project. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by addy.io. It ships the free and open source AGPL-3.0 licensed software, unmodified.

What is included:
- addy.io 1.7.0 (AGPL-3.0), unmodified upstream, with the complete corresponding source on the image
- PHP 8.3-FPM, MariaDB 10.11, Redis 7.0, Postfix 3.8, Rspamd and nginx, all from the Ubuntu 24.04 archive so they keep receiving distribution security updates
- A first boot service that generates every secret on your own VM and seeds exactly one administrator
- A fail closed guard that refuses to open either public listener with an empty, unrotated or published default secret
addy-set-domain, a single command that repoints the whole appliance at your domain and prints the DNS records to publish
Prerequisites
- An active Azure subscription
- Permission to create virtual machines and network security rules
- An SSH key pair for administrative access
- A domain you control, with access to its DNS. Aliases cannot receive real mail until your domain's MX record points at this VM.
- Inbound TCP
80and443from the networks that will use the web interface - Inbound TCP
25from anywhere, so other mail servers can deliver to your aliases - The recommended size is Standard_B2s (2 vCPU, 4 GiB) or larger
Step 1: Deploy from the Azure Portal
- Sign in to the Azure Portal and choose Create a resource.
- Search the Marketplace for addy.io by cloudimg and select the offer.
- Choose your subscription, resource group and region.
- Set the VM size to Standard_B2s or larger.
- Set the administrator username to
azureuserand upload or generate an SSH public key. - On Disks, choose Standard SSD or Premium SSD.
- On Networking, allow inbound SSH (22), HTTP (80), HTTPS (443) and SMTP (25). Restrict
22,80and443to your own address ranges;25must be open to the internet for mail delivery to work. - Review and create. The VM boots, generates its own secrets, seeds its administrator and only then opens the public listeners.
Step 2: Deploy from the Azure CLI
Replace the placeholders with your own values.
az group create \
--name my-resource-group \
--location eastus
az vm create \
--resource-group my-resource-group \
--name addy-io-01 \
--image cloudimg:addy-io-ubuntu-24-04:default:latest \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# open the ports the appliance needs
az vm open-port --resource-group my-resource-group --name addy-io-01 --port 80 --priority 1001
az vm open-port --resource-group my-resource-group --name addy-io-01 --port 443 --priority 1002
az vm open-port --resource-group my-resource-group --name addy-io-01 --port 25 --priority 1003
Azure blocks outbound port 25 on most subscriptions. That does not stop aliases receiving mail, but it does stop the appliance delivering forwards directly. See Step 11 for the relay configuration.
Step 3: Connect to your VM
ssh azureuser@<vm-ip>
Step 4: Confirm the appliance is running
First boot generates every secret, rotates the database password, creates this VM's own DKIM key, seeds the administrator and only then opens the public listeners. It normally completes within two minutes of the VM becoming reachable.
Check that first boot finished:
test -f /var/lib/cloudimg/addy-io-firstboot.done && echo "first boot completed"
Expected output:
first boot completed
Check the services:
for s in nginx postfix php8.3-fpm mariadb redis-server rspamd supervisor; do
printf '%-16s %s\n' "$s" "$(systemctl is-active $s)"
done
Expected output:
nginx active
postfix active
php8.3-fpm active
mariadb active
redis-server active
rspamd active
supervisor active
Check the queue workers that deliver forwarded mail:
sudo supervisorctl status 'addy-io:*'
Expected output:
addy-io:addy-io_00 RUNNING pid 46779, uptime 0:09:55
addy-io:addy-io_01 RUNNING pid 46780, uptime 0:09:55
Confirm the web interface answers:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/login
Expected output:
200
Only four ports are reachable from outside the VM. Everything else is bound to loopback:
ss -lntH | awk '{print $4}' | sort -u
Expected output:
0.0.0.0:22
0.0.0.0:25
0.0.0.0:443
0.0.0.0:80
127.0.0.1:11332
127.0.0.1:11333
127.0.0.1:11334
127.0.0.1:3306
127.0.0.1:6379
Step 5: Read your per instance credentials
Every secret on this appliance is generated on your own virtual machine at first boot and exists nowhere else. There is no default password, and no cloudimg image ever ships one.
sudo cat /root/addy-io-credentials.txt
Expected output (your values will differ):
# addy.io - per-VM credentials, generated on this machine at first boot.
# Nothing here is shared with any other virtual machine or with cloudimg.
ADDY_URL=http://20.30.40.50
ADDY_ADMIN_USERNAME=admin
ADDY_ADMIN_PASSWORD=********************************
ADDY_ADMIN_RECIPIENT=admin@appliance.example.com
ADDY_EMAIL_DOMAIN=addy-appliance.example.com
ADDY_ALIAS_DOMAIN=admin.addy-appliance.example.com
ADDY_DKIM_PUBLIC_KEY=<public key, publish as a DNS TXT record>
The file is readable only by root:
sudo stat -c '%a %U:%G' /root/addy-io-credentials.txt
Expected output:
600 root:root

ADDY_EMAIL_DOMAIN is a placeholder — addy-appliance.example.com is a subdomain of the RFC 2606 documentation domain, so no third party can ever own it and an unconfigured appliance can never emit mail at a real party. Step 9 replaces it with your own domain.
Step 6: Sign in
Browse to the address in ADDY_URL and sign in with the username and password from the credentials file.

After signing in you land on the dashboard, which summarises your aliases, recipients, usernames, domains and this month's bandwidth.

Change the password after your first sign in — go to Settings, then Change password. Once you have set your own password you can delete the credentials file:
sudo shred -u /root/addy-io-credentials.txt
Step 7: Why nobody else can claim your appliance
Self hosted addy.io ships with ANONADDY_ENABLE_REGISTRATION=true and has no concept of an owner. On a public address that means any passer by can register an account on your server and start issuing aliases on your domain. Upstream's own instructions tell you to create your account first and disable registration afterwards, which leaves a window open.
This image never enters that state. Registration is closed in the configuration from the moment it is written, and both public listeners are held shut until exactly one administrator exists.
Confirm registration is refused:
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-d 'username=intruder&email=intruder@example.com&password=Intruder12345' \
http://127.0.0.1/register
Expected output — the route does not exist at all, so there is nothing to attack:
404
Confirm there is exactly one account:
sudo mariadb -uroot -N -B -e 'SELECT COUNT(*) FROM addy_database.users;'
Expected output:
1

A guard runs before both public listeners start and refuses to let either serve if any secret is empty, still unrotated, or one of the values published in upstream's .env.example:
sudo /usr/local/sbin/addy-io-preflight.sh
Expected output:
addy-io-preflight: OK
You can see it wired in front of each listener:
systemctl show -p ExecStartPre --value nginx.service | grep -o addy-io-preflight
systemctl show -p ExecStartPre --value postfix.service | grep -o addy-io-preflight
Expected output:
addy-io-preflight
addy-io-preflight
Step 8: Create your first aliases
There are two ways to create aliases.
On the fly. Make up any address at your alias domain and hand it out. The alias appears in the interface as soon as it forwards its first email. Nothing needs to be created in advance.
Explicitly. Choose Aliases, then Create Alias, pick the domain, and choose a format — random characters, random words, a random name, a UUID, or a custom local part.

Aliases can also be created over the API. First exchange your password for an API key, then call the alias endpoint:
API_KEY=$(curl -s -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"username":"admin","password":"<your-password>","device_name":"cli"}' \
http://127.0.0.1/api/auth/login | jq -r .api_key)
curl -s -X POST -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"domain":"admin.addy-appliance.example.com"}' \
http://127.0.0.1/api/v1/aliases | jq '.data|{email,active,created_at}'
Expected output (your alias will differ):
{
"email": "vz1gmyww@admin.addy-appliance.example.com",
"active": true,
"created_at": "2026-07-26 07:17:18"
}

The sign in endpoint is rate limited to three requests per minute, so space out repeated calls. Once you have an API key, save it — the browser extensions and the official mobile apps use the same key.
Step 9: Point your own domain at the appliance
Until you do this, the appliance uses the placeholder domain and cannot receive real mail. One command repoints the application, the mail server and the DKIM signing tables together:
sudo addy-set-domain example.com
It rewrites the application configuration, generates a 2048-bit DKIM key for your domain, re-renders Postfix and the Rspamd signing tables, reloads every service, and prints exactly what to publish:
==========================================================================
Publish these DNS records for example.com
==========================================================================
A mail.example.com 20.30.40.50
A example.com 20.30.40.50
A *.example.com 20.30.40.50
MX example.com mail.example.com (priority 10)
MX *.example.com mail.example.com (priority 10)
TXT example.com "v=spf1 mx ~all"
TXT *.example.com "v=spf1 mx ~all"
TXT _dmarc.example.com "v=DMARC1; p=none; sp=none; adkim=r; aspf=r; pct=100;"
TXT default._domainkey.example.com
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."
==========================================================================
Pass --hostname if your mail host is not mail.<domain>, and --https once you have a certificate. Run this form instead of the one above, not as well as it:
sudo addy-set-domain example.com --hostname mx1.example.com --https
Two more things are needed outside the VM:
- Reverse DNS. In the Azure Portal, open the VM's public IP address, set the DNS name label and then set reverse DNS to your mail hostname. Many receiving mail servers reject mail from an address with no matching PTR record.
- Inbound port 25. Make sure the network security group allows TCP
25from anywhere.
Confirm Postfix now accepts mail for your alias domain — this is a live lookup through the application database, not just a file check. Query the username subdomain form, which is what the database map resolves (the apex domain is accepted separately, through virtual_mailbox_domains):
sudo postmap -q admin.example.com mysql:/etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf
Expected output:
1
Step 10: Recipients, the mailboxes your aliases forward to
A recipient is a real mailbox that aliases forward to. The appliance starts with one placeholder recipient; replace it with your own address.
Choose Recipients, then Add Recipient, and enter your real email address. addy.io sends a verification email to it — which requires outbound mail to be working, so complete Step 11 first if Azure is blocking port 25.

Once a recipient is verified you can point individual aliases at it, add a PGP public key so forwards are encrypted, and make it the default for new aliases.
Step 11: Outbound mail and Azure's port 25 block
Azure blocks outbound TCP 25 on most subscriptions. Inbound mail to your aliases still arrives, but the appliance cannot deliver the forward. The fix is to relay through a smart host.
Add your provider's details to Postfix:
sudo postconf -e 'relayhost = [smtp.example-relay.com]:587'
sudo postconf -e 'smtp_sasl_auth_enable = yes'
sudo postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
sudo postconf -e 'smtp_sasl_security_options = noanonymous'
sudo postconf -e 'smtp_tls_security_level = encrypt'
echo '[smtp.example-relay.com]:587 <relay-user>:<relay-password>' \
| sudo tee /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo systemctl restart postfix
Then add the relay to your SPF record so receivers accept mail sent on your behalf, for example:
TXT example.com "v=spf1 mx include:spf.example-relay.com ~all"
Watch a message go out:
sudo tail -f /var/log/mail.log
Public DNS blocklists are deliberately off. Upstream's reference configuration enables Spamhaus zen and dbl lookups. Those return "query refused" for every query made through a public cloud provider's resolver, which turns a spam check into a mail losing rejection. Subscribe to Spamhaus DQS (free for personal use) and uncomment the pre-written lines in /etc/postfix/main.cf with your own key if you want them. Rspamd still performs SPF, DKIM, DMARC and Bayesian checks either way.
Step 12: Where your data lives
| Path | Contents |
|---|---|
/var/www/addy-io |
The application, its configuration (.env) and the compiled frontend |
/var/lib/mysql |
MariaDB — your aliases, recipients, domains, rules and forwarding history |
/var/lib/redis |
Redis — sessions, cache and the delivery queue |
/var/lib/rspamd/dkim |
This VM's own DKIM private keys |
/etc/postfix |
Mail server configuration and the database lookup maps |
/root/addy-io-credentials.txt |
The per instance credentials written at first boot |
/usr/share/addy-io/source |
The complete corresponding AGPL source, its checksum and the written offer |
Back up the database and the DKIM keys together — restoring one without the other leaves signed mail that cannot be verified:
sudo mariadb-dump -uroot --single-transaction addy_database \
| gzip > ~/addy-io-$(date +%F).sql.gz
sudo tar czf ~/addy-io-keys-$(date +%F).tar.gz /var/lib/rspamd/dkim /var/www/addy-io/.env
Step 13: The complete source is on the machine
addy.io is licensed under the GNU Affero General Public License v3. The complete corresponding source for the exact version running here ships on the image with its checksum, the licence text and a written offer.
ls /usr/share/addy-io/source
Expected output:
LICENSE
WRITTEN-OFFER.txt
addy-io-1.7.0-source.tar.gz
addy-io-1.7.0-source.tar.gz.sha256
Verify it:
cd /usr/share/addy-io/source && sha256sum -c addy-io-1.7.0-source.tar.gz.sha256
Expected output:
addy-io-1.7.0-source.tar.gz: OK
Step 14: Add HTTPS
The appliance answers on 443 with a self signed certificate from first boot, so the port is open and working immediately, but browsers will warn. Once your DNS A record points at the VM, replace it with a real certificate:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d '*.example.com'
sudo addy-set-domain example.com --https
The --https flag switches the application URL to https:// and turns on secure session cookies. Certbot installs a renewal timer automatically.
Step 15: Day to day operation
Restart the application after a configuration change:
sudo systemctl restart php8.3-fpm supervisor nginx
Watch the mail flow:
sudo tail -f /var/log/mail.log
Watch the application log:
sudo tail -f /var/www/addy-io/storage/logs/laravel.log
Check the Rspamd view of a message's spam score, over an SSH tunnel from your own machine:
ssh -L 11334:127.0.0.1:11334 azureuser@<vm-ip>
Then open http://127.0.0.1:11334/ in your browser.
Ubuntu security updates are applied automatically by unattended-upgrades. To update the operating system by hand:
sudo apt-get update && sudo apt-get -y dist-upgrade
Security hardening
- Change the administrator password at first sign in, then
shredthe credentials file - Enable two factor authentication under Settings, then Two factor authentication
- Restrict inbound
22,80and443to your own address ranges in the network security group; only25needs to be open to the internet - Add a real TLS certificate (Step 14) before using the interface over the internet
- Keep registration closed. The guard refuses to start either public listener if
ANONADDY_ENABLE_REGISTRATIONis missing or true - Add a PGP public key to your recipient so forwarded mail is encrypted at rest in your mailbox
- Set
p=quarantineorp=rejectin your DMARC record once you have confirmed your mail is being delivered and signed correctly
Troubleshooting
The web interface does not answer. First boot may still be running. Check test -f /var/lib/cloudimg/addy-io-firstboot.done, then sudo journalctl -u addy-io-firstboot -n 50. nginx will not start at all until first boot has seeded the administrator — that is deliberate.
nginx or Postfix refuse to start. Run sudo /usr/local/sbin/addy-io-preflight.sh to see which check failed. The guard blocks an empty, unrotated or published default secret.
Aliases are not receiving mail. Confirm your MX record points at the VM's hostname, that inbound 25 is open in the network security group, and that sudo postmap -q <your-domain> mysql:/etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf returns 1. Then check /var/log/mail.log.
Forwards are not being delivered. Azure is almost certainly blocking outbound port 25 — see Step 11. Check the queue with sudo postqueue -p and the workers with sudo supervisorctl status 'addy-io:*'.
Mail is being marked as spam. Publish the SPF, DKIM and DMARC records printed by addy-set-domain, and set reverse DNS for the VM's public IP to your mail hostname.
A recipient never receives its verification email. Outbound mail is not working yet; complete the relay configuration in Step 11.
Support
cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk.