Add Listmonk, Mattermost manifests; Ghost SMTP and device verification fix

- Listmonk: newsletter/mailing list manager with PostgreSQL backend,
  NodePort 32375, Postmark SMTP. Replaces Ghost's broken Mailgun-only
  newsletter sending via n8n automation pipeline.
- Mattermost: team messaging manifest, NodePort 32374, PostgreSQL backend.
- Ghost: added Postmark SMTP config for transactional email, disabled
  staffDeviceVerification on all three instances (Ghost has no TOTP,
  only email-based verification which requires working SMTP).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samantha Atkins 2026-04-11 18:07:35 -04:00
parent 759ef949bc
commit 8cf5640757
5 changed files with 346 additions and 0 deletions

View file

@ -64,6 +64,28 @@ spec:
value: ghost1_db value: ghost1_db
- name: url - name: url
value: https://blog.the-fulfillment.org value: https://blog.the-fulfillment.org
- name: security__staffDeviceVerification
value: "false"
- name: mail__transport
value: SMTP
- name: mail__from
value: "Sister Sam <samantha@the-fulfillment.org>"
- name: mail__options__host
value: smtp.postmarkapp.com
- name: mail__options__port
value: "587"
- name: mail__options__auth__user
valueFrom:
secretKeyRef:
name: ghost-secrets
key: postmark-token
- name: mail__options__auth__pass
valueFrom:
secretKeyRef:
name: ghost-secrets
key: postmark-token
- name: bulk_email__provider
value: smtp
ports: ports:
- containerPort: 2368 - containerPort: 2368
volumeMounts: volumeMounts:
@ -139,6 +161,8 @@ spec:
value: ghost2_db value: ghost2_db
- name: url - name: url
value: https://blog.privacy-practice.com value: https://blog.privacy-practice.com
- name: security__staffDeviceVerification
value: "false"
- name: server__port - name: server__port
value: "2369" value: "2369"
ports: ports:
@ -216,6 +240,8 @@ spec:
value: ghost3_db value: ghost3_db
- name: url - name: url
value: https://blog.sjasoft.com value: https://blog.sjasoft.com
- name: security__staffDeviceVerification
value: "false"
- name: server__port - name: server__port
value: "2370" value: "2370"
ports: ports:

View file

@ -0,0 +1,45 @@
# Listmonk DB Init Job
# Creates listmonk database and user in PostgreSQL.
# Run once before deploying Listmonk.
#
# Deploy:
# kubectl create secret generic listmonk-secret \
# --from-literal=db-password='<password>' \
# --from-literal=admin-password='<password>' \
# --from-literal=postmark-token='<token>'
# kubectl apply -f listmonk-db-init.yaml
#
# Watch completion:
# kubectl get jobs -w
# kubectl logs job/listmonk-db-init
apiVersion: batch/v1
kind: Job
metadata:
name: listmonk-db-init
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: listmonk-db-init
image: postgres:16
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: LISTMONK_DB_PASSWORD
valueFrom:
secretKeyRef:
name: listmonk-secret
key: db-password
command:
- /bin/sh
- -c
- |
psql -h postgres -U postgres <<EOF
CREATE USER listmonk_user WITH PASSWORD '${LISTMONK_DB_PASSWORD}';
CREATE DATABASE listmonk_db OWNER listmonk_user;
EOF

134
k3s/listmonk/listmonk.yaml Normal file
View file

