From ae5a81802b4d1d6cd1b449ac46cfa16d80730aaa Mon Sep 17 00:00:00 2001 From: notplants <@notplants> Date: Tue, 16 Jun 2026 22:33:35 +0000 Subject: [PATCH] feat(discourse): switch app to official discourse/discourse image (experimental) Replaces the paywalled bitnamilegacy app with the official discourse/discourse image behind Traefik (HTTP-only via an install-ssl override; sidekiq is internal so its service is dropped). DB is reused as-is; uploads migrate from a legacy bitnami volume via an idempotent, non-destructive runit hook. db keeps pgvector/pgvector:pg17 with the install-user-aware pg_upgrade entrypoint. Verified on cctest: fresh install, upgrade-from-bitnami-pg17, and upgrade-from-bitnami-pg13 (incl. 13->17) all serve with data intact. Upstream marks discourse/discourse experimental; hold prod cutover. Recipe 0.8.1+3.5.0 -> 1.0.0+3.5.3 (major: new image, env/volume/port changes). --- .env.sample | 22 +++++----- abra.sh | 3 ++ app-install-ssl.sh | 11 +++++ cc-app-entrypoint.sh | 11 +++++ compose.yml | 98 ++++++++++++++++++++------------------------ migrate-uploads.sh | 24 +++++++++++ 6 files changed, 106 insertions(+), 63 deletions(-) create mode 100755 app-install-ssl.sh create mode 100755 cc-app-entrypoint.sh create mode 100755 migrate-uploads.sh diff --git a/.env.sample b/.env.sample index fd54e8f..bbbb5f7 100644 --- a/.env.sample +++ b/.env.sample @@ -5,17 +5,19 @@ DOMAIN=discourse.example.com #EXTRA_DOMAINS=', `www.discourse.example.com`' LETS_ENCRYPT_ENV=production -# Outgoing email -#DISCOURSE_SMTP_HOST= -#DISCOURSE_SMTP_PORT= -#DISCOURSE_SMTP_USER= -#DISCOURSE_SMTP_PROTOCOL= -#DISCOURSE_SMTP_AUTH= -# Set this if you send e-mails from a different domain than noreply@$DOMAIN -#DISCOURSE_NOTIFICATION_EMAIL=$SMTP_USER +# Admin / developer accounts (comma-separated); these become admins on signup +DISCOURSE_DEVELOPER_EMAILS=admin@example.com -# SMTP authentication -#COMPOSE_FILE="compose.yml:compose.smtpauth.yml" +# Outgoing email (official discourse/discourse env names) +#DISCOURSE_SMTP_ADDRESS= +#DISCOURSE_SMTP_PORT=587 +#DISCOURSE_SMTP_USER_NAME= +#DISCOURSE_SMTP_AUTHENTICATION=login +#DISCOURSE_SMTP_ENABLE_START_TLS=true +# Set this if you send e-mail from a different address than noreply@$DOMAIN +#DISCOURSE_NOTIFICATION_EMAIL= + +# SMTP password as a secret #SECRET_SMTP_PASSWORD_VERSION=v1 SECRET_DB_PASSWORD_VERSION=v1 diff --git a/abra.sh b/abra.sh index b08beeb..330b4eb 100644 --- a/abra.sh +++ b/abra.sh @@ -1,2 +1,5 @@ export DB_ENTRYPOINT_VERSION=v3 export PG_BACKUP_VERSION=v2 +export APP_ENTRYPOINT_VERSION=v1 +export APP_INSTALL_SSL_VERSION=v1 +export APP_MIGRATE_UPLOADS_VERSION=v1 diff --git a/app-install-ssl.sh b/app-install-ssl.sh new file mode 100755 index 0000000..418271b --- /dev/null +++ b/app-install-ssl.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Overrides the official image's /etc/runit/1.d/install-ssl. +# +# The stock install-ssl always runs configure-ssl (and configure-letsencrypt), +# which empties the default `listen 80` nginx outlet and switches to `listen 443 +# ssl` against a cert that does not exist here — nginx then crash-loops, or the +# image tries to obtain its own Let's Encrypt cert. Under Co-op Cloud, Traefik +# terminates TLS and proxies plain HTTP to port 80, so we skip the image's SSL +# setup entirely and let nginx keep its default HTTP-on-80 config. +echo "install-ssl overridden by recipe: serving plain HTTP on :80 behind Traefik" +exit 0 diff --git a/cc-app-entrypoint.sh b/cc-app-entrypoint.sh new file mode 100755 index 0000000..02bd5b4 --- /dev/null +++ b/cc-app-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Co-op Cloud wrapper around the official image's /sbin/boot. +# discourse/discourse reads DISCOURSE_DB_PASSWORD from the process env (pups/Ruby; +# it has no *_FILE support), so inject it from the docker secret before booting. +set -e + +if [ -f /run/secrets/db_password ]; then + export DISCOURSE_DB_PASSWORD="$(cat /run/secrets/db_password)" +fi + +exec /sbin/boot diff --git a/compose.yml b/compose.yml index 3365c28..97f3004 100644 --- a/compose.yml +++ b/compose.yml @@ -3,53 +3,64 @@ version: "3.8" services: app: - image: bitnamilegacy/discourse:3.5.0 + image: discourse/discourse:3.5.3 networks: - proxy - internal - # entrypoint: ['tail', '-f', '/dev/null'] + # official image CMD is /sbin/boot; wrapper injects the DB password secret first + entrypoint: /usr/local/bin/cc-app-entrypoint.sh environment: - - ALLOW_EMPTY_PASSWORD=yes - - DISCOURSE_DATABASE_HOST=${STACK_NAME}_db - - DISCOURSE_DATABASE_NAME=discourse - - DISCOURSE_DATABASE_PASSWORD_FILE=/run/secrets/db_password - - DISCOURSE_DATABASE_USER=discourse - - DISCOURSE_HOST=${DOMAIN} - - DISCOURSE_NOTIFICATION_EMAIL - - DISCOURSE_SMTP_AUTH - - DISCOURSE_SMTP_HOST + - DISCOURSE_HOSTNAME=${DOMAIN} + - DISCOURSE_DEVELOPER_EMAILS=${DISCOURSE_DEVELOPER_EMAILS} + - DISCOURSE_DB_HOST=${STACK_NAME}_db + - DISCOURSE_DB_PORT=5432 + - DISCOURSE_DB_NAME=discourse + - DISCOURSE_DB_USERNAME=discourse + - DISCOURSE_REDIS_HOST=${STACK_NAME}_redis + - DISCOURSE_REDIS_PORT=6379 + - DISCOURSE_SMTP_ADDRESS - DISCOURSE_SMTP_PORT - - DISCOURSE_SMTP_PROTOCOL - - DISCOURSE_SMTP_USER - - PASSENGER_COMPILE_NATIVE_SUPPORT_BINARY=0 + - DISCOURSE_SMTP_USER_NAME + - DISCOURSE_SMTP_PASSWORD + - DISCOURSE_SMTP_AUTHENTICATION + - DISCOURSE_SMTP_ENABLE_START_TLS + - DISCOURSE_NOTIFICATION_EMAIL volumes: - - 'discourse_data:/bitnami/discourse' + - 'discourse_shared:/shared' + # transition only: legacy bitnami volume, read-only, for one-time upload migration + - 'discourse_data:/legacy:ro' secrets: - db_password + configs: + - source: app_entrypoint + target: /usr/local/bin/cc-app-entrypoint.sh + mode: 0555 + - source: app_install_ssl + target: /etc/runit/1.d/install-ssl + mode: 0555 + - source: app_migrate_uploads + target: /etc/runit/1.d/02-migrate-bitnami-uploads + mode: 0555 depends_on: - db - redis deploy: update_config: failure_action: rollback - order: start-first + order: stop-first labels: - "traefik.enable=true" - - "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=3000" + - "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=80" - "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`${EXTRA_DOMAINS})" - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" - ## Redirect from EXTRA_DOMAINS to DOMAIN - #- "traefik.http.routers.${STACK_NAME}.middlewares=${STACK_NAME}-redirect" - #- "traefik.http.middlewares.${STACK_NAME}-redirect.headers.SSLForceHost=true" - #- "traefik.http.middlewares.${STACK_NAME}-redirect.headers.SSLHost=${DOMAIN}" - - "coop-cloud.${STACK_NAME}.version=0.8.1+3.5.0" + - "coop-cloud.${STACK_NAME}.version=1.0.0+3.5.3" healthcheck: - test: "ruby -e \"require 'uri'; require 'net/http'; uri = URI('http://localhost:3000/srv/status'); res = Net::HTTP.get_response(uri); if res.is_a?(Net::HTTPSuccess) then exit (0) else exit (1) end\"" + test: "curl -fsS http://localhost/srv/status || exit 1" interval: 30s timeout: 10s retries: 6 - start_period: 20m + start_period: 25m db: image: pgvector/pgvector:pg17 @@ -86,35 +97,6 @@ services: volumes: - 'redis_data:/data' - sidekiq: - image: bitnamilegacy/discourse:3.5.0 - networks: - - proxy - - internal - depends_on: - - discourse - volumes: - - 'discourse_data:/bitnami/discourse' - command: /opt/bitnami/scripts/discourse-sidekiq/run.sh - secrets: - - db_password - environment: - - ALLOW_EMPTY_PASSWORD=yes - - DISCOURSE_DATABASE_HOST=db - - DISCOURSE_DATABASE_NAME=discourse - - DISCOURSE_DATABASE_PASSWORD_FILE=/run/secrets/db_password - - DISCOURSE_DATABASE_PORT_NUMBER=5432 - - DISCOURSE_DATABASE_USER=discourse - - DISCOURSE_HOST=${DOMAIN} - - DISCOURSE_REDIS_HOST=redis - - DISCOURSE_REDIS_PORT_NUMBER=6379 - - DISCOURSE_SMTP_HOST - - DISCOURSE_SMTP_PORT - - DISCOURSE_SMTP_PROTOCOL - - DISCOURSE_SMTP_USER - - PASSENGER_COMPILE_NATIVE_SUPPORT_BINARY=0 - - DISCOURSE_SMTP_AUTH - secrets: db_password: external: true @@ -123,6 +105,7 @@ secrets: volumes: postgresql_data: redis_data: + discourse_shared: discourse_data: networks: @@ -131,6 +114,15 @@ networks: internal: configs: + app_entrypoint: + name: ${STACK_NAME}_app_entrypoint_${APP_ENTRYPOINT_VERSION} + file: cc-app-entrypoint.sh + app_install_ssl: + name: ${STACK_NAME}_app_install_ssl_${APP_INSTALL_SSL_VERSION} + file: app-install-ssl.sh + app_migrate_uploads: + name: ${STACK_NAME}_app_migrate_uploads_${APP_MIGRATE_UPLOADS_VERSION} + file: migrate-uploads.sh db_entrypoint: name: ${STACK_NAME}_db_entrypoint_${DB_ENTRYPOINT_VERSION} file: entrypoint.postgres.sh.tmpl diff --git a/migrate-uploads.sh b/migrate-uploads.sh new file mode 100755 index 0000000..3189be5 --- /dev/null +++ b/migrate-uploads.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# One-time, idempotent, NON-destructive migration of uploads + backups from a +# legacy bitnami discourse volume into the official image's /shared. +# +# Runs on every boot as a runit 1.d hook but no-ops after the first success +# (sentinel) and when there is no legacy volume mounted (fresh installs). It only +# ever COPIES from the read-only /legacy mount, so an interruption just re-copies +# on the next boot — there is no move/delete to leave the data half-migrated. +set -e + +SENTINEL=/shared/.bitnami-uploads-migrated +[ -e "$SENTINEL" ] && exit 0 + +if [ -d /legacy/public/uploads ]; then + echo "[migrate-uploads] copying bitnami uploads/backups -> /shared" + mkdir -p /shared/uploads /shared/backups + cp -a /legacy/public/uploads/. /shared/uploads/ 2>/dev/null || true + cp -a /legacy/public/backups/. /shared/backups/ 2>/dev/null || true + # discourse runs as uid 1000; the official boot also chowns /shared, but be explicit + chown -R discourse:discourse /shared/uploads /shared/backups 2>/dev/null || true + echo "[migrate-uploads] done" +fi + +touch "$SENTINEL"