Applications Azure

GNU Mailman 3 on Ubuntu 24.04 on Azure User Guide

| Product: GNU Mailman 3 on Ubuntu 24.04 LTS on Azure

Overview

GNU Mailman 3 is the long-established free-software platform for running email discussion and announcement lists. This cloudimg image ships the whole suite, installed and wired together as a single appliance: Mailman Core (the list engine and its REST API), Postorius (the Django web interface for list administration and subscriber self-service) and HyperKitty (the Django web archive that turns each list into a searchable, threaded history). A local PostgreSQL 16 database backs the web tier, a local Postfix mail server handles inbound and outbound mail, and nginx serves the web UIs on port 80.

Every secret is generated on the first boot of each deployed VM. The Django SECRET_KEY, the PostgreSQL password, the Mailman Core REST API password and the HyperKitty archiver key are all minted per VM, and a single administrator account is created with a password generated on that machine. Two VMs launched from the same image never share a credential. The initial administrator details and the mail domain are written to /root/mailman-credentials.txt with mode 0600, so only root can read them. No list data, no archived mail and no shared secret ship in the image; a demo announce list with a welcome message already archived is created on first boot so Postorius and HyperKitty show real content the moment you sign in.

What is included:

  • GNU Mailman Core 3.3.8 (the mailman3 engine + REST API on loopback :8001, LMTP on :8024)
  • Postorius 1.3.10 (list-admin web UI) and HyperKitty 1.3.7 (web archive), Django 4.2 LTS apps on Python 3.12
  • PostgreSQL 16 for the web tier and Postfix as the mail transport agent, both local
  • nginx serving Postorius, HyperKitty, the Django admin and static assets on :80
  • Per-VM administrator, database, REST API and archiver credentials generated at first boot, in a root-only file
  • A demo announce list and archived welcome message created per VM (never baked into the image)
  • Postfix locked down to refuse unauthenticated relay, proven on first boot
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) runs the full stack comfortably for a small community or team; step up to Standard_B2ms (8 GiB) for a busy server with large archives. NSG inbound: allow 22/tcp from your management network, 80/tcp (plus 443/tcp once you enable HTTPS) from the networks your members will reach the web UI on, and 25/tcp from the internet if this server is to receive mail from external senders.

Step 1: Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for GNU Mailman 3 by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create and Create. First-boot initialisation takes approximately one to two minutes after the VM starts.

Step 2: Deploy from the Azure CLI

Replace the resource group, image reference, VNet and subnet with your own:

az vm create \
  --resource-group <your-rg> \
  --name mailman \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm open-port --resource-group <your-rg> --name mailman --port 80 --priority 1010

Step 3: Connect and Retrieve Initial Credentials

Connect over SSH with the key pair you supplied at deploy time:

ssh azureuser@<vm-public-ip>

The per-VM administrator password, mail domain and web URL are written to a root-only file. Read it with sudo, and confirm the versions you are running:

dpkg-query -W -f='Mailman Core ${Version}\n' mailman3
dpkg-query -W -f='Postorius ${Version}\n' python3-django-postorius
dpkg-query -W -f='HyperKitty ${Version}\n' python3-django-hyperkitty
sudo cat /root/mailman-credentials.txt

The file lists the administrator username and password, the administrator email, the mail domain for your lists, and the URL to reach the web interface. Keep these somewhere safe.

The installed Mailman Core, Postorius and HyperKitty versions and the per-VM administrator credentials, generated on first boot and masked here

Step 4: Confirm the Services Are Running

The appliance runs five services. Confirm they are all active, that Mailman Core's REST API and LMTP endpoints are bound to loopback only, and that the web UIs answer:

systemctl is-active postgresql mailman3 mailman3-web postfix nginx
ss -tln | grep -E '127.0.0.1:8001|127.0.0.1:8024|:80 |:25 '
curl -s -o /dev/null -w 'postorius %{http_code}\n' http://127.0.0.1/postorius/lists/
curl -s -o /dev/null -w 'hyperkitty %{http_code}\n' http://127.0.0.1/hyperkitty/

All five units report active, Mailman Core's REST API (8001) and LMTP (8024) listen on 127.0.0.1 only, nginx serves the web UIs on :80 and Postfix listens on :25, and both Postorius and HyperKitty return 200.