@ -0,0 +1,134 @@
# Listmonk — self-hosted newsletter and mailing list manager
# PostgreSQL backend via cluster DNS: postgres
# SMTP via Postmark
# Unpinned — scheduler places freely
# NodePort 32375
#
# Deploy:
# kubectl create secret generic listmonk-secret \
# --from-literal=db-password='<password>' \
# --from-literal=admin-password='<password>' \
# --from-literal=postmark-token='<token>'
# kubectl apply -f listmonk-db-init.yaml
# kubectl get jobs -w # wait for completion
# kubectl apply -f listmonk.yaml
#
# First run: Listmonk auto-runs DB migrations on startup.
# Admin UI: http://<any-node-wg-ip>:32375
#
# n8n integration: Ghost webhook -> n8n -> Listmonk API
# POST /api/campaigns to create campaign from Ghost post
# PUT /api/campaigns/{id}/status to trigger send
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: listmonk-uploads-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: listmonk-config
data:
config.toml: |
[app]
address = "0.0.0.0:9000"
admin_username = "admin"
admin_password = ""
[db]
host = "postgres"
port = 5432
user = "listmonk_user"
password = ""
database = "listmonk_db"
ssl_mode = "disable"
max_open = 25
max_idle = 25
max_lifetime = "300s"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: listmonk
spec:
replicas: 1
selector:
matchLabels:
app: listmonk
template:
metadata:
labels:
app: listmonk
spec:
containers:
- name: listmonk
image: listmonk/listmonk:latest
command: ["./listmonk", "--config", "/etc/listmonk/config.toml"]
env:
- name: LISTMONK_app__admin_password
valueFrom:
secretKeyRef:
name: listmonk-secret
key: admin-password
- name: LISTMONK_db__password
valueFrom:
secretKeyRef:
name: listmonk-secret
key: db-password
ports:
- containerPort: 9000
volumeMounts:
- name: config
mountPath: /etc/listmonk
- name: uploads
mountPath: /listmonk/uploads
initContainers:
- name: listmonk-install
image: listmonk/listmonk:latest
command: ["./listmonk", "--install", "--idempotent", "--yes", "--config", "/etc/listmonk/config.toml"]
env:
- name: LISTMONK_app__admin_password
valueFrom:
secretKeyRef:
name: listmonk-secret
key: admin-password
- name: LISTMONK_db__password
valueFrom:
secretKeyRef:
name: listmonk-secret
key: db-password
volumeMounts:
- name: config
mountPath: /etc/listmonk
volumes:
- name: config
configMap:
name: listmonk-config
- name: uploads
persistentVolumeClaim:
claimName: listmonk-uploads-pvc
---
apiVersion: v1
kind: Service
metadata:
name: listmonk
spec:
selector:
app: listmonk
ports:
- port: 9000
targetPort: 9000
nodePort: 32375
type: NodePort

View file

@ -0,0 +1,43 @@
# Mattermost DB Init Job
# Creates mattermost database and user in PostgreSQL.
# Run once before deploying Mattermost.
#
# Deploy:
# kubectl create secret generic mattermost-secret \
# --from-literal=db-password='<password>'
# kubectl apply -f mattermost-db-init.yaml
#
# Watch completion:
# kubectl get jobs -w
# kubectl logs job/mattermost-db-init
apiVersion: batch/v1
kind: Job
metadata:
name: mattermost-db-init
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: mattermost-db-init
image: postgres:16
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: MATTERMOST_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mattermost-secret
key: db-password
command:
- /bin/sh
- -c
- |
psql -h postgres -U postgres <<EOF
CREATE USER mattermost_user WITH PASSWORD '${MATTERMOST_DB_PASSWORD}';
CREATE DATABASE mattermost_db OWNER mattermost_user;
EOF

View file

@ -0,0 +1,98 @@
# Mattermost — team messaging
# PostgreSQL backend via cluster DNS: postgres
# Unpinned — scheduler places freely, local-path PVC
# NodePort 32374
#
# Deploy:
# kubectl create secret generic mattermost-secret \
# --from-literal=db-password='<password>'
# kubectl apply -f mattermost-db-init.yaml
# kubectl get jobs -w # wait for completion
# kubectl apply -f mattermost.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mattermost-data-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mattermost-plugins-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mattermost
spec:
replicas: 1
selector:
matchLabels:
app: mattermost
template:
metadata:
labels:
app: mattermost
spec:
containers:
- name: mattermost
image: mattermost/mattermost-team-edition:10
env:
- name: MM_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mattermost-secret
key: db-password
- name: MM_SQLSETTINGS_DRIVERNAME
value: postgres
- name: MM_SQLSETTINGS_DATASOURCE
value: "postgres://mattermost_user:$(MM_DB_PASSWORD)@postgres:5432/mattermost_db?sslmode=disable&connect_timeout=10"
- name: MM_SERVICESETTINGS_SITEURL
value: https://chat.the-fulfillment.org
- name: MM_SERVICESETTINGS_LISTENADDRESS
value: ":8065"
ports:
- containerPort: 8065
volumeMounts:
- name: mattermost-data
mountPath: /mattermost/data
- name: mattermost-plugins
mountPath: /mattermost/plugins
volumes:
- name: mattermost-data
persistentVolumeClaim:
claimName: mattermost-data-pvc
- name: mattermost-plugins
persistentVolumeClaim:
claimName: mattermost-plugins-pvc
---
apiVersion: v1
kind: Service
metadata:
name: mattermost
spec:
selector:
app: mattermost
ports:
- port: 8065
targetPort: 8065
nodePort: 32374
type: NodePort