A whole-directory ConfigMap mount puts ..data symlink dirs in --static-dir, which listmonk refuses to read (CrashLoopBackOff). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ansible-role-listmonk
Deploys listmonk v6.2.0, the eom.dev newsletter, to Kubernetes.
It fits in with the rest of eom.dev:
- Login goes through Dex (
listmonkstatic client) with the same "eom.dev account" button as the other services. Only e-mail addresses listed inlistmonk_oidc_adminscan sign in, so being a member is not enough to get into the admin UI. The admin login page sends you straight to Dex. For theadminpassword login, kept as a fallback for when Dex is down, use/admin/login?local=1. - Mail goes out through
postfix.eom.dev:587as the LDAP accountnews, the same way Gitea, Nextcloud and PeerTube send mail. - Look: the public pages (subscribe, archive, preferences, unsubscribe) use the Catppuccin Mocha/Latte palette from
dex.eom.dev, along with the eom.dev logo and favicon. - Cluster conventions:
ca-issuerTLS,nfs-clientvolumes, and anti-affinity that keeps the pods off the ingress-nginx node.
What it creates
| Object | Purpose |
|---|---|
Secret/listmonk |
DB password and bootstrap admin credentials |
StatefulSet/listmonk-db |
PostgreSQL 17 (alpine, uid 70) on nfs-client |
Deployment/listmonk |
Runs --install --idempotent, then --upgrade, then serves on :9000 |
PVC/listmonk-uploads |
Media uploaded for campaigns |
Ingress/listmonk |
https://listmonk.eom.dev |
listmonk keeps most of its configuration in the settings table, not in a config file. The role writes those settings with an idempotent SQL script (templates/configure.sql.j2) that it runs in the database pod. The script covers SMTP, Dex, theme, captcha, privacy, the Dex admin users and the lists in listmonk_lists. On the first run it also removes listmonk's sample data. listmonk is restarted only when a row actually changes.
Settings the role manages are reset on every run. Change them in defaults/main.yml or listmonk_settings_extra, not in the UI. Everything else, including templates, campaigns and subscribers, stays under the UI's control.
Required variables (Vault)
| Variable | Notes |
|---|---|
listmonk_db_password |
16+ chars. Only used when the volume is first initialised |
listmonk_admin_password |
8+ chars, for the admin fallback login |
listmonk_smtp_password |
Password of the news LDAP account |
listmonk_oidc_client_secret |
Must match the listmonk client in Dex |
The eom.dev playbook maps them from the Vault names listmonk_postgresql_password, listmonk_admin_password, listmonk_smtp_password and listmonk_dex_client_secret.
One-time setup outside this role
- In LDAP, create the posix account
newswithmail: news@eom.devand add it tocn=Postfix Users,ou=Postfix,ou=Services,dc=eom,dc=dev. Its password islistmonk_smtp_password. - Add the
listmonkclient to Dex (ericomeehan.dex/playbooks/deploy-eom.yml) and run that play.
E-mail design
Newsletters and listmonk's system e-mails (opt-in confirmation, password reset and so on) use the Catppuccin Mocha palette and the eom.dev logo, matching www.eom.dev.
templates/email/campaign.html.j2produces the eom.dev campaign template, which is the default for new campaigns, and an eom.dev archive variant without unsubscribe links. Both are kept in sync in the database on every run.templates/email/base.html.j2overrides only the shared system e-mail header and footer. It is mounted from a ConfigMap through--static-dir.- The logo is a PNG (
www.eom.dev/static/email-logo.png), because most mail clients don't show SVG. - Set
listmonk_email_postal_addressto add a mailing address to every footer. US anti-spam law (CAN-SPAM) requires one in commercial e-mail.
API users
| User | Can do | Token |
|---|---|---|
membership |
Read and manage subscribers on eom.dev (signup opt-in, profile toggle) |
listmonk_api_token (Vault); same value as LISTMONK_API_TOKEN in the www membership Secret |
newsletter |
Create, edit, test-send and schedule campaigns to eom.dev; no subscriber data or settings |
Generated by the role, stored in Secret/listmonk-newsletter-api (url, username, token) |
Neither user can read settings or other lists. To rotate the newsletter token, delete its Secret and run the play again.
Embedding a signup form
listmonk accepts a plain HTML POST, so any eom.dev page can include a form:
<form method="post" action="https://listmonk.eom.dev/subscription/form">
<input type="hidden" name="nonce">
<input type="email" name="email" required placeholder="you@example.com">
<input type="hidden" name="l" value="LIST-UUID">
<button type="submit">Subscribe</button>
</form>
Copy the list UUID from Lists → eom.dev → Forms in the admin UI. The form uses double opt-in, so subscribers have to confirm by e-mail. The page at https://listmonk.eom.dev/subscription/form adds an Altcha proof-of-work captcha.
Testing
ansible-playbook -i tests/inventory tests/render.yml # renders into .render/
This version was tested against listmonk/listmonk:v6.2.0 and postgres:17.11-alpine. The SQL changes 12 settings, 1 user, 1 list and the sample data on the first run, and nothing on the second.
License
MIT-0