Discourse: move from Bitnami chart to launcher-built image

The Bitnami chart and images stop at 3.5.0. Run Discourse 2026.10.0 from
an image built with the official launcher, on PostgreSQL 18, reusing the
existing uploads volume, Redis and secrets.

Update the Dex sign-in step for the new image paths, drop the removed
LDAP setting, and keep chat off now that it is bundled in core. The image
adds a real-IP rule so visitors are not all rate limited as the ingress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 15:32:46 -05:00
co-authored by Claude Opus 5.5
parent 507b2225b1
commit e91586b968
5 changed files with 266 additions and 83 deletions
+107
View File
@@ -0,0 +1,107 @@
# Discourse v2026.10.0, built with the official launcher (see web_only.yml).
# Reuses the existing Bitnami-era uploads volume, Redis and Kubernetes secrets.
apiVersion: apps/v1
kind: Deployment
metadata:
name: discourse-web
namespace: discourse
labels:
app.kubernetes.io/name: discourse-web
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: discourse-web
template:
metadata:
labels:
app.kubernetes.io/name: discourse-web
spec:
# The image is 3.77 GB and most nodes have only 3-8 GB free; pulling it
# elsewhere risks DiskPressure and evicting the MetalLB speakers.
nodeSelector:
kubernetes.io/hostname: alpha-worker-0
initContainers:
# The launcher image runs Discourse as uid 1000; Bitnami used 999.
- name: fix-ownership
image: gitea.eom.dev/devops/discourse-web:v2026.10.0
command: ["sh", "-c", "chown -R 1000:1000 /shared/uploads /shared/backups"]
volumeMounts:
- name: discourse-data
mountPath: /shared/uploads
subPath: discourse/public/uploads
- name: discourse-data
mountPath: /shared/backups
subPath: discourse/public/backups
containers:
- name: discourse
image: gitea.eom.dev/devops/discourse-web:v2026.10.0
command: ["/sbin/boot"]
ports:
- name: http
containerPort: 80
env:
- { name: RAILS_ENV, value: production }
- { name: LANG, value: en_US.UTF-8 }
- { name: LC_ALL, value: en_US.UTF-8 }
- { name: MIGRATE_ON_BOOT, value: "1" }
- { name: PRECOMPILE_ON_BOOT, value: "1" }
- { name: CREATE_DB_ON_BOOT, value: "0" }
- { name: UNICORN_WORKERS, value: "2" }
- { name: UNICORN_SIDEKIQS, value: "1" }
- { name: DISCOURSE_HOSTNAME, value: discourse.eom.dev }
- { name: DISCOURSE_DB_SOCKET, value: "" }
- { name: DISCOURSE_DB_HOST, value: discourse-postgres18 }
- { name: DISCOURSE_DB_NAME, value: bitnami_application }
- { name: DISCOURSE_DB_USERNAME, value: bn_discourse }
- name: DISCOURSE_DB_PASSWORD
valueFrom:
secretKeyRef: { name: discourse-postgresql, key: password }
- { name: DISCOURSE_REDIS_HOST, value: discourse-redis-master }
- name: DISCOURSE_REDIS_PASSWORD
valueFrom:
secretKeyRef: { name: discourse-redis, key: redis-password }
- { name: DISCOURSE_SMTP_ADDRESS, value: postfix.eom.dev }
- { name: DISCOURSE_SMTP_PORT, value: "587" }
- { name: DISCOURSE_SMTP_USER_NAME, value: discourse }
- name: DISCOURSE_SMTP_PASSWORD
valueFrom:
secretKeyRef: { name: discourse-discourse, key: smtp-password }
startupProbe:
httpGet: &status
path: /srv/status
port: http
httpHeaders:
- { name: Host, value: discourse.eom.dev }
- { name: X-Forwarded-Proto, value: https }
periodSeconds: 10
failureThreshold: 180
readinessProbe:
httpGet: *status
periodSeconds: 10
failureThreshold: 6
livenessProbe:
httpGet: *status
periodSeconds: 20
failureThreshold: 6
resources:
requests:
cpu: 250m
memory: 1Gi
volumeMounts:
- name: shared
mountPath: /shared
- name: discourse-data
mountPath: /shared/uploads
subPath: discourse/public/uploads
- name: discourse-data
mountPath: /shared/backups
subPath: discourse/public/backups
volumes:
- name: shared
emptyDir: {}
- name: discourse-data
persistentVolumeClaim:
claimName: discourse
+73
View File
@@ -0,0 +1,73 @@
# PostgreSQL 18 with pgvector for Discourse. Runs alongside the old Bitnami
# PostgreSQL 17 StatefulSet, whose volume stays untouched for rollback.
apiVersion: v1
kind: Service
metadata:
name: discourse-postgres18
namespace: discourse
labels:
app.kubernetes.io/name: discourse-postgres18
spec:
clusterIP: None
selector:
app.kubernetes.io/name: discourse-postgres18
ports:
- name: postgresql
port: 5432
targetPort: postgresql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: discourse-postgres18
namespace: discourse
labels:
app.kubernetes.io/name: discourse-postgres18
spec:
serviceName: discourse-postgres18
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: discourse-postgres18
template:
metadata:
labels:
app.kubernetes.io/name: discourse-postgres18
spec:
containers:
- name: postgresql
image: pgvector/pgvector:pg18
ports:
- name: postgresql
containerPort: 5432
env:
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-postgresql
key: postgres-password
readinessProbe:
exec:
command: ["pg_isready", "-U", "postgres", "-h", "127.0.0.1"]
periodSeconds: 10
failureThreshold: 6
livenessProbe:
exec:
command: ["pg_isready", "-U", "postgres", "-h", "127.0.0.1"]
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 6
volumeMounts:
- name: data
mountPath: /var/lib/postgresql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: nfs-client
resources:
requests:
storage: 2Ti
+36
View File
@@ -0,0 +1,36 @@
# Build-only config for the eom.dev Discourse image. No secrets belong here:
# database, Redis and SMTP credentials are supplied at runtime by Kubernetes.
templates:
- "templates/web.template.yml"
- "templates/web.ratelimited.template.yml"
params:
version: v2026.10.0
env:
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
LANGUAGE: en_US.UTF-8
DISCOURSE_HOSTNAME: discourse.eom.dev
DISABLE_LETSENCRYPT: 1
# Everything else the forum uses (openid-connect, reactions, math, post-voting,
# adplugin, subscriptions, chat) is bundled in core at this version.
hooks:
after_code:
- exec:
cd: $home/plugins
cmd:
- git clone https://github.com/discourse/discourse-activity-pub.git
- git clone https://github.com/discourse/discourse-prometheus.git
run:
# ingress-nginx terminates TLS, so trust its X-Forwarded-For only from the pod
# network. Without this every visitor shares the ingress IP and hits rate limits.
- file:
path: /etc/nginx/conf.d/outlets/server/real-ip.conf
chmod: 644
contents: |
set_real_ip_from 10.244.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;