Discourse ohne Docker bereitstellen

Obwohl es bequemer und sicherer ist, Discourse gemäß der offiziellen Installationsanleitung bereitzustellen, möchte ich tiefer in den Container eintauchen und untersuchen, wie er ohne Docker unter Linux bereitgestellt werden kann. Ich möchte die Schritte nur zur Information teilen. Passen Sie sie an und verwenden Sie sie auf eigene Gefahr.

Eintauchen in die Ausführung von Discourse im Container

Ich werfe einen Blick auf die Ausgabe von ./launcher start-cmd webonly:

true run --shm-size=512m --link data:data -d --restart=always -e LANG=en_US.UTF-8 -e RAILS_ENV=production … --name webonly -t -v /var/discourse/shared/webonly:/shared … local_discourse/webonly /sbin/boot

Dann schaue ich mir /sbin/boot und /etc/service/unicorn/run an und erhalte den Kernbefehl zum Starten von Discourse:

LD_PRELOAD=$RUBY_ALLOCATOR HOME=/home/discourse USER=discourse exec thpoff chpst -u discourse:www-data -U discourse:www-data bundle exec config/unicorn_launcher -E production -c config/unicorn.conf.rb

System vorbereiten

Zur Info: Ich verwende Ubuntu 24.04 und zsh.

Folgen Sie dem offiziellen Installationsleitfaden von PG, um PostgreSQL aus dem PostgreSQL Apt Repository zu installieren. Ich habe Version 18 installiert, die gut funktioniert, obwohl die offizielle Installation zum Zeitpunkt des Schreibens Version 15 verwendet.

Installieren Sie redis (8.2 zum Zeitpunkt des Schreibens, die offizielle Installation verwendet 7.0), nginx und erstellen Sie einen dedizierten Benutzer discourse:

apt install nginx libnginx-mod-http-brotli-static redis zsh zsh-autosuggestions zsh-syntax-highlighting
systemctl enable --now postgresql redis nginx
useradd -m -s /bin/zsh discourse

Installieren Sie ImageMagick 7 (ich verwende IMEI) und überprüfen Sie die Version. Meine ist:

magick --version
Version: ImageMagick 7.1.2-3 Q16-HDRI

Ändern Sie dann den Benutzer (su - discourse) und installieren Sie pnpm sowie rvm.

curl -fsSL https://get.pnpm.io/install.sh | zsh -
curl -sSL https://get.rvm.io | bash

Passen Sie dann die folgende Konfiguration an und fügen Sie sie Ihrer .zshrc hinzu:

/home/discourse/.zshrc

# pnpm
export PNPM_HOME="/home/discourse/.local/share/pnpm"
case ":$PATH:" in
  *":$PNPM_HOME:"*) ;;
  *) export PATH="$PNPM_HOME:$PATH" ;;
esac
# pnpm Ende
alias npm='pnpm'
alias npx='pnpx'

# RVM zum Pfad für Skripte hinzufügen. Stellen Sie sicher, dass dies die letzte Änderung des PATH ist.
export PATH="$PATH:$HOME/.rvm/bin"

export ALLOW_EMBER_CLI_PROXY_BYPASS=1

export RAILS_ENV=production

export UNICORN_SIDEKIQ_MAX_RSS=1000
export UNICORN_WORKERS=4
export UNICORN_SIDEKIQS=1

export PUMA_SIDEKIQ_MAX_RSS=1000
export PUMA_WORKERS=4
export PUMA_SIDEKIQS=1

#export RUBY_YJIT_ENABLE=1
#export RUBY_CONFIGURE_OPTS="--enable-yjit"
export DISCOURSE_HOSTNAME=example.com
export DISCOURSE_DEVELOPER_EMAILS=discourse-admin@example.com

export DISCOURSE_MAXMIND_ACCOUNT_ID=<id>
export DISCOURSE_MAXMIND_LICENSE_KEY=<key>

export DISCOURSE_ENABLE_CORS=true
export DISCOURSE_MAX_REQS_PER_IP_MODE=none
export DISCOURSE_MAX_REQS_PER_IP_PER_MINUTE=20000
export DISCOURSE_MAX_REQS_PER_IP_PER_10_SECONDS=5000
export DISCOURSE_MAX_ASSET_REQS_PER_IP_PER_10_SECONDS=20000
export DISCOURSE_MAX_REQS_RATE_LIMIT_ON_PRIVATE=false
export DISCOURSE_MAX_USER_API_REQS_PER_MINUTE=200
export DISCOURSE_MAX_USER_API_REQS_PER_DAY=28800
export DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE=600
export DISCOURSE_MAX_DATA_EXPLORER_API_REQ_MODE=none

export DISCOURSE_MAX_REQS_PER_IP_EXCEPTIONS="127.0.0.1 ::1"
cd /var/www/discourse

Melden Sie sich ab und wieder als discourse an, damit die .zshrc wirksam wird.

Installieren Sie Node und Ruby:

pnpm env use --global latest # wird zum Zeitpunkt des Schreibens Node 24.9 installieren. Die offizielle Installation verwendet 22
rvm get master
rvm install 3.4 # wird zum Zeitpunkt des Schreibens Ruby 3.4.6 installieren. Die offizielle Installation verwendet 3.3
rvm use 3.4 --default

Datenbank vorbereiten (und ein Backup wiederherstellen)

sudo -u postgres createuser -s discourse                                                   
sudo -u postgres createdb discourse 

$sudo -u postgres psql discourse
psql>
ALTER USER discourse WITH PASSWORD 'xxx';
CREATE EXTENSION hstore;CREATE EXTENSION pg_trgm;
CREATE EXTENSION plpgsql;
CREATE EXTENSION unaccent;
CREATE EXTENSION vector;
# Um eine aus einem Backup extrahierte Datenbank wiederherzustellen:
$ gunzip < dump.sql.gz | psql discourse

