SimpleLogin on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of SimpleLogin on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. SimpleLogin 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 note so you remember what it was for, can be enabled and disabled at any time, and keeps a record of what has been forwarded through it. You can add several destination mailboxes and point different aliases at different ones, block individual senders, and have forwards signed and encrypted with your own PGP key. Bring as many domains as you like, define catch all rules and directory prefixes, and use the built in OpenID Connect provider to sign in to other applications.
The cloudimg image ships the free and open source, AGPL-3.0 licensed SimpleLogin 4.81.6, run unmodified as the official upstream release container under docker compose: the web application, the mail handler and the job runner all on the loopback interface, backed by PostgreSQL 16 which is also loopback only. Host nginx on port 80 is the single public web listener and host Postfix on port 25 is the single public mail listener. Backed by 24/7 cloudimg support.
SimpleLogin is an independent open source project. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by SimpleLogin or Proton. It ships the free and open source AGPL-3.0 licensed software, unmodified.

What is included:
- SimpleLogin 4.81.6, the AGPL-3.0 licensed upstream release, run unmodified from the official image pinned by digest
- The version the deployed application declares is asserted against the pinned version at build time and again after deployment, so a mislabelled upstream tag fails the build rather than shipping
- nginx on port
80and Postfix on port25, both installed as host packages so unattended security upgrades keep the internet facing components patched - The web application, the mail handler and PostgreSQL 16 all bound to the loopback interface, so nothing behind the front door is reachable from your network
- Exactly one administrator, seeded on first boot with a password unique to your VM, and registration closed in the same step
- The session signing secret, the partner API token secret, the PostgreSQL password, the DKIM key and the OpenID signing key all generated per VM on first boot and written to a root only file
- A per VM OpenID signing key, because the upstream container image carries one that is committed to a public source repository and the application defaults to it
- Fail closed guards: both public listeners refuse to start if any secret is empty, unrotated, or a published upstream default
simplelogin-set-domain, which repoints the whole appliance at your own domain and prints the exact A, MX, SPF, DKIM and DMARC records to publish- The complete corresponding source, its checksum, the AGPL licence text and a written offer at
/usr/share/simplelogin/source - Ubuntu 24.04 LTS base with latest security patches applied at build time and unattended security upgrades enabled
- Azure Linux Agent for seamless cloud integration and SSH key injection
- 24/7 cloudimg support with guaranteed 24 hour response SLA
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
80(and443once you add HTTPS) from 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 SimpleLogin 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) and SMTP (25). Restrict
22and80to 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, migrates its database and seeds its administrator automatically.
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 simplelogin-01 \
--image cloudimg:simplelogin-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 simplelogin-01 --port 80 --priority 1001
az vm open-port --resource-group my-resource-group --name simplelogin-01 --port 25 --priority 1002
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, migrates the database, seeds the administrator and only then opens the public listeners. It normally completes within two minutes of the VM being reachable.
Check the services:
sudo systemctl is-active docker simplelogin simplelogin-firstboot simplelogin-postboot nginx postfix
active
active
active
active
active
active
Check the containers:
sudo docker compose -f /opt/simplelogin/docker-compose.yml ps --format 'table {{.Name}}\t{{.Service}}\t{{.Status}}'
NAME SERVICE STATUS
sl-app app Up 3 minutes (healthy)
sl-db db Up 3 minutes (healthy)
sl-email email Up 3 minutes
sl-job-runner job-runner Up 3 minutes
Confirm the deployed version and the liveness endpoint:
curl -s http://localhost/version
v4.81.6
curl -s http://localhost/live
live
If the containers are not up yet, watch first boot finish:
sudo journalctl -u simplelogin-firstboot.service -u simplelogin-postboot.service --no-pager -n 20
Step 5: Read your per instance credentials
Every secret on this appliance was generated on your virtual machine at first boot and exists nowhere else. The administrator sign in details are written to a root only file.
sudo cat /root/simplelogin-credentials.txt

Confirm the file is readable only by root:
sudo stat -c '%a %U:%G' /root/simplelogin-credentials.txt
600 root:root
The same is true of the application configuration, which holds the session signing secret and the database password:
sudo stat -c '%a %U:%G' /opt/simplelogin/simplelogin.env
600 root:root
Step 6: Sign in
Browse to http://<vm-ip>/ and sign in with the address and password from the credentials file.