Service status, loopback-only Core endpoints and the Postorius and HyperKitty health checks on a freshly deployed VM

Step 5: First Login to the Web Interface

Open a web browser and navigate to http://<vm-public-ip>/. You are taken to the Postorius list index. Select Sign In and enter the administrator username (admin) and the password from /root/mailman-credentials.txt. The same account is a Django superuser, so it can also reach the Django admin at http://<vm-public-ip>/admin/.

The Postorius sign-in page, served on first boot with a per-VM administrator account and no manual setup

For a production deployment, change the administrator password that was generated on first boot once you have signed in.

Step 6: Browse and Manage Lists in Postorius

The Postorius list index shows every advertised list. A freshly deployed VM already has the demo Announce list, so you can see a real list rather than an empty page.

The Postorius list index showing the demo announce list created on first boot

Signed in as the administrator you get the full admin navigation (Domains, Users, System Information) and, on each list, a Settings page covering message acceptance, member policy, DMARC mitigations, archiving, digests and bounce processing.

The authenticated list settings page in Postorius, reachable only by the per-VM administrator

You can inspect and manage lists from the command line too. These commands read your mail domain from the credentials file, so they work unchanged on any deployed VM:

DOMAIN=$(sudo grep '^MAILMAN_MAIL_DOMAIN=' /root/mailman-credentials.txt | cut -d= -f2-)
sudo runuser -u list -- mailman lists
sudo runuser -u list -- mailman members "announce@${DOMAIN}"

mailman lists shows the demo announce list and its posting address; mailman members shows that the administrator is subscribed to it.

The mailman command line showing the demo list, its domain and its members

To create your own list, use Postorius (Lists → Create a new list) or the command line. Creating a list on a new mail domain first requires the domain to exist; add it under Domains → Add Domain in Postorius, or via the REST API shown in Step 8.

Step 7: Read the Archive in HyperKitty

HyperKitty renders each list as a searchable, threaded web archive. Open http://<vm-public-ip>/hyperkitty/ and choose the Announce list: the welcome message posted on first boot is already archived, so the archive shows a real thread on a freshly deployed VM.

The HyperKitty archive of the announce list, showing the welcome thread archived on first boot

A message reaches the archive by the same path a real post takes: Postfix receives it, hands it to Mailman Core over LMTP, Core distributes it to subscribers and pushes a copy into HyperKitty, and HyperKitty's background worker indexes it for search.

Step 8: The Mailman Core REST API

Mailman Core exposes an administrative REST API on 127.0.0.1:8001. The per-VM password lives in /etc/mailman3/mailman.cfg. This round-trip lists the mail domains and the demo list, and shows that a message post is archived end to end and that unauthenticated external relay is refused:

sudo /usr/local/sbin/mailman-selftest.sh

The selftest authenticates to the Core REST API with the per-VM password, logs the administrator into a protected admin page, posts a message to the demo list and confirms it appears in the HyperKitty archive, and confirms an unauthenticated relay to an external domain is refused. It prints SELFTEST OK on success.

The end-to-end selftest: Core REST auth, superuser login, a post archived in HyperKitty, and external relay refused

To call the API directly, read the password and query it:

RESTPASS=$(sudo grep '^admin_pass:' /etc/mailman3/mailman.cfg | awk '{print $2}')
curl -s -u "restadmin:${RESTPASS}" http://127.0.0.1:8001/3.0/domains | python3 -m json.tool | head -20

Step 9: Sending Mail and the Azure Outbound Port 25 Block

This appliance receives mail, runs its lists and archives locally without any external connectivity. Sending mail to external recipients is different on Azure. Microsoft blocks outbound TCP port 25 from Azure VMs on most subscription types as an anti-abuse measure; this is an Azure platform policy, not a limitation of this image. With outbound 25 blocked, list mail addressed to external subscribers will not leave the VM directly.

There are two supported paths to real outbound delivery:

  1. Relay through an authenticated smart host (recommended). Point Postfix at your provider's submission service on port 587. Edit /etc/postfix/main.cf:

text relayhost = [smtp.your-provider.com]:587 smtp_sasl_auth_enable = yes smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd smtp_sasl_security_options = noanonymous smtp_tls_security_level = encrypt

Create /etc/postfix/sasl_passwd containing [smtp.your-provider.com]:587 user@example.com:password, then run sudo postmap /etc/postfix/sasl_passwd and sudo systemctl reload postfix.

  1. Request an Azure outbound port 25 exemption through Azure support, if your subscription is eligible.

Inbound mail on port 25, all web administration, and the local post-to-archive round trip are unaffected either way. The appliance is deliberately not an open relay: Postfix accepts mail for its own list domains and refuses to relay to external destinations for unauthenticated senders. You can see the refusal directly:

DOMAIN=$(sudo grep '^MAILMAN_MAIL_DOMAIN=' /root/mailman-credentials.txt | cut -d= -f2-)
PRIVIP=$(hostname -I | awk '{print $1}')
printf 'EHLO probe\r\nMAIL FROM:<a@ext-a.example>\r\nRCPT TO:<b@ext-b.example>\r\nQUIT\r\n' | nc -w 6 "${PRIVIP}" 25

The server accepts the sender but answers the external recipient with 554 5.7.1 <b@ext-b.example>: Relay access denied.

Step 10: Add Members and Users

Subscribers self-serve through Postorius: they sign up at http://<vm-public-ip>/accounts/signup/, confirm their address, then subscribe to any advertised list from its page. As administrator you can add and remove members from a list's Users menu in Postorius, set each list's subscription and posting policy under Settings → Member Policy, and moderate held messages from the Held messages tab.

Step 11: Services and Operations

The appliance is managed with systemctl. The Core engine is mailman3, the web tier (Postorius + HyperKitty under uwsgi, with the HyperKitty background worker) is mailman3-web, and the mail transport is postfix:

systemctl status mailman3 --no-pager | head -5
sudo journalctl -u mailman3-web -n 20 --no-pager

Configuration lives under /etc/mailman3/ (mailman.cfg for Core, mailman-web.py for the Django site, mailman-hyperkitty.cfg for the archiver). List and archive data live in the mailman3web PostgreSQL database and under /var/lib/mailman3/.

Step 12: Enable HTTPS with Let's Encrypt

Put a DNS name in front of the VM and obtain a certificate with Certbot. With a real hostname pointing at the public IP:

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d lists.example.com --redirect --agree-tos -m you@example.com

Certbot edits the nginx site in place and installs a renewal timer. After enabling HTTPS, add your hostname to ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS in /etc/mailman3/mailman-web.py and reload with sudo systemctl restart mailman3-web.

Step 13: Backups and Maintenance

Back up the PostgreSQL web database and the Mailman Core data directory together, so the lists, subscribers and archive stay consistent:

sudo -u postgres pg_dump mailman3web | gzip > mailman3web-$(date +%F).sql.gz
sudo tar czf mailman-var-$(date +%F).tar.gz /var/lib/mailman3

The base image is fully patched and has unattended security updates enabled, so the operating system keeps itself current. Upgrade the Mailman packages with the distribution's normal apt-get update && apt-get upgrade cycle.

Server Components

Component Version Purpose
Mailman Core 3.3.8 List engine, REST API (:8001), LMTP (:8024)
Postorius 1.3.10 List administration and subscriber web UI
HyperKitty 1.3.7 Searchable web archive
PostgreSQL 16 Web-tier database (mailman3web)
Postfix 3.8 Mail transport agent (relay locked down)
nginx 1.24 Reverse proxy and static server on :80

Scripts and Log Files

Path Purpose
/root/mailman-credentials.txt Per-VM administrator credentials, mail domain and web URL (mode 0600)
/usr/local/sbin/mailman-firstboot.sh First-boot bootstrap (secrets, DB, demo list)
/usr/local/sbin/mailman-selftest.sh End-to-end credential, mail-flow and relay-lockdown check
/var/log/cloudimg-firstboot.log First-boot log
/var/log/mailman3/ Mailman Core and web logs

Support

Every cloudimg image comes with a deploy guide tested against the exact build and 24/7 support. If you hit anything this guide does not cover, contact cloudimg support.