Um ein Backup wiederherzustellen, müssen Sie auch die Ordner public und plugins kopieren.

Discourse installieren

Ich habe mich an discourse_docker/templates/web.template.yml at 20e33fbfd98d3b8d9c57f7a111beff8aa51a5b98 · discourse/discourse_docker · GitHub orientiert.

Als Benutzer root:

cd /var/www/
git clone https://github.com/discourse/discourse
mkdir -p /var/www/discourse/public
chown -R discourse:discourse /var/www/discourse/     
chown -R discourse:www-data /var/www/discourse/public

Konfigurieren Sie config/discourse.conf:

config/discourse.conf
max_data_explorer_api_req_mode = 'none'
max_user_api_reqs_per_day = '28800'
hostname = '127.0.0.1'
hostname = 'example.com'
redis_host = '127.0.0.1'
db_password = '<password>'
db_socket = ''
max_reqs_per_ip_per_10_seconds = '5000'
max_asset_reqs_per_ip_per_10_seconds = '20000'
max_reqs_rate_limit_on_private = 'false'
developer_emails = 'discourse-admin@example.com'
max_user_api_reqs_per_minute = '200'
maxmind_license_key = '<key>'
maxmind_account_id = '<id>'
max_reqs_per_ip_per_minute = '20000'
db_host = '127.0.0.1'
enable_cors = 'true'
db_port = ''
max_reqs_per_ip_mode = 'none'
max_admin_api_reqs_per_minute = '600'

smtp_user_name = '<name>'
smtp_address = 'postal.example.com'
smtp_port = '25'
smtp_password = '<password>'
smtp_domain = 'postalsend.example.com'
notification_email = 'noreply@postalsend.example.com'

Führen Sie die Installation von Bundle/pnpm, die Datenbankmigration und das Vorkompilieren von Assets durch. Dies ist auch der Weg, um Discourse und Plugins zu aktualisieren.

Als Benutzer discourse:

cd /var/www/discourse
git stash
git pull
git checkout tests-passed 
cd plugins
for plugin in *
do
    echo $plugin; cd ${plugin}; git pull; cd ..
done
cd ../
sed -i '/gem "rails_multisite"/i gem "rails"' Gemfile
bundle install --jobs $(($(nproc) - 1))
pnpm i
bundle exec rake db:migrate
bundle exec rake themes:update
bundle exec rake assets:precompile

Ich möchte unicorn nicht verwenden. Heroku empfiehlt die Verwendung des Puma-Webservers anstelle von Unicorn. Hier ist meine config/puma.rb, die nach der Konsultation von config/unicorn.conf.rb geschrieben wurde:

config/puma.rb
# frozen_string_literal: true

require "fileutils"
#require 'puma/acme'

discourse_path = File.expand_path(File.expand_path(File.dirname(__FILE__)) + "/../")

enable_logstash_logger = ENV["ENABLE_LOGSTASH_LOGGER"] == "1"
puma_stderr_path = "#{discourse_path}/log/puma.stderr.log"
puma_stdout_path = "#{discourse_path}/log/puma.stdout.log"

# Logstash-Logger laden, falls aktiviert
if enable_logstash_logger
  require_relative "../lib/discourse_logstash_logger"
  FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
  # Hinweis: Sie müssen möglicherweise die Logger-Initialisierung für Puma anpassen
  log_formatter =
    proc do |severity, time, progname, msg|
      event = {
        "@timestamp" => Time.now.utc,
        "message" => msg,
        "severity" => severity,
        "type" => "puma",
      }
      "#{event.to_json}\n"
    end
else
  stdout_redirect puma_stdout_path, puma_stderr_path, true
end

# Anzahl der Worker (Prozesse)
workers ENV.fetch("PUMA_WORKERS", 6).to_i

# Verzeichnis festlegen
directory discourse_path

# An die angegebene Adresse und den Port binden
bind ENV.fetch(
       "PUMA_BIND",
       "tcp://#{ENV["PUMA_BIND_ALL"] ? "" : "127.0.0.1:"}#{ENV.fetch("PUMA_PORT", 3000)}",
     )
#bind 'tcp://0.0.0.0:80'
#plugin :acme
#acme_server_name 'example.com'
#acme_tos_agreed true
#bind 'acme://0.0.0.0:443'

# PID-Dateispeicherort
FileUtils.mkdir_p("#{discourse_path}/tmp/pids")
pidfile ENV.fetch("PUMA_PID_PATH", "#{discourse_path}/tmp/pids/puma.pid")

# Zustandsdatei – wird von pumactl verwendet
state_path "#{discourse_path}/tmp/pids/puma.state"

# Konfiguration spezifisch für die Umgebung
if ENV["RAILS_ENV"] == "production"
  # Timeout für Produktion
  worker_timeout 30
else
  # Timeout für Entwicklung
  worker_timeout ENV.fetch("PUMA_TIMEOUT", 60).to_i
end

# Anwendung vorladen
preload_app!