Change the password immediately from Settings, then keep the credentials file for reference or delete it.
Step 7: Why nobody else can claim your appliance
This is the single most important difference between this image and a hand rolled install. Self hosted SimpleLogin ships with sign up open and has no notion of an owner, so on a public address any passer by can create an account on your server and start issuing aliases on your domain. Upstream's own instructions tell you to create your account first and close registration afterwards, which leaves a window between the two.
This image never enters that state. First boot seeds exactly one administrator with a password unique to your VM, registration is closed in the configuration from the moment it is written, and neither the web listener nor the mail listener opens until that has happened.
An anonymous sign up attempt is refused:
curl -s -o /dev/null -w 'POST /api/auth/register (anonymous): %{http_code}\n' \
-X POST -H 'Content-Type: application/json' \
-d '{"email":"intruder@example.com","password":"Intruder12345"}' \
http://localhost/api/auth/register
POST /api/auth/register (anonymous): 400
An unauthenticated API request is refused:
curl -s -o /dev/null -w 'GET /api/user_info (anonymous): %{http_code}\n' http://localhost/api/user_info
GET /api/user_info (anonymous): 401
And the refusal is real rather than cosmetic, because the appliance still has exactly one account:
sudo docker exec sl-db psql -U simplelogin -d simplelogin -tAc 'SELECT count(*) FROM users;'
1

The published private key. The upstream container image carries an OpenID signing key that is committed to SimpleLogin's public source repository, and the application's own defaults point at it. Anyone could therefore forge tokens issued by a stock install. This appliance generates its own key on your VM and refuses to serve if the published one is ever in use:
sudo grep -c '^OPENID_PRIVATE_KEY_PATH=/keys/jwtRS256.key' /opt/simplelogin/simplelogin.env
1
To open registration later, for example so colleagues can have their own accounts, delete the DISABLE_REGISTRATION line from /opt/simplelogin/simplelogin.env and restart the stack. Only do that if the web interface is restricted to networks you trust.
Step 8: Create your first aliases
The dashboard lists every alias, what it was created for, and whether it is currently accepting mail. Random Alias generates one instantly; New Custom Alias lets you choose the local part.


