diff --git a/README.md b/README.md
index 8a4d4517..012176fc 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,6 @@ this at https://github.com/cpanel/mailman2-python3 which you can/should
also review.
This is being used in production today.
+
+An installer for Ubuntu 26.04 (which should be translatable to other modern
+Linux systems) is available in the doc/install-ubuntu/ directory.
diff --git a/doc/install-ubuntu/04_exim4-config_mailman b/doc/install-ubuntu/04_exim4-config_mailman
new file mode 100644
index 00000000..a21aafd7
--- /dev/null
+++ b/doc/install-ubuntu/04_exim4-config_mailman
@@ -0,0 +1,31 @@
+# start
+# Home dir for your Mailman installation -- aka Mailman's prefix
+# directory.
+# By default this is set to "/usr/local/mailman"
+# On a Red Hat/Fedora system using the RPM use "/var/mailman"
+# On Debian using the deb package use "/var/lib/mailman"
+# This is normally the same as ~mailman
+MM_HOME=/var/lib/mailman
+#
+# User and group for Mailman, should match your --with-mail-gid
+# switch to Mailman's configure script. Value is normally "mailman"
+MM_UID=Debian-exim
+MM_GID=Debian-exim
+#
+# Domains that your lists are in - colon separated list
+# you may wish to add these into local_domains as well
+domainlist mm_domains=lists.example.com
+#
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+#
+# These values are derived from the ones above and should not need
+# editing unless you have munged your mailman installation
+#
+# The path of the Mailman mail wrapper script
+MM_WRAP=MM_HOME/mail/mailman
+#
+# The path of the list config file (used as a required file when
+# verifying list addresses)
+MM_LISTCHK=MM_HOME/lists/${lc::$local_part_data}/config.pck
+# end
+
diff --git a/doc/install-ubuntu/101_exim4-config_mailman b/doc/install-ubuntu/101_exim4-config_mailman
new file mode 100644
index 00000000..08ed517d
--- /dev/null
+++ b/doc/install-ubuntu/101_exim4-config_mailman
@@ -0,0 +1,16 @@
+# Note:
+# $local_part_data only gets set after $local_part has undergone some sort of lookup.
+# The local_parts line uses $local_part to do a directory lookup in MM_HOME/lists, and returns the matching item.
+# It then checks the local part against that value, which obviously it matches if the directory exists, and doesn't if it doesn't.
+# If it does match, $local_part_data gets set.
+
+mailman_router:
+ driver = accept
+ local_parts = ${lookup {$local_part} dsearch,filter=dir {MM_HOME/lists}}
+ require_files = MM_HOME/lists/${local_part_data}/config.pck
+ local_part_suffix_optional
+ local_part_suffix = -bounces : -bounces+* : \
+ -confirm+* : -join : -leave : \
+ -owner : -request : -admin
+ domains = +mm_domains
+ transport = mailman_transport
diff --git a/doc/install-ubuntu/40_exim4-config_mailman b/doc/install-ubuntu/40_exim4-config_mailman
new file mode 100644
index 00000000..60b697e2
--- /dev/null
+++ b/doc/install-ubuntu/40_exim4-config_mailman
@@ -0,0 +1,12 @@
+mailman_transport:
+ driver = pipe
+ command = MM_WRAP \
+ '${if def:local_part_suffix \
+ {${sg{$local_part_suffix}{-(\\w+)(\\+.*)?}{\$1}}} \
+ {post}}' \
+ $local_part_data
+ current_directory = MM_HOME
+ home_directory = MM_HOME
+ user = MM_UID
+ group = MM_GID
+
diff --git a/doc/install-ubuntu/README.md b/doc/install-ubuntu/README.md
new file mode 100644
index 00000000..844cffa1
--- /dev/null
+++ b/doc/install-ubuntu/README.md
@@ -0,0 +1,11 @@
+# Installation script for Mailman 2 (on Python 3) on Ubuntu 26.04
+
+This directory contains an install script that will create a fully-working Mailman 2 (on Python 3) instance on a fresh Ubuntu 26.04 LTS machine.
+
+Usage (as root) - quote the list name and main e-mail address as arguments:
+
+```
+./install-mailman.sh lists.example.com webmaster@example.com
+```
+
+Script license: Public domain / CC0 (Creative Commons Zero).
diff --git a/doc/install-ubuntu/install-mailman.sh b/doc/install-ubuntu/install-mailman.sh
new file mode 100755
index 00000000..e17059c4
--- /dev/null
+++ b/doc/install-ubuntu/install-mailman.sh
@@ -0,0 +1,183 @@
+#!/bin/bash
+
+
+# Script to install Mailman 2 running on Python 3 on Ubuntu 26.04
+
+# Usage example:
+# ./install-mailman.sh lists.example.com webmaster@example.com
+
+# Uses mailman2-python3 fork at:
+# https://github.com/jaredmauch/mailman2-python3
+
+# Installation documentation (for older Mailman2/Python2) at:
+# https://www.gnu.org/software/mailman/mailman-install.pdf
+# Useful notes (for older Mailman2/Python2 Ubuntu package) at:
+# https://help.ubuntu.com/community/Mailman
+
+# NB You must ensure DNS hostname is set to the machine to ensure succesful SSL certificate creation
+
+
+# Stop on error
+set -e
+
+# Ensure this script is run as root
+if [ "$(id -u)" != "0" ]; then
+ echo "This script must be run as root." 1>&2
+ exit 1
+fi
+
+# Require hostname and e-mail arguments
+display_usage() {
+ printf "Usage:\n./install-mailman.sh lists.example.com webmaster@example.com\n"
+}
+if [ $# -le 1 ] # If less than two arguments supplied, display usage
+then
+ display_usage
+ exit 1
+fi
+hostname=$1
+email=$2
+
+# Update/patch machine
+apt-get update
+apt-get -y upgrade
+apt-get -y dist-upgrade
+apt-get -y autoremove
+
+# 1 Installation Requirements: Install Python3
+apt-get install -y build-essential
+gcc --version
+apt-get install python3
+
+# 2.1 Add the group and user
+groupadd -f mailman
+id -u mailman &>/dev/null || useradd -c"GNU Mailman" -s /usr/sbin/nologin --no-create-home -g mailman mailman
+
+# Mailserver installation
+apt-get -y install exim4
+usermod -a -G Debian-exim mailman
+
+# 2.2 Create the directory where the installation will be created
+# NB If you use a directory such as /usr/local/mailman/ , you will need to logs and other directories to avoid AppArmor restrictions causing "OSError: [Errno 30] Read-only file system: '/usr/local/mailman/logs/error'"
+# See: https://github.com/jaredmauch/mailman2-python3/issues/23 and https://linux-audit.com/systemd/settings/units/protectsystem/
+prefix=/var/lib/mailman
+mkdir -p $prefix
+chgrp -R mailman $prefix
+chmod -R a+rx,g+ws $prefix
+
+# Obtain distribution
+# See: https://github.com/jaredmauch/mailman2-python3/
+installDir=/tmp/mailman2-python3/
+if [ ! -d $installDir ]; then
+ apt-get install -y git
+ git clone https://github.com/jaredmauch/mailman2-python3.git $installDir
+fi
+chown -R mailman $installDir
+
+# 3 Build and install Mailman
+# 3.1 Run configure
+# Build, first adding build/runtime dependencies
+apt-get install -y python3-dnspython python3-pip python3-legacy-cgi python3-html2text gettext python3-bsddb3
+# NB The following can be added, but it will be more self-explanatory to add these in $prefix/Mailman/mm_cfg.py: " --with-mailhost=$hostname --with-urlhost=$hostname"
+cd $installDir
+sudo -H -u mailman bash -c "./configure --prefix=$prefix --with-mail-gid=Debian-exim --with-cgi-gid=www-data"
+
+# 3.2 Make and install
+sudo -H -u mailman bash -c 'make'
+if [ ! -d "$prefix/Mailman/" ]; then
+ sudo -H -u mailman bash -c 'make install'
+fi
+cd -
+
+# 4 Check your installation
+cd $prefix
+sudo -H -u mailman bash -c "bin/check_perms -f"
+cd -
+
+# 7 Review your site defaults
+# Add in config
+cp ./mm_cfg.py $prefix/Mailman/mm_cfg.py
+chown mailman:mailman $prefix/Mailman/mm_cfg.py
+sed -i "s/lists.example.com/${hostname}/g" $prefix/Mailman/mm_cfg.py
+
+# 5 Set up your web server
+apt-get install -y apache2
+a2enmod rewrite cgid
+
+# Add HTTP VirtualHost
+cp -pr ./lists.conf /etc/apache2/sites-available/
+sed -i "s/lists.example.com/${hostname}/g" /etc/apache2/sites-available/lists.conf
+sed -i "s/webmaster@example.com/${email}/g" /etc/apache2/sites-available/lists.conf
+mkdir -p /var/www/lists/
+a2ensite lists
+service apache2 restart
+
+# Create SSL certificate, and enable the HTTPS (SSL) VirtualHost using this newly-created certificate
+set +e # Allow this section to fail
+apt-get install -y certbot
+a2enmod ssl
+certbot --agree-tos --no-eff-email certonly --keep-until-expiring --webroot -w /var/www/lists/ --email $email -d $hostname
+if [ -f "/etc/letsencrypt/live/${hostname}/fullchain.pem" ]; then
+ sed -i "s/##//g" /etc/apache2/sites-available/lists.conf # Uncomment the ## lines from the template
+fi
+service apache2 restart
+set -e # Revert to stop on fail
+
+# 6 Set up your mail server
+# 6.2 Using the Exim mail server
+# See: https://help.ubuntu.com/community/Mailman#Exim4_Configuration
+# NB This uses split configuration
+# Copy in Mailman files for Exim4
+cp -pr ./04_exim4-config_mailman /etc/exim4/conf.d/main/
+sed -i "s/lists.example.com/${hostname}/g" /etc/exim4/conf.d/main/04_exim4-config_mailman
+cp -pr ./40_exim4-config_mailman /etc/exim4/conf.d/transport/
+cp -pr ./101_exim4-config_mailman /etc/exim4/conf.d/router/
+# Set dc_use_split_config to true, and ensure dc_other_hostnames has the new listserver hostname
+sed -i -r "s/dc_use_split_config.+/dc_use_split_config='true'/" /etc/exim4/update-exim4.conf.conf
+# Add hostname to dc_other_hostnames
+if [ $(cat /etc/exim4/update-exim4.conf.conf | grep -c "${hostname}") -eq 0 ]; then
+ sed -i -E "s/dc_other_hostnames='([^']+)'/dc_other_hostnames='\1:${hostname}'/" /etc/exim4/update-exim4.conf.conf
+fi
+update-exim4.conf
+service exim4 restart
+exim -bP '+local_domains' # Verify config - should show the new listserver hostname
+
+# Set /etc/mailname (may not be necessary, and possibly not desirable if the machine has other mail functions)
+#echo $domain > /etc/mailname
+
+# 8 Create a site-wide mailing list
+apt-get install -y pwgen
+if [ ! -d $prefix/lists/mailman ]; then
+ AUTOGENERATED_PASS=`pwgen -c -1 20`
+ echo "Site password is: $AUTOGENERATED_PASS"
+ $prefix/bin/newlist -q mailman $email $AUTOGENERATED_PASS
+fi
+
+# 9 Set up cron; will create entry at /var/spool/cron/crontabs/mailman
+crontab -u mailman $prefix/cron/crontab.in
+
+# Create service
+cp ./mailman.service /etc/systemd/system/mailman.service
+chown root:root /etc/systemd/system/mailman.service
+systemctl daemon-reload
+systemctl enable mailman.service
+
+# 10 Start the Mailman qrunner
+#$prefix/bin/mailmanctl start # Manual start
+systemctl start mailman.service
+
+# Confirm success
+echo "Mailman installation complete!"
+echo "Mailman is now running."
+echo "The site password is shown above."
+echo "You can monitor the Mailman error log at: $prefix/logs/error"
+
+# Migration notes
+echo ""
+echo "If you have existing list archives, they should be copied in to $prefix/archives/"
+echo "Then set ownership of that directory throughout to mailman:mailman"
+echo ""
+echo "To migrate list configs from an old server, export and import using:"
+echo "old-server$ config_list -o foo oldlistname"
+echo "new-server$ config_list -i foo newlistname"
+
diff --git a/doc/install-ubuntu/lists.conf b/doc/install-ubuntu/lists.conf
new file mode 100644
index 00000000..1c9d1f3e
--- /dev/null
+++ b/doc/install-ubuntu/lists.conf
@@ -0,0 +1,85 @@
+# Config for Mailman lists site
+
+# HTTP host - redirect to SSL host
+
+ ServerName lists.example.com
+ DocumentRoot /var/www/lists/
+ CustomLog /var/log/apache2/lists.example.com_http-access.log combined
+ ErrorLog /var/log/apache2/lists.example.com_http-error.log
+## RedirectMatch Permanent ^/(?!.well-known)(.*)$ https://lists.example.com/$1
+
+
+# SSL host
+##
+## ServerName lists.example.com
+## ServerAdmin webmaster@example.com
+## DocumentRoot /var/www/lists/
+## CustomLog /var/log/apache2/lists.example.com-access.log combined
+## ErrorLog /var/log/apache2/lists.example.com-error.log
+## LogLevel warn
+## ServerSignature On
+##
+## # Enable SSL
+## SSLEngine on
+## SSLCertificateFile /etc/letsencrypt/live/lists.example.com/fullchain.pem
+## SSLCertificateKeyFile /etc/letsencrypt/live/lists.example.com/privkey.pem
+##
+##
+## Options FollowSymLinks
+## AllowOverride None
+##
+##
+## Options Indexes FollowSymLinks MultiViews
+## AllowOverride None
+## Require all granted
+##
+##
+## AcceptPathInfo On
+##
+## Alias /pipermail/ /var/lib/mailman/archives/public/
+## Alias /icons/ /var/lib/mailman/icons/
+## ScriptAlias /admin /var/lib/mailman/cgi-bin/admin
+## ScriptAlias /admindb /var/lib/mailman/cgi-bin/admindb
+## ScriptAlias /confirm /var/lib/mailman/cgi-bin/confirm
+## ScriptAlias /create /var/lib/mailman/cgi-bin/create
+## ScriptAlias /edithtml /var/lib/mailman/cgi-bin/edithtml
+## ScriptAlias /listinfo /var/lib/mailman/cgi-bin/listinfo
+## ScriptAlias /options /var/lib/mailman/cgi-bin/options
+## ScriptAlias /private /var/lib/mailman/cgi-bin/private
+## ScriptAlias /rmlist /var/lib/mailman/cgi-bin/rmlist
+## ScriptAlias /roster /var/lib/mailman/cgi-bin/roster
+## ScriptAlias /subscribe /var/lib/mailman/cgi-bin/subscribe
+## ScriptAlias /mailman/ /var/lib/mailman/cgi-bin/
+##
+##
+## AllowOverride None
+## Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
+## Require all granted
+##
+##
+## Options Indexes FollowSymlinks
+## AllowOverride None
+## Require all granted
+##
+##
+## AllowOverride None
+## Require all granted
+##
+##
+## # Redirect to front page
+## RedirectMatch Permanent ^/$ https://lists.example.com/mailman/admin
+##
+## # Use /mailman/private instead of /private if wished
+## #Redirect /private/ /mailman/private/
+##
+## # For a private installation, add additional HTTP auth if wished
+## #
+## # AuthType Basic
+## # AuthName "You need to be authenticated"
+## # AuthBasicProvider file
+## # AuthUserFile /etc/apache2/.htpasswd
+## # Require valid-user
+## #
+##
+##
+
diff --git a/doc/install-ubuntu/mailman.service b/doc/install-ubuntu/mailman.service
new file mode 100644
index 00000000..118f0954
--- /dev/null
+++ b/doc/install-ubuntu/mailman.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=Mailman Master Queue Runner
+After=network.target
+
+[Service]
+Type=forking
+PIDFile=/var/lib/mailman/data/master-qrunner.pid
+ExecStart=/var/lib/mailman/bin/mailmanctl -s start
+ExecStop=/var/lib/mailman/bin/mailmanctl stop
+ExecReload=/var/lib/mailman/bin/mailmanctl restart
+Restart=always
+RestartSec=3s
+RestartPreventExitStatus=1
+
+[Install]
+WantedBy=multi-user.target
+Alias=mailman-qrunner.service
diff --git a/doc/install-ubuntu/mm_cfg.py b/doc/install-ubuntu/mm_cfg.py
new file mode 100644
index 00000000..1d4386dc
--- /dev/null
+++ b/doc/install-ubuntu/mm_cfg.py
@@ -0,0 +1,83 @@
+# -*- python -*-
+
+# Copyright (C) 1998,1999,2000 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+
+"""This is the module which takes your site-specific settings.
+
+From a raw distribution it should be copied to mm_cfg.py. If you
+already have an mm_cfg.py, be careful to add in only the new settings
+you want. The complete set of distributed defaults, with annotation,
+are in ./Defaults. In mm_cfg, override only those you want to
+change, after the
+
+ from Defaults import *
+
+line (see below).
+
+Note that these are just default settings - many can be overridden via the
+admin and user interfaces on a per-list or per-user basis.
+
+Note also that some of the settings are resolved against the active list
+setting by using the value as a format string against the
+list-instance-object's dictionary - see the distributed value of
+DEFAULT_MSG_FOOTER for an example."""
+
+
+#######################################################
+# Here's where we get the distributed defaults. #
+from Mailman.Defaults import *
+
+##############################################################
+# Put YOUR site-specific configuration below, in mm_cfg.py . #
+# See Defaults.py for explanations of the values. #
+
+#-------------------------------------------------------------
+# If you change these, you have to configure your http server
+# accordingly (Alias and ScriptAlias directives in most httpds)
+DEFAULT_URL_PATTERN = 'https://%s/'
+
+#-------------------------------------------------------------
+# Default domain for email addresses of newly created MLs
+DEFAULT_EMAIL_HOST = 'lists.example.com'
+#-------------------------------------------------------------
+# Default host for web interface of newly created MLs
+DEFAULT_URL_HOST = 'lists.example.com'
+#-------------------------------------------------------------
+# Required when setting any of its arguments.
+add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST)
+
+#-------------------------------------------------------------
+# Unset send_reminders on newly created lists
+DEFAULT_SEND_REMINDERS = 0
+
+#-------------------------------------------------------------
+# Uncomment this if you configured your MTA such that it
+# automatically recognizes newly created lists.
+# (see /usr/share/doc/mailman/README.Exim4.Debian or
+# /usr/share/mailman/postfix-to-mailman.py)
+MTA=None # Misnomer, suppresses alias output on newlist
+
+#-------------------------------------------------------------
+# Uncomment if you want to filter mail with SpamAssassin. For
+# more information please visit this website:
+# http://www.jamesh.id.au/articles/mailman-spamassassin/
+# GLOBAL_PIPELINE.insert(1, 'SpamAssassin')
+
+# Note - if you're looking for something that is imported from mm_cfg, but you
+# didn't find it above, it's probably in /usr/lib/mailman/Mailman/Defaults.py.