# Worker-Start und -Abschaltung behandeln
before_fork do
  Discourse.preload_rails!
  Discourse.before_fork

  # Supervisor-Prüfung
  supervisor_pid = ENV["PUMA_SUPERVISOR_PID"].to_i
  if supervisor_pid > 0
    Thread.new do
      loop do
        unless File.exist?("/proc/#{supervisor_pid}")
          puts "Kill self supervisor is gone"
          Process.kill "TERM", Process.pid
        end
        sleep 2
      end
    end
  end

  # Sidekiq-Worker
  sidekiqs = ENV["PUMA_SIDEKIQS"].to_i
  if sidekiqs > 0
    puts "starting #{sidekiqs} supervised sidekiqs"

    require "demon/sidekiq"
    Demon::Sidekiq.after_fork { DiscourseEvent.trigger(:sidekiq_fork_started) }
    Demon::Sidekiq.start(sidekiqs)

    if Discourse.enable_sidekiq_logging?
      Signal.trap("USR1") do
        # Verzögerung beim erneuten Öffnen der Sidekiq-Logs
        sleep 1
        Demon::Sidekiq.kill("USR2")
      end
    end
  end

  # E-Mail-Sync-Demon
  if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
    puts "starting up EmailSync demon"
    Demon::EmailSync.start(1)
  end

  # Plugin-Demons
  DiscoursePluginRegistry.demon_processes.each do |demon_class|
    puts "starting #{demon_class.prefix} demon"
    demon_class.start(1)
  end

  # Überwachungs-Thread für Demons
  Thread.new do
    loop do
      begin
        sleep 60

        if sidekiqs > 0
          Demon::Sidekiq.ensure_running
          Demon::Sidekiq.heartbeat_check
          Demon::Sidekiq.rss_memory_check
        end

        if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
          Demon::EmailSync.ensure_running
          Demon::EmailSync.check_email_sync_heartbeat
        end

        DiscoursePluginRegistry.demon_processes.each(&:ensure_running)
      rescue => e
        Rails.logger.warn(
          "Error in demon processes heartbeat check: #{e}\n#{e.backtrace.join("\n")}",
        )
      end
    end
  end

  # Redis-Verbindung schließen
  Discourse.redis.close
end

on_worker_boot do
  DiscourseEvent.trigger(:web_fork_started)
  Discourse.after_fork
end

# Worker-Timeout-Behandlung
worker_timeout 30

# Low-Level-Worker-Optionen
threads 8, 32

Um Discourse auszuführen, führen Sie puma -C config/puma.rb aus.

Mit systemd können Sie es beim Start ausführen und bei Fehlern neu starten. Hier ist die Service-Datei:

/etc/systemd/system/discourse.service
[Unit]
Description=Discourse mit Puma-Server
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
User=discourse
Group=discourse
WorkingDirectory=/var/www/discourse
# Erfordert das Ausführen von `rvm 3.4.6 --default`, bevor dieser Dienst gestartet wird
ExecStart=/usr/bin/zsh -lc 'source /home/discourse/.zshrc && /home/discourse/.rvm/gems/ruby-3.4.6/bin/puma -C config/puma.rb'
ExecReload=/usr/bin/zsh -lc 'source /home/discourse/.zshrc && /home/discourse/.rvm/gems/ruby-3.4.6/bin/pumactl restart'

# Neustart-Konfiguration
Restart=always
RestartSec=5s

# Grundlegende Sicherheitsmaßnahmen
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only

[Install]
WantedBy=multi-user.target

Jetzt lauscht der Puma-Server auf 127.0.0.1:3000. Passen Sie die Nginx-Konfigurationsdatei aus Docker an:

/etc/nginx/sites-enabled/discourse.conf
# Zusätzliche MIME-Typen, die Sie von Nginx verarbeiten lassen möchten, gehen hier hinein
types {
    text/csv csv;
    #application/wasm wasm;
}

upstream discourse { server 127.0.0.1:3000; }

# Inaktiv bedeutet, dass wir Dinge für 1440 Minuten aufbewahren, unabhängig vom letzten Zugriff (1 Woche)
# Levels bedeutet, dass es eine 2-tiefe Hierarchie ist, da wir viele Dateien haben können
# max_size begrenzt die Größe des Caches
proxy_cache_path /var/nginx/cache inactive=1440m levels=1:2 keys_zone=one:10m max_size=600m;

# Vom Standardwert erhöht, um große Cookies während oAuth2-Flows aufzunehmen
# wie in https://meta.discourse.org/t/x/74060 und große CSP- und Link (Preload)-Header
proxy_buffer_size 32k;
proxy_buffers 4 32k;

# Vom Standardwert erhöht, um eine große Anzahl von Cookies in Anfrage-Headern zuzulassen
# Discourse selbst versucht, die Cookie-Größe zu minimieren, aber wir können andere Cookies, die von anderen Tools auf derselben Domain gesetzt werden, nicht kontrollieren.
large_client_header_buffers 4 32k;

# Versuchen, das Protokoll beizubehalten, muss im http-Kontext sein
map $http_x_forwarded_proto $thescheme {
  default $scheme;
  "~https$" https;
}

log_format log_discourse '[$time_local] "$http_host" $remote_addr "$request" "$http_user_agent" "$sent_http_x_discourse_route" $status $bytes_sent "$http_referer" $upstream_response_time $request_time "$upstream_http_x_discourse_username" "$upstream_http_x_discourse_trackview" "$upstream_http_x_queue_time" "$upstream_http_x_redis_calls" "$upstream_http_x_redis_time" "$upstream_http_x_sql_calls" "$upstream_http_x_sql_time"';

# Cache-Bypass von localhost zulassen
#geo $bypass_cache {
#  default         0;
#  127.0.0.1       1;
#  ::1             1;
#}