Everything in the interface is also available through the API. Sign in to get an API key, reading the seeded credentials straight from the root only file so no secret is ever typed on a command line:
SL_EMAIL=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_EMAIL=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_PASS=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_PASSWORD=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_KEY=$(curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"email\":\"${SL_EMAIL}\",\"password\":\"${SL_PASS}\",\"device\":\"guide\"}" \
http://localhost/api/auth/login | jq -r '.api_key')
test -n "$SL_KEY" && test "$SL_KEY" != null && echo "signed in, API key length ${#SL_KEY}"
signed in, API key length 60
Create an alias for one specific website:
SL_EMAIL=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_EMAIL=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_PASS=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_PASSWORD=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_KEY=$(curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"email\":\"${SL_EMAIL}\",\"password\":\"${SL_PASS}\",\"device\":\"guide\"}" \
http://localhost/api/auth/login | jq -r '.api_key')
curl -s -X POST -H "Authentication: ${SL_KEY}" -H 'Content-Type: application/json' \
-d '{"note":"Created from the cloudimg deployment guide"}' \
'http://localhost/api/alias/random/new?hostname=example.com' | jq -r '.alias'
example@sl-appliance.example.com
List what you have:
SL_EMAIL=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_EMAIL=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_PASS=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_PASSWORD=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_KEY=$(curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"email\":\"${SL_EMAIL}\",\"password\":\"${SL_PASS}\",\"device\":\"guide\"}" \
http://localhost/api/auth/login | jq -r '.api_key')
curl -s -H "Authentication: ${SL_KEY}" 'http://localhost/api/v2/aliases?page_id=0' \
| jq -r '.aliases[]?.email' | sort
conference-wifi@sl-appliance.example.com
example-shop@sl-appliance.example.com
example@sl-appliance.example.com
forum-example@sl-appliance.example.com
news-weekly@sl-appliance.example.com
simplelogin-newsletter.subpar710@sl-appliance.example.com
Step 9: Point your own domain at the appliance
Until you do this the appliance uses sl-appliance.example.com, a placeholder that can never receive real mail. One command repoints the application, the mail server and the alias domain table together, and prints the DNS records to publish:
sudo simplelogin-set-domain <your-domain>
Repointing this appliance from 'sl-appliance.example.com' to 'your-domain.com'
Publish these DNS records for your-domain.com, then aliases will receive mail:
A app.your-domain.com 20.85.234.16
MX your-domain.com 10 app.your-domain.com.
TXT your-domain.com "v=spf1 mx ~all"
TXT dkim._domainkey.your-domain.com "v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
TXT _dmarc.your-domain.com "v=DMARC1; p=quarantine; adkim=r; aspf=r"
Set the reverse DNS (PTR) for 20.85.234.16 to app.your-domain.com in your cloud console,
and allow inbound TCP 25 to this VM.
Publish those five records with your DNS provider, then set the VM's reverse DNS in Azure so receiving mail servers accept your forwards:
az network public-ip update \
--resource-group my-resource-group \
--name simplelogin-01PublicIP \
--reverse-fqdn app.<your-domain>
Once DNS has propagated, Postfix accepts mail for your domain and hands it to the alias handler. You can confirm the routing at any time:
ED=$(sudo grep -m1 '^SIMPLELOGIN_EMAIL_DOMAIN=' /root/simplelogin-credentials.txt | cut -d= -f2-)
sudo postmap -q "$ED" pgsql:/etc/postfix/pgsql-relay-domains.cf
sudo postmap -q "$ED" pgsql:/etc/postfix/pgsql-transport-maps.cf
sl-appliance.example.com
smtp:127.0.0.1:20381

Additional domains are added from the interface, which shows you the records to publish and verifies them for you.

Step 10: Mailboxes, the addresses your aliases forward to
A mailbox is a real address that receives the forwarded mail. Your administrator account starts with one, and you can add others and point different aliases at different mailboxes.

Adding a mailbox sends a verification email to it, so configure outbound mail first (next step).
Step 11: Outbound mail and Azure's port 25 block
Azure blocks outbound connections on port 25 for most subscriptions. Inbound mail is unaffected, so aliases still receive, but forwards and verification emails will not leave the VM until you relay them through a provider that accepts authenticated submission on port 587.
Check whether outbound 25 works at all on your subscription:
timeout 8 bash -c 'cat < /dev/null > /dev/tcp/aspmx.l.google.com/25' 2>/dev/null \
&& echo "outbound 25 is open" || echo "outbound 25 is blocked, configure a relay below"
outbound 25 is blocked, configure a relay below
To relay, add your provider's submission host and credentials to Postfix:
sudo postconf -e 'relayhost = [smtp.<your-domain>]: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.<your-domain>]:587 <your-relay-user>:<your-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
Make sure the relay is authorised to send for your domain, and include it in your SPF record.
Step 12: Where your data lives
Everything the appliance stores is on the VM.
sudo docker volume ls --filter name=simplelogin --format '{{.Name}}'
simplelogin_sl_db_data
sudo ls -1 /var/lib/simplelogin
bootstrap
keys
sl
upload
Back up the database and the per VM keys together, because the keys are what make your DKIM signatures and OpenID tokens valid:
sudo docker exec sl-db pg_dump -U simplelogin -d simplelogin -Fc -f /tmp/simplelogin.dump \
&& sudo docker cp sl-db:/tmp/simplelogin.dump /var/backups/simplelogin-$(date +%F).dump \
&& sudo ls -l /var/backups/ | tail -2
Copy /var/lib/simplelogin/keys, /opt/simplelogin/simplelogin.env and /root/simplelogin-credentials.txt to secure storage as well. They are the only copies.
Step 13: The complete source is on the machine
SimpleLogin is licensed under the GNU Affero General Public License v3. Section 13 requires that anyone interacting with it over a network can obtain the corresponding source, so it ships on the image.
ls -1 /usr/share/simplelogin/source/
LICENSE
WRITTEN-OFFER.txt
simplelogin-4.81.6-source.tar.gz
simplelogin-4.81.6-source.tar.gz.sha256
The archive is verified against its checksum:
cd /usr/share/simplelogin/source && sha256sum -c simplelogin-4.81.6-source.tar.gz.sha256
simplelogin-4.81.6-source.tar.gz: OK
head -3 /usr/share/simplelogin/source/LICENSE
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Step 14: Add HTTPS
The appliance serves plain HTTP on port 80 so it works the moment it boots. Once app.your-domain.com resolves to the VM, add a certificate:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.<your-domain> --agree-tos --redirect -m you@<your-domain>
Then tell SimpleLogin it is being served over HTTPS, so the links it puts in emails are correct:
sudo sed -i 's|^URL=http://.*|URL=https://app.<your-domain>|' /opt/simplelogin/simplelogin.env
sudo docker compose -f /opt/simplelogin/docker-compose.yml restart app
Certbot installs a renewal timer automatically.
Step 15: Day to day operation
Restart the whole stack:
sudo systemctl restart simplelogin.service
Watch the application log:
sudo docker compose -f /opt/simplelogin/docker-compose.yml logs --tail 20 app
Watch the mail handler, which is where forwarding problems show up:
sudo docker compose -f /opt/simplelogin/docker-compose.yml logs --tail 20 email
Check the mail queue:
sudo postqueue -p | tail -3
Mail queue is empty
Confirm exactly which images are running. They are pinned by digest, so this is the precise build of every component, not a moving tag:
sudo docker compose -f /opt/simplelogin/docker-compose.yml images --format json \
| jq -r '.[] | .ContainerName + " " + .Repository + "@" + .ID' | sort
sl-app simplelogin/app-ci@sha256:3abf622d85fbcb090566927c94733ad1a9b1edb215e3a1db61c1e2c116229ee5
sl-db postgres@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
sl-email simplelogin/app-ci@sha256:3abf622d85fbcb090566927c94733ad1a9b1edb215e3a1db61c1e2c116229ee5
sl-job-runner simplelogin/app-ci@sha256:3abf622d85fbcb090566927c94733ad1a9b1edb215e3a1db61c1e2c116229ee5
Security hardening
- Restrict port 80 and 443 in the network security group to the address ranges that need the interface. Port
25has to stay open to the internet, but it only speaks SMTP. - Add HTTPS (Step 14) before using the appliance over an untrusted network.
- Change the seeded administrator password at first sign in and enable two factor authentication from Settings.
- Keep registration closed unless the interface is restricted. An open sign up page on a public address is how a stranger ends up issuing aliases on your domain.
- Publish SPF, DKIM and DMARC exactly as
simplelogin-set-domainprints them. Without them your forwards will be treated as spam. - Set reverse DNS on the public IP to match the mail server's hostname.
- Back up
/var/lib/simplelogin/keyswith the database. A restored database with a new DKIM key will fail authentication for every message. - Enable spam filtering with Spamhaus DQS if you need it. The free public block lists refuse queries from cloud provider resolvers, so they ship disabled rather than silently deferring your mail; the commented block in
/etc/postfix/main.cfshows where the DQS key goes. - Unattended security upgrades are enabled for the host packages, including nginx and Postfix.
Troubleshooting
The web interface does not answer. The public listener is deliberately closed until first boot has finished. Check sudo systemctl status simplelogin-firstboot.service simplelogin-postboot.service and sudo journalctl -u simplelogin-postboot.service.
nginx or Postfix refuses to start. Both run a fail closed guard first. Run sudo /usr/local/sbin/simplelogin-preflight.sh to see exactly which secret it objected to.
Aliases do not receive mail. Check your MX record points at app.your-domain.com, that inbound 25 is allowed in the network security group, and that sudo postmap -q your-domain.com pgsql:/etc/postfix/pgsql-transport-maps.cf returns smtp:127.0.0.1:20381.
Forwards do not arrive. This is almost always Azure's outbound port 25 block. See Step 11.
Mail is going to spam. Publish SPF, DKIM and DMARC, and set reverse DNS on the public IP.
Support
cloudimg provides 24/7 support with a guaranteed 24 hour response SLA for this image. Contact support@cloudimg.co.uk with your Azure subscription ID and the offer name.
For questions about SimpleLogin itself, see the upstream project.