limit_req_zone $binary_remote_addr zone=flood:10m rate=12r/s;
limit_req_zone $binary_remote_addr zone=bot:10m rate=200r/m;
limit_req_status 429;
limit_conn_zone $binary_remote_addr zone=connperip:10m;
limit_conn_status 429;
server {
  access_log /var/log/nginx/access.log log_discourse;
  
  #listen unix:/var/nginx/nginx.http.sock;
  listen 443 ssl;
  listen [::]:443 ssl;
  server_name example.com;
  ssl_certificate /etc/nginx/ssl/example.com.cer;
  ssl_certificate_key /etc/nginx/ssl/example.com.key;
  ssl_protocols       TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
  ssl_ciphers         HIGH:!aNULL:!MD5;

  set_real_ip_from unix:;
  set_real_ip_from  127.0.0.1/32;
  set_real_ip_from  ::1/128;
  real_ip_header    X-Forwarded-For;
  real_ip_recursive on;

  gzip on;
  gzip_vary on;
  gzip_min_length 1000;
  gzip_comp_level 5;
  gzip_types application/json text/css text/javascript application/x-javascript application/javascript image/svg+xml application/wasm;
  gzip_proxied any;

  # Kommentieren Sie diesen Abschnitt aus und konfigurieren Sie ihn für HTTPS-Unterstützung
  # HINWEIS: Legen Sie Ihr SSL-Zertifikat in Ihrem Haupt-nginx-Konfigurationsverzeichnis ab (/etc/nginx)
  #
  # rewrite ^/(.*) https://enter.your.web.hostname.here/$1 permanent;
  #
  # listen 443 ssl;
  # ssl_certificate your-hostname-cert.pem;
  # ssl_certificate_key your-hostname-cert.key;
  # ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  # ssl_ciphers HIGH:!aNULL:!MD5;
  #

  server_tokens off;

  sendfile on;


  keepalive_timeout 65;

  # maximale Dateiladegröße (bei Änderung der entsprechenden Site-Einstellung aktuell halten)
  client_max_body_size 128m ;


  # Pfat zu Discourses öffentlichem Verzeichnis
  set $public /var/www/discourse/public;

  # Ohne schwache Etags erhalten wir bei dynamisch komprimierten Inhalten keinen Nutzen aus Etags
  # Außerdem basieren Etags auf der Datei in Nginx, nicht auf dem SHA der Daten
  # Verwenden Sie Daten, das löst das Problem auch serverübergreifend gut
  etag off;

  # Direkten Download von Backups verhindern
  location ^~ /backups/ {
    internal;
  }

  # Rails-Stack mit einem günstigen 204 für favicon.ico-Anfragen umgehen
  location /favicon.ico {
    return 204;
    access_log off;
    log_not_found off;
  }

  location / {
    root $public;
    add_header ETag "";

    # auth_basic on;
    # auth_basic_user_file /etc/nginx/htpasswd;

    location ~ ^/uploads/short-url/ {
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Request-Start "t=${msec}";
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $thescheme;
      proxy_pass http://discourse;
      break;
    }

    location ~ ^/(secure-media-uploads/|secure-uploads)/ {
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Request-Start "t=${msec}";
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $thescheme;
      proxy_pass http://discourse;
      break;
    }

    location ~* (fonts|assets|plugins|uploads)/.*\.(eot|ttf|woff|woff2|ico|otf)$ {
      expires 1y;
      add_header Cache-Control public,immutable;
      add_header Access-Control-Allow-Origin *;
    }

    location = /srv/status {
      access_log off;
      log_not_found off;
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Request-Start "t=${msec}";
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $thescheme;
      proxy_pass http://discourse;
      break;
    }

    # etwas minimales Caching hier, damit wir nicht ständig fragen
    # langfristig sollten wir wahrscheinlich auf 1 Jahr erhöhen
    location ~ ^/javascripts/ {
      expires 1d;
      add_header Cache-Control public,immutable;
      add_header Access-Control-Allow-Origin *;
    }

    location ~ ^/assets/(?<asset_path>.+)$ {
      expires 1y;
      # Asset-Pipeline aktiviert dies
      brotli_static on;
      gzip_static on;
      add_header Cache-Control public,immutable;
      # HOOK an Asset-Stelle (verwendet für Erweiterbarkeit)
      # TODO Ich denke, dieser break wird nicht benötigt, er bricht nur aus rewrite aus
      break;
    }

    location ~ ^/plugins/ {
      expires 1y;
      add_header Cache-Control public,immutable;
      add_header Access-Control-Allow-Origin *;
    }

    # Emojis cachen
    location ~ /images/emoji/ {
      expires 1y;
      add_header Cache-Control public,immutable;
      add_header Access-Control-Allow-Origin *;
    }

    location ~ ^/uploads/ {

      # HINWEIS: Es ist wirklich ärgerlich, dass wir Header nicht einfach auf oberster Ebene definieren und erben können.
      #
      # proxy_set_header erbt NICHT, per Design müssen wir es wiederholen,
      # sonst werden Header nicht korrekt gesetzt
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Request-Start "t=${msec}";
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $thescheme;
      proxy_set_header X-Sendfile-Type X-Accel-Redirect;
      proxy_set_header X-Accel-Mapping $public/=/downloads/;
      expires 1y;
      add_header Cache-Control public,immutable;

      ## optionale Upload-Anti-Hotlinking-Regeln
      #valid_referers none blocked mysite.com *.mysite.com;
      #if ($invalid_referer) { return 403; }

      # benutzerdefiniertes CSS
      location ~ /stylesheet-cache/ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # dies ermöglicht uns, Rails zu umgehen
      location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp|avif)$ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # SVG benötigt einen zusätzlichen Header
      location ~* \.(svg)$ {
      }
      # Thumbnails & optimierte Bilder
      location ~ /_?optimized/ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }

      proxy_pass http://discourse;
      break;
    }

    location ~ ^/admin/backups/ {
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Request-Start "t=${msec}";
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $thescheme;
      proxy_set_header X-Sendfile-Type X-Accel-Redirect;
      proxy_set_header X-Accel-Mapping $public/=/downloads/;
      proxy_pass http://discourse;
      break;
    }

    # Dieser große Block wird benötigt, damit wir die Beschleunigung selektiv für Backups, Avatare, Sprites usw. aktivieren können.
    # Siehe Hinweis zur Wiederholung oben
    location ~ ^/(svg-sprite/|letter_avatar/|letter_avatar_proxy/|user_avatar|highlight-js|stylesheets|theme-javascripts|favicon/proxied|service-worker) {
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Request-Start "t=${msec}";
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $thescheme;

      # wenn Set-Cookie in der Antwort ist, wird nichts gecacht
      # das ist doppelt schlecht, weil wir last modified nicht übergeben
      proxy_ignore_headers "Set-Cookie";
      proxy_hide_header "Set-Cookie";
      proxy_hide_header "X-Discourse-Username";
      proxy_hide_header "X-Runtime";

      # Hinweis: x-accel-redirect kann nicht mit proxy_cache verwendet werden
      proxy_cache one;
      proxy_cache_key "$scheme,$host,$request_uri";
      proxy_cache_valid 200 301 302 7d;
      #proxy_cache_bypass $bypass_cache;
      proxy_pass http://discourse;
      break;
    }

    # wir benötigen Pufferung aus für Message Bus
    location /message-bus/ {
      proxy_set_header X-Request-Start "t=${msec}";
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $thescheme;
      proxy_http_version 1.1;
      proxy_buffering off;
      proxy_pass http://discourse;
      break;
    }

    # das bedeutet, dass jede Datei in public zuerst versucht wird
    try_files $uri @discourse;
  }

  location /downloads/ {
    internal;
    alias $public/;
  }

  location @discourse {
  limit_conn connperip 20;
  limit_req zone=flood burst=12 nodelay;
  limit_req zone=bot burst=100 nodelay;
    proxy_set_header Host $http_host;
    proxy_set_header X-Request-Start "t=${msec}";
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $thescheme;
    proxy_pass http://discourse;
  }

}

Jetzt kann auf Ihr Discourse über example.com:443 zugegriffen werden.

Wartung

Um auf die Rails-Konsole zuzugreifen, führen Sie einfach rails c als Benutzer discourse in /var/www/discourse aus. Der Befehl discourse, der in der offiziellen Dokumentation zu finden ist, ist im Wesentlichen bundle exec script/discourse.

Um Discourse zu aktualisieren, konsultieren Sie #upgrade-cmd und starten Sie dann Puma entweder mit puma restart oder puma phased-restart neu. Für den Unterschied konsultieren Sie puma/docs/restart.md at main · puma/puma · GitHub.

8 „Gefällt mir“

Sollte dies vielleicht nach Community wiki > Sysadmins verschoben werden?

Hallo. Danke fürs Teilen.

Ich schreibe ein Skript, um dies auf Debian 12 in einem LXC-Container zu installieren. Es ist fast fertig und funktioniert gut. Ich werde es veröffentlichen, wenn es fertig ist.

Ich habe es geschafft, die erste Seite für die Administratorregistrierung anzuzeigen. Aber die Bestätigungs-E-Mail wird an discourse@myhostname gesendet, und myhostname als smtp_server, was absurd ist. Die Variablen in .bashrc (oder .zshrc), weder in discourse.conf werden für den Versand der E-Mail berücksichtigt. Die E-Mail-Adresse für den Entwickler ist korrekt, aber alle anderen Parameter sind falsch und ich konnte sie nicht ändern. Haben Sie eine Idee, wie ich das erreichen kann?

1 „Gefällt mir“

Laut Code

sollte SMTP in config/discourse.conf konfiguriert werden.
Bei mir stehen diese Zeilen darin:

smtp_user_name = '...com'
smtp_address = '...com'
smtp_port = '587'
smtp_password = '...'
smtp_domain = '...com'
notification_email = 'noreply@....com'

Haben Sie die Logs überprüft? Für diese benutzerdefinierte Installation befindet sich das Log in production.log, production.log, puma.stdout.log im Log-Verzeichnis.

Vielen Dank für Ihre Antwort.

Ich habe diese Einstellungen bereits in config/discourse.conf. Ich habe genau das getan, was Sie geschrieben haben, außer mit zsh.
Meine Logs sprechen nicht über smtp, und die einzige aus (production.log) ist

Started GET “/” for 192.168.1.14 at 2025-09-15 17:12:51 +0000
Processing by FinishInstallationController#index as HTML
Rendered layout layouts/finish_installation.html.erb (Duration: 57.8ms | GC: 1.0ms)
Completed 200 OK in 164ms (Views: 61.7ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 8.7ms)
Started GET “/finish-installation/register” for 192.168.1.14 at 2025-09-15 17:12:53 +0000
Processing by FinishInstallationController#register as HTML
Rendered layout layouts/finish_installation.html.erb (Duration: 63.8ms | GC: 1.5ms)
Completed 200 OK in 166ms (Views: 68.0ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 5.4ms)
Started POST “/finish-installation/register” for 192.168.1.14 at 2025-09-15 17:12:54 +0000
Processing by FinishInstallationController#register as HTML
Parameters: {“authenticity_token”=>“U9_0mqt8iE5Y_jdNSV5uZxOgz9rspJbEsohs0jU8QTOPaOXdyaG-oLSFYtn9dQ2-mdHYvCzjFsRaqzp6YlNzbQ”, “email”=>“webmaster@domain.app”, “username”=>“ioio”, “password”=>“[FILTERED]”, “commit”=>“Register”}
Redirected to http://myhostname/finish-installation/confirm-email
Completed 302 Found in 140ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 4.4ms)
Started GET “/finish-installation/confirm-email” for 192.168.1.14 at 2025-09-15 17:12:54 +0000
Processing by FinishInstallationController#confirm_email as HTML
Rendered layout layouts/finish_installation.html.erb (Duration: 62.1ms | GC: 1.5ms)

Meine Mailserver-Logs zeigen keine Einträge vom Discourse-Server (funktioniert für alle meine anderen Server (alle LXC-Container). Ich kann erfolgreich eine E-Mail über den mail-Terminalbefehl senden.

mit puma -C config/puma.rb:

Use ‘before_worker_boot’, ‘on_worker_boot’ is deprecated and will be removed in v8
[498] Puma starting in cluster mode…
[498] * Puma version: 7.0.0 (“Romantic Warrior”)
[498] * Ruby version: ruby 3.3.9 (2025-07-24 revision f5c772fc7c) [x86_64-linux]
[498] * Min threads: 8
[498] * Max threads: 32
[498] * Environment: production
[498] * Master PID: 498
[498] * Workers: 8
[498] * Restarts: (✔) hot (✖) phased (✖) refork
[498] * Preloading application
[498] * Listening on http://127.0.0.1:3000
[498] ! WARNING: Detected 2 Thread(s) started in app boot:
[498] ! #<Thread:0x00007f43cec88b38 /home/discourse/.rvm/gems/ruby-3.3.9/gems/message_bus-4.4.1/lib/message_bus.rb:738 sleep> - /home/discourse/.rvm/gems/ruby-3.3.9/gems/redis-client-0.25.2/lib/redis_client/ruby_connection/buffered_io.rb:213:in `wait_readable'
[498] ! #<Thread:0x00007f43cec887f0 /home/discourse/.rvm/gems/ruby-3.3.9/gems/message_bus-4.4.1/lib/message_bus.rb:738 sleep> - /home/discourse/.rvm/gems/ruby-3.3.9/gems/message_bus-4.4.1/lib/message_bus/timer_thread.rb:38 sleep> - /home/discourse/.rvm/gems/ruby-3.3.9/gems/message_bus-4.4.1/lib/message_bus/timer_thread.rb:130:in `sleep’
[498] Use Ctrl-C to stop

mein discourse.conf:

max_data_explorer_api_req_mode = ‘none’
max_user_api_reqs_per_day = ‘28800’
hostname = ‘xxxxxxxxxxxxxxxxx.xxxx.app’
redis_host = ‘localhost’
smtp_user_name = ‘xxxxx@xxxxx.app’
db_password = ‘password’
smtp_address = ‘mail.xxxxx.app’
db_socket = ‘’
max_reqs_per_ip_per_10_seconds = ‘5000’
max_asset_reqs_per_ip_per_10_seconds = ‘20000’
max_reqs_rate_limit_on_private = ‘false’
developer_emails = ‘webmaster@xxx.app’
max_user_api_reqs_per_minute = ‘200’
maxmind_license_key = ‘’
smtp_port = ‘465’
maxmind_account_id = ‘50’
smtp_password = ‘xxxxxx’
max_reqs_per_ip_per_minute = ‘20000’
notification_email = ‘no-reply-discourse@xxx.app’
db_host = ‘localhost’
enable_cors = ‘true’
db_port = ‘’
max_reqs_per_ip_mode = ‘none’
smtp_domain = ‘xxx.app’
max_admin_api_reqs_per_minute = ‘600’

.bashrc ist wie Ihr .zhrc
Änderungen an den Einträgen developper_emails oder db_password funktionieren (die korrekte E-Mail wird auf der Registrierungsseite des Website-Admins angezeigt), aber andere SMTP-Parameter werden ignoriert.

Ihre Datei config/puma.rb enthält einige Fehler (ungefähr in der Zeile mit Port 3000). Könnten Sie sie bitte erneut bereitstellen?

Ich habe den Administrator mit rails c registriert und danach erhalte ich auf der Startseite die Seite „Ups…“. Keine Seiten werden korrekt gerendert.

Bitte helfen Sie mir.

@lion , versuchen Sie es mit diesem:\n\n```

frozen_string_literal: true

require “fileutils”

discourse_path = File.expand_path(File.expand_path(File.dirname(FILE)) + “/../”)

enable_logstash_logger = ENV[“ENABLE_LOGSTASH_LOGGER”] == “1”
puma_stderr_path = “#{discourse_path}/log/puma.stderr.log”
puma_stdout_path = “#{discourse_path}/log/puma.stdout.log”

Logstash-Logger laden, falls aktiviert

if enable_logstash_logger
require_relative “../lib/discourse_logstash_logger”
FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)

Hinweis: Möglicherweise müssen Sie die Logger-Initialisierung für Puma anpassen

log_formatter = proc do |severity, time, progname, msg|
event = {
@timestamp” => Time.now.utc,
“message” => msg,
“severity” => severity,
“type” => “puma”
}
“#{event.to_json}\n”
end
else
stdout_redirect puma_stdout_path, puma_stderr_path, true
end

Anzahl der Worker (Prozesse)

workers ENV.fetch(“PUMA_WORKERS”, 8).to_i

Verzeichnis festlegen

directory discourse_path

An die angegebene Adresse und den Port binden

bind ENV.fetch(“PUMA_BIND”, “tcp://#{ENV[‘PUMA_BIND_ALL’] ? ‘’ : ‘127.0.0.1:’}3000”)

PID-Datei-Speicherort

FileUtils.mkdir_p(“#{discourse_path}/tmp/pids”)
pidfile ENV.fetch(“PUMA_PID_PATH”, “#{discourse_path}/tmp/pids/puma.pid”)

Statusdatei - wird von pumactl verwendet

state_path “#{discourse_path}/tmp/pids/puma.state”

Umgebungsabhängige Konfiguration

if ENV[“RAILS_ENV”] == “production”

Produktions-Timeout

worker_timeout 30
else

Entwicklungs-Timeout

worker_timeout ENV.fetch(“PUMA_TIMEOUT”, 60).to_i
end

Anwendung vorladen

preload_app!

Worker-Start und -Herunterfahren behandeln

before_fork do
Discourse.preload_rails!
Discourse.before_fork

Supervisor-Prüfung

supervisor_pid = ENV[“PUMA_SUPERVISOR_PID”].to_i
if supervisor_pid > 0
Thread.new do
loop do
unless File.exist?(“/proc/#{supervisor_pid}”)
puts “Kill self supervisor is gone”
Process.kill “TERM”, Process.pid
end
sleep 2
end
end
end

Sidekiq-Worker

sidekiqs = ENV[“PUMA_SIDEKIQS”].to_i
if sidekiqs > 0
puts “starting #{sidekiqs} supervised sidekiqs”

require "demon/sidekiq"
Demon::Sidekiq.after_fork { DiscourseEvent.trigger(:sidekiq_fork_started) }
Demon::Sidekiq.start(sidekiqs)

if Discourse.enable_sidekiq_logging?
  Signal.trap("USR1") do
    # Verzögern Sie das erneute Öffnen der Sidekiq-Protokolle
    sleep 1
    Demon::Sidekiq.kill("USR2")
  end
end

end

E-Mail-Synchronisierungs-Daemon

if ENV[“DISCOURSE_ENABLE_EMAIL_SYNC_DEMON”] == “true”
puts “starting up EmailSync demon”
Demon::EmailSync.start(1)
end

Plugin-Daemons

DiscoursePluginRegistry.demon_processes.each do |demon_class|
puts “starting #{demon_class.prefix} demon”
demon_class.start(1)
end

Daemon-Überwachungs-Thread

Thread.new do
loop do
begin
sleep 60

    if sidekiqs > 0
      Demon::Sidekiq.ensure_running
      Demon::Sidekiq.heartbeat_check
      Demon::Sidekiq.rss_memory_check
    end

    if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
      Demon::EmailSync.ensure_running
      Demon::EmailSync.check_email_sync_heartbeat
    end

    DiscoursePluginRegistry.demon_processes.each(&:ensure_running)
  rescue => e
    Rails.logger.warn("Error in demon processes heartbeat check: #{e}\n#{e.backtrace.join("\n")}")
  end
end

end

Redis-Verbindung schließen

Discourse.redis.close
end

on_worker_boot do
DiscourseEvent.trigger(:web_fork_started)
Discourse.after_fork
end

Worker-Timeout-Behandlung

worker_timeout 30

Low-Level-Worker-Optionen

threads 8, 32

Danke. Ich habe vergessen zu erwähnen, dass es hinter haproxy für mehrere Server steht.

  1. Ich erhalte „Mixed Content“ aus der Browserkonsole. Was kann ich in der Nginx-Datei ändern?
  2. Ich erhalte dieses Log über Magick beim Start des Puma-Befehls. Es ist auch zu Beginn installiert.

==
/var/www/discourse/log/puma.stderr.log
<==
=== puma startup: 2025-09-19 01:40:45 +0200 ===
unknown OID 16720: failed to recognize type of ‘embeddings’. It will be treated as String.
#<Thread:0x00007fc59a2f5a78 /var/www/discourse/lib/discourse.rb:1190 run> terminated with exception (report_on_exception is true):
/var/www/discourse/lib/letter_avatar.rb:112:in ``‘: No such file or directory - magick (Errno::ENOENT)
from /var/www/discourse/lib/letter_avatar.rb:112:in image_magick_version' from /var/www/discourse/lib/discourse.rb:1190:in block in preload_rails!’
Ich habe gelesen, dass dies der Grund dafür sein kann, dass Seiten nicht angezeigt werden.

Erzwingen Sie SSL in den Discourse-Einstellungen?

Können Sie magick --version als Discourse-Benutzer ausführen? Bei mir lautet die Ausgabe:
Version: ImageMagick 7.1.1-45 Q16-HDRI x86_64 3cbce5696:20250308 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: ImageMagick | License
Features: Cipher DPC HDRI Modules OpenMP(4.5)
Delegates (built-in): bzlib cairo djvu fftw fontconfig freetype gslib gvc heic jbig jng jp2 jpeg jxl lcms lqr ltdl lzma openexr pangocairo png ps raqm raw rsvg tiff webp wmf x xml zip zlib zstd
Compiler: gcc (13.3)

Ja, genau das habe ich mir auch gedacht.

Ich habe viele nicht erfüllte Abhängigkeiten gefunden, während ich “manuell” installiert habe. Ich schreibe gerade ein Ansible, um Discourse in mehreren Umgebungen bereitstellen zu können.

Meine Systeme sind AlmaLinux/Red Hat, aber ich werde teilen, was ich gefunden habe, wenn ich es fast fertiggestellt habe.

1 „Gefällt mir“

In haproxy.cfg ist 443 mit crt, also verschlüsselt, an den Container über 8080 weitergeleitet:

backend BACKEND_XXX
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server xxx 192.168.1.48:8080 weight 1 # Fügt “check ssl verify none” hinzu, wenn SSL auf Nginx Discourse aktiviert ist

  1. In nginx/…/discourse.conf habe ich die Version dieses Beitrags kopiert und den Port, den Servernamen und den Speicherort der Zertifikatsdateien geändert.
    => PB1: Gemischte Inhalte und Protokolle mit demselben Problem wie bei Magick
    => PB2: Auf der Seite “/confirm-email” erhalte ich die Fehlermeldung “Seite nicht gefunden” im Browser.

  2. Dann habe ich die Datei geändert, indem ich den SSL-Port entfernt und alle Zeilen mit “$thescheme” auskommentiert habe, um nur in HAProxy zu verschlüsseln.
    => 502-Fehler auf der ersten Seite, dieselben Protokolle mit Magick

  3. Dann habe ich Magick aus dem Quellcode (7.1.2.3) installiert und den Binärordner in PATH in .bashrc für Root und für Discourse-Benutzer hinzugefügt. Mit “apt install” hat es nicht funktioniert (keine Auswirkung wie diese).
    => 502-Fehler behoben, immer noch gemischte Inhalte und “Seite nicht gefunden” im Browser.
    => Protokolle zeigen immer noch “magick not found”, aber anders.
    ==> /var/www/discourse/log/puma.stderr.log <==
    === puma startup: 2025-09-19 12:06:05 +0200 ===
    unknown OID 16720: failed to recognize type of ‘embeddings’. It will be treated as String.
    #<Thread:0x00007f611a21c0f0 /var/www/discourse/lib/discourse.rb:1190 run> terminated with exception (report_on_exception is true):
    /var/www/discourse/lib/letter_avatar.rb:112:in ``‘: No such file or directory - magick (Errno::ENOENT)
    from /var/www/discourse/lib/letter_avatar.rb:112:in image_magick_version' from /var/www/discourse/lib/discourse.rb:1190:in block in preload_rails!’
    ==> /var/www/discourse/log/puma.stdout.log <==
    [8967] WARNING hook before_fork failed with exception (Errno::ENOENT) No such file or directory - magick

Schlussfolgerung:
Wie kann ich Magick für Puma auffindbar machen?
Wie deaktiviere ich SSL in Nginx wegen HAProxy?

Ich habe es geschafft, es wie in meinem ersten Beitrag beschrieben zum Laufen zu bringen, aber hier ohne SSL unter Nginx, indem ich die nginx.config.sample kopiert und nur den Hostnamen und den Port geändert habe.
Sowohl mit als auch ohne SSL unter Nginx erhalte ich die erste Seite für die Admin-Registrierung bis zur Seite zum erneuten Senden der E-Mail, aber BEIDES:

  1. Zeigt “gemischten Inhalt” an (z. B. für Bilddateien).
  2. Seiten außer der Admin-Registrierung zeigen “Ups…” an.
  3. Es werden keine Bestätigungs-E-Mails gesendet.
  4. ImageMagick wird immer noch nicht gefunden.

Irgendwo in einem Ihrer Beiträge erwähnen Sie Container.

Verwenden Sie Container für Teile davon?

Heute möchte ich mein Deployment in der Preprod-Umgebung abschließen und vielleicht habe ich mehr Informationen zum Teilen.

Genau wie du :unamused_face:

  • Keine Bestätigungs-E-Mail wird gesendet (ich benutze Mailtrap, ich bin sicher, dass es funktioniert).
  • Admin-Benutzer mit Rake erstellt
  • Huch auf / danach…
  • Auch Magick-Problem gesehen und Magick ist installiert (ich bin ziemlich sicher, dass ein Rubygem fehlt…)

Ich setze es mit Docker ein und vergleiche dann. Ich hasse Docker :expressionless_face:, aber ich verschwende viel wertvolle Zeit…

Ich kenne Docker kaum (wenn überhaupt!) und ich musste es nicht kennen, um ein Discourse-Forum in der Produktion zu betreiben. Allein durch das Befolgen der unterstützten Anweisungen erhalten Sie, was Sie brauchen.

Ja, ich glaube dir. Aber ich muss wissen, was ich bereitstelle und wie es funktioniert.

Es ist der beste Weg, Probleme zu finden und zu lösen :squinting_face_with_tongue:

Denn du wirst Probleme haben, immer, vielleicht nicht jetzt, aber ich bin sicher, dass die zukünftige du jemanden finden wird.

Wie auch immer, da ich keine Unterstützung für alternative Installationsmethoden erhalte, werde ich die offiziellen Anweisungen befolgen und anderen helfen :winking_face_with_tongue:

Ich habe auch viel Zeit mit diesen Problemen verbracht.
Was Docker betrifft, sehen Sie sich dieses Problem bei Docker an (ein Fehler oder eine Hintertür?..), das im Moment behoben ist: https://youtu.be/dTqxNc1MVLE
Das ist einer meiner Gründe, warum ich Docker nicht verwenden würde.
Ich benutze LXD (LXC)-Container und fühle mich damit wohl. Ich werde Docker zunächst in einem LXC-Container installieren und die Datenbank später exportieren, wenn die Installation ohne Docker in einem LXC möglich ist.

Nun zwinge ich Discourse nicht zu SSL, weil ich möchte, dass mein HAProxy es tut, da HAProxy-Passthrough nicht mit dem HTTP-Protokoll funktioniert, um GET-Anfragen umzuleiten, und ich mehrere Websites verwalte, sodass ich das HTTP-Protokoll in HAProxy benötige, was eine SSL-Handhabung auf der HAProxy-Seite erfordert. Ich möchte ein doppeltes SSL-Gateway vermeiden.
Also hört HAProxy (auf einem LXC) auf 443: leitet auf 8080 (ohne SSL) an meinen Discourse-Container (LXC) weiter.
Interessanterweise ist die nginx-config-sample im Discourse-Ordner ohne SSL und Port 80 konfiguriert, sodass es gut funktionieren sollte, aber ich bekomme die oben genannten Probleme.

Sie müssen das tun, um Discourse anzuweisen, nur HTTPS-Links zu senden. Es ändert nicht die Weiterleitungen, sondern die Notwendigkeit dafür.

Sie müssen die Einstellung „HTTPS erzwingen“ aktivieren.

HTTPS: Ja, aber sagen Sie mir bitte, wie ich es einschalten kann?

GROSSARTIGE NACHRICHTEN! ICH HABE DIE PROBLEME GELÖST!!! Die Quelle war das Magick-Programm. Nach der korrekten Magick-Installation aus Version 7 der Quelle (installieren Sie ImageMagick nicht mit apt, es ist Version 6) zeigt meine Discourse-Website alle Seiten korrekt an! E-Mails, Admin-Registrierung usw. funktionieren! Nachdem das “Mixed Content”-Problem gelöst ist, werde ich mein Skript für die Installation in einem LXD-LXC-Container (Debian 12) hinter HAProxy auf einem anderen LXD-LXC-Container veröffentlichen.

1 „Gefällt mir“