Deploy Discourse senza Docker

Sebbene sia più conveniente e sicuro distribuire Discourse seguendo la guida ufficiale di installazione, voglio approfondire il contenitore e vedere come può essere distribuito in Linux senza Docker. Voglio condividere i passaggi solo a titolo informativo. Adattali e utilizzali a tuo rischio e pericolo.

Approfondisci come Discourse viene eseguito nel contenitore

Esamino l’output di ./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

poi guardo /sbin/boot e /etc/service/unicorn/run e ottengo il comando principale per avviare 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

preparare il sistema

Per tua informazione, uso Ubuntu 24.04 e zsh.

Segui la guida ufficiale di installazione di PG per installare PostgreSQL dal repository APT di PostgreSQL. Ho installato la versione 18, che funziona molto bene, anche se l’installazione ufficiale utilizza la 15 al momento della stesura.

Installa redis (8.2 al momento della stesura, l’installazione ufficiale utilizza la 7.0), nginx e crea l’utente dedicato 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

Installa ImageMagick 7 (uso IMEI) e controlla la versione. La mia è:

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

poi, cambia utente (su - discourse) e installa pnpm, rvm.

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

poi adatta e aggiungi la seguente configurazione al tuo .zshrc

/home/discourse/.zshrc

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

# Aggiungi RVM al PATH per lo scripting. Assicurati che questa sia l'ultima modifica alla variabile PATH.
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

Disconnettiti e riaccedi come discourse affinché .zshrc abbia effetto.

Installa node e ruby:

pnpm env use --global latest # installerà node 24.9 al momento della stesura. l'installazione ufficiale utilizza la 22
rvm get master
rvm install 3.4 # installerà ruby 3.4.6 al momento della stesura. l'installazione ufficiale utilizza la 3.3
rvm use 3.4 --default

preparare il db (e ripristinare un backup)

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;
# per ripristinare il database estratto da un backup:
$ gunzip < dump.sql.gz | psql discourse

Per ripristinare un backup, devi anche copiare le cartelle public e plugins.

installare Discourse

Ho consultato discourse_docker/templates/web.template.yml at 20e33fbfd98d3b8d9c57f7a111beff8aa51a5b98 · discourse/discourse_docker · GitHub

come utente 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

configura 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'

Esegui bundle / pnpm install, migrazione db, precompilazione asset e così via. Questo è anche il modo per aggiornare Discourse e i plugin.

come utente 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

Non voglio usare unicorn. Heroku consiglia di utilizzare il server web Puma invece di Unicorn. Ecco il mio config/puma.rb scritto dopo aver consultato config/unicorn.conf.rb:

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"

# Carica il logger logstash se abilitato
if enable_logstash_logger
  require_relative "../lib/discourse_logstash_logger"
  FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
  # Nota: potresti dover adattare l'inizializzazione del logger per Puma
  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

# Numero di worker (processi)
workers ENV.fetch("PUMA_WORKERS", 6).to_i

# Imposta la directory
directory discourse_path

# Bind all'indirizzo e alla porta specificati
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'

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

# File di stato - utilizzato da pumactl
state_path "#{discourse_path}/tmp/pids/puma.state"

# Configurazione specifica per ambiente
if ENV["RAILS_ENV"] == "production"
  # Timeout di produzione
  worker_timeout 30
else
  # Timeout di sviluppo
  worker_timeout ENV.fetch("PUMA_TIMEOUT", 60).to_i
end

# Precarica l'applicazione
preload_app!

# Gestisci l'avvio e lo spegnimento dei worker
before_fork do
  Discourse.preload_rails!
  Discourse.before_fork

  # Controllo supervisor
  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

  # Worker Sidekiq
  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
        # Ritarda la riapertura del log di Sidekiq
        sleep 1
        Demon::Sidekiq.kill("USR2")
      end
    end
  end

  # Demone di sincronizzazione email
  if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
    puts "starting up EmailSync demon"
    Demon::EmailSync.start(1)
  end

  # Demoni dei plugin
  DiscoursePluginRegistry.demon_processes.each do |demon_class|
    puts "starting #{demon_class.prefix} demon"
    demon_class.start(1)
  end

  # Thread di monitoraggio dei demoni
  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

  # Chiudi la connessione Redis
  Discourse.redis.close
end

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

# Gestione del timeout del worker
worker_timeout 30

# Opzioni worker di basso livello
threads 8, 32

Per eseguire Discourse, esegui puma -C config/puma.rb

Usando systemd, puoi avviarlo all’avvio e riavviarlo in caso di errore. Ecco il file del servizio:

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

[Service]
Type=simple
User=discourse
Group=discourse
WorkingDirectory=/var/www/discourse
# richiede l'esecuzione di `rvm 3.4.6 --default` prima di avviare questo servizio
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'

# Configurazione riavvio
Restart=always
RestartSec=5s

# Misure di sicurezza di base
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only

[Install]
WantedBy=multi-user.target

Ora il server Puma ascolta su 127.0.0.1:3000. Adatta il file di configurazione nginx da Docker:

/etc/nginx/sites-enabled/discourse.conf
# Altri tipi MIME che desideri che nginx gestisca vanno qui
types {
    text/csv csv;
    #application/wasm wasm;
}

upstream discourse { server 127.0.0.1:3000; }

# inactive significa che manteniamo i dati per 1440 minuti indipendentemente dall'ultimo accesso (1 settimana)
# levels significa che è una gerarchia profonda 2 perché possiamo avere molti file
# max_size limita la dimensione della cache
proxy_cache_path /var/nginx/cache inactive=1440m levels=1:2 keys_zone=one:10m max_size=600m;

# Aumentato dal valore predefinito per accommodating grandi cookie durante i flussi oAuth2
# come in https://meta.discourse.org/t/x/74060 e grandi intestazioni CSP e Link (preload)
proxy_buffer_size 32k;
proxy_buffers 4 32k;

# Aumentato dal valore predefinito per consentire un grande volume di cookie nelle intestazioni delle richieste
# Discourse stesso cerca di minimizzare la dimensione dei cookie, ma non possiamo controllare altri cookie impostati da altri strumenti sullo stesso dominio.
large_client_header_buffers 4 32k;

# tenta di preservare il protocollo, deve essere nel contesto http
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"';

# Consenti di bypassare la cache da localhost
#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;

  # Scommenta e configura questa sezione per il supporto HTTPS
  # NOTA: inserisci il tuo certificato SSL nella directory principale di configurazione nginx (/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;

  # dimensione massima del file caricato (mantienila aggiornata quando cambi l'impostazione del sito corrispondente)
  client_max_body_size 128m ;


  # percorso alla directory pubblica di discourse
  set $public /var/www/discourse/public;

  # senza etag deboli non otteniamo alcun vantaggio dagli etag sui contenuti compressi dinamicamente
  # inoltre gli etag sono basati sul file in nginx e non sull'hash sha dei dati
  # usa le date, risolve il problema bene anche tra server diversi
  etag off;

  # previeni il download diretto dei backup
  location ^~ /backups/ {
    internal;
  }

  # bypassa lo stack rails con un economico 204 per le richieste favicon.ico
  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;
    }

    # una minima cache qui così non continuiamo a chiedere
    # a lungo termine dovremmo aumentare probabilmente a 1y
    location ~ ^/javascripts/ {
      expires 1d;
      add_header Cache-Control public,immutable;
      add_header Access-Control-Allow-Origin *;
    }

    location ~ ^/assets/(?<asset_path>.+)$ {
      expires 1y;
      # asset pipeline abilita questo
      brotli_static on;
      gzip_static on;
      add_header Cache-Control public,immutable;
      # HOOK nella posizione asset (usato per l'estendibilità)
      # TODO non credo che questo break sia necessario, interrompe solo il rewrite
      break;
    }

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

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

    location ~ ^/uploads/ {

      # NOTA: è davvero fastidioso che non possiamo semplicemente definire le intestazioni
      # a livello superiore e ereditare.
      #
      # proxy_set_header NON eredita, per progettazione, dobbiamo ripeterlo,
      # altrimenti le intestazioni non sono impostate correttamente
      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;

      ## regole opzionali anti-hotlinking per il caricamento
      #valid_referers none blocked mysite.com *.mysite.com;
      #if ($invalid_referer) { return 403; }

      # CSS personalizzato
      location ~ /stylesheet-cache/ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # questo ci permette di bypassare rails
      location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp|avif)$ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # SVG richiede un'intestazione aggiuntiva allegata
      location ~* \.(svg)$ {
      }
      # miniature e immagini ottimizzate
      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;
    }

    # Questo grande blocco è necessario per abilitare selettivamente
    # l'accelerazione per backup, avatar, sprite e così via.
    # vedi nota sulla ripetizione sopra
    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;

      # se Set-Cookie è nella risposta nulla viene memorizzato nella cache
      # questo è doppiamente negativo perché non stiamo passando last modified
      proxy_ignore_headers "Set-Cookie";
      proxy_hide_header "Set-Cookie";
      proxy_hide_header "X-Discourse-Username";
      proxy_hide_header "X-Runtime";

      # nota x-accel-redirect non può essere utilizzato con proxy_cache
      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;
    }

    # dobbiamo disabilitare il buffering per 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;
    }

    # questo significa che ogni file in public viene provato per primo
    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;
  }

}

Ora il tuo Discourse è accessibile da example.com:443.

manutenzione

Per accedere alla console rails, esegui semplicemente rails c come utente discourse in /var/www/discourse. Il comando discourse che si trova nella documentazione ufficiale è essenzialmente bundle exec script/discourse.

Per aggiornare Discourse, consulta #upgrade-cmd, quindi riavvia puma usando puma restart o puma phased-restart. Per la differenza, consulta puma/docs/restart.md at main · puma/puma · GitHub .

8 Mi Piace

Dovrebbe essere spostato in Community wiki > Sysadmins, forse?

Ciao. Grazie per aver condiviso.

Sto scrivendo uno script per installarlo su Debian 12 in un container LXC. È quasi finito e funziona bene. Lo pubblicherò quando sarà pronto.

Sono riuscito a visualizzare la prima pagina per la registrazione dell’amministratore. Ma l’email di conferma viene inviata a discourse@myhostname e a myhostname come server smtp, quindi è assurdo. Le variabili in .bashrc (o .zshrc), né in discourse.conf vengono prese in considerazione per l’invio dell’email. L’indirizzo email per lo sviluppatore è corretto ma tutti gli altri parametri sono sbagliati e non sono riuscito a cambiarli. Avresti qualche idea su come raggiungere questo obiettivo?

1 Mi Piace

Secondo il codice

SMTP dovrebbe essere configurato in config/discourse.conf.
Per me, ho queste righe al suo interno:

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

Hai controllato i log? Per questa installazione personalizzata, il log si trova in production.log, production.log, puma.stdout.log nella directory log.

Grazie mille per la tua risposta.

Ho già queste impostazioni in config/discourse.conf. Ho fatto esattamente quello che hai scritto tranne che con zsh.
I miei log non parlano di smtp, e l’unico (production.log) è

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)

I log del mio mail server non mostrano alcuna voce dal server discourse (funziona per tutti gli altri miei server (tutti i container lxc). Riesco a inviare un’email con successo tramite il comando del terminale mail.

con 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/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

il mio 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 è come il tuo .zhrc
modificare le voci developper_emails o db_password funziona (la corretta email viene visualizzata nella pagina di registrazione dell’amministratore del sito web), ma altri parametri smtp vengono ignorati.

Il tuo file config/puma.rb contiene alcuni errori (intorno alla riga con la porta 3000). Potresti fornirlo di nuovo?

Ho registrato l’admin con rails c e dopo di che ottengo la pagina “Oops…” sulla pagina iniziale. Nessuna pagina viene renderizzata correttamente.

Per favore aiutami.

@lion , prova questo:

# 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"

# Carica il logger logstash se abilitato
if enable_logstash_logger
  require_relative "../lib/discourse_logstash_logger"
  FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
  # Nota: Potrebbe essere necessario adattare l'inizializzazione del logger per Puma
  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

# Numero di worker (processi)
workers ENV.fetch("PUMA_WORKERS", 8).to_i

# Imposta la directory
directory discourse_path

# Associa all'indirizzo e alla porta specificati
bind ENV.fetch("PUMA_BIND", "tcp://#{ENV['PUMA_BIND_ALL'] ? '' : '127.0.0.1:'}3000")

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

# File di stato - utilizzato da pumactl
state_path "#{discourse_path}/tmp/pids/puma.state"

# Configurazione specifica dell'ambiente
if ENV["RAILS_ENV"] == "production"
  # Timeout di produzione
  worker_timeout 30
else
  # Timeout di sviluppo
  worker_timeout ENV.fetch("PUMA_TIMEOUT", 60).to_i
end

# Precarica l'applicazione
preload_app!

# Gestione dell'avvio e dello spegnimento dei worker
before_fork do
  Discourse.preload_rails!
  Discourse.before_fork

  # Controllo del supervisore
  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

  # Worker Sidekiq
  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
        # Ritarda la riapertura del log di Sidekiq
        sleep 1
        Demon::Sidekiq.kill("USR2")
      end
    end
  end

  # Demone di sincronizzazione email
  if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
    puts "starting up EmailSync demon"
    Demon::EmailSync.start(1)
  end

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

  # Thread di monitoraggio demone
  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

  # Chiudi la connessione Redis
  Discourse.redis.close
end

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

# Gestione del timeout dei worker
worker_timeout 30

# Opzioni worker di basso livello
threads 8, 32

Grazie. Ho dimenticato di dire che è dietro haproxy per più server.

  1. Ricevo “mix content” dalla console del browser. Cosa posso cambiare nel file nginx?
  2. Ricevo questo log su magick all’avvio del comando puma. È anche installato all’inizio.

==
/var/www/discourse/log/puma.stderr.log
<==
=== avvio puma: 2025-09-19 01:40:45 +0200 ===
OID sconosciuto 16720: impossibile riconoscere il tipo di ‘embeddings’. Verrà trattato come String.
#<Thread:0x00007fc59a2f5a78 /var/www/discourse/lib/discourse.rb:1190 run> terminato con eccezione (report_on_exception è true):
/var/www/discourse/lib/letter_avatar.rb:112:in ``‘: Nessun file o directory di questo tipo - 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!’
Ho letto che può essere una causa per cui le pagine non vengono mostrate.

Forzi l’SSL nelle impostazioni di Discourse?

Puoi eseguire magick --version come utente discourse? Per me l’output è:
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)

Sì, esattamente quello che stavo pensando.

Ho trovato molte dipendenze non soddisfatte durante l’installazione “manuale”. Sto scrivendo un Ansible per poter distribuire Discourse su più ambienti.

I miei sistemi sono AlmaLinux/Red Hat, ma condividerò quello che ho trovato quando lo riterrò quasi finito.

1 Mi Piace

In haproxy.cfg 443 è con crt, quindi crittografato, reindirizzato al container tramite 8080:

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 #aggiungendo “check ssl verify none” se ssl su nginx discourse

  1. In nginx/…/discourse.conf, ho copiato la versione di questo post e modificato la porta, il servername e la posizione dei file del certificato.
    => PB1: ho ottenuto contenuti misti e log con lo stesso problema di magick
    => PB2: sulla pagina “/confirm-email” ricevo un errore “sito non trovato” dal browser.

  2. Quindi ho modificato il file rimuovendo l’ssl sulla porta e commentando tutte le righe con “$thescheme”, per crittografare solo in haproxy.
    => Errore 502 sulla prima pagina, stessi log con magick

  3. Quindi ho installato magick da sorgente (7.1.2.3), aggiunto la cartella binaria a PATH in .bashrc per gli utenti root e discourse. Non ha funzionato con l’installazione tramite apt (nessun effetto come questo).
    => L’errore 502 è scomparso, ancora contenuti misti e “sito non trovato” dal browser.
    => I log mostrano ancora magick non trovato ma in modo diverso.

== /var/www/discourse/log/puma.stderr.log ==
=== puma startup: 2025-09-19 12:06:05 +0200 ===
OID sconosciuto 16720: impossibile riconoscere il tipo di ‘embeddings’. Verrà trattato come String.
#<Thread:0x00007f611a21c0f0 /var/www/discourse/lib/discourse.rb:1190 run> terminato con eccezione (report_on_exception è true):
/var/www/discourse/lib/letter_avatar.rb:112:in ``‘: Nessun file o 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] ATTENZIONE hook before_fork fallito con eccezione (Errno::ENOENT) Nessun file o directory - magick

Conclusione:
Come rendere magick trovabile da puma?
Come disabilitare ssl in nginx a causa di haproxy?

Sono riuscito a farlo funzionare come descritto nel mio primo post, ma qui senza ssl su nginx copiando nginx.config.sample cambiando solo hostname e porta.
Quindi sia con ssl che senza su nginx, ottengo la prima pagina per la registrazione dell’amministratore fino alla pagina di re-invio email, ma ENTRAMBI:

  1. visualizza “contenuto misto” (per file di immagini ad esempio),
  2. pagine diverse dalla registrazione dell’amministratore mostrano “Ooops…”,
  3. nessuna email di conferma viene inviata
  4. magick non viene ancora trovato

In uno dei tuoi post menzioni “container”.

Stai usando container per qualcuna delle parti coinvolte?

Oggi voglio finalizzare il mio deployment nell’ambiente preprod e forse avrò più informazioni da condividere.

Uguale a te :unamused_face:

  • Non invia l’email di conferma (sto usando mailtrap, sono sicuro che funzioni).
  • Creato utente admin con rake
  • Ops su / dopo questo…
  • Visto anche il problema di magick e magick è installato (sono abbastanza sicuro che manchi qualche rubygem…)

Lo sto distribuendo usando docker e poi confronterò. Odio docker :expressionless_face: ma sprecherò un sacco di tempo prezioso…

Conosco a malapena Docker (se non per niente!) e non ho dovuto conoscerlo per eseguire un forum Discourse in produzione. Seguendo le istruzioni supportate, otterrai ciò di cui hai bisogno.

Sì, ti credo. Ma ho bisogno di sapere cosa sto distribuendo e come funziona.

È il modo migliore per trovare e risolvere i problemi :squinting_face_with_tongue:

Perché avrai problemi, sempre forse non ora, ma sono sicuro che il futuro tu troverai qualcuno.

Comunque, dato che non riceverò supporto utilizzando metodi di installazione alternativi, seguirò le istruzioni ufficiali e cercherò di aiutare gli altri :winking_face_with_tongue:

Ho anche passato molto tempo su questi problemi.
Riguardo a Docker, guarda questo problema su Docker (un errore o una backdoor ?..), corretto al momento: https://youtu.be/dTqxNc1MVLE
Questo è uno dei motivi per cui non userei Docker.
Sto usando container lxd (lxc) e mi trovo bene. Installerò Docker su un container lxc all’inizio, ed esporterò il database più tardi quando l’installazione senza Docker su un lxc sarà possibile.

Bene, non sto forzando Discourse a usare SSL perché voglio che il mio haproxy lo gestisca, dato che il passthrough di haproxy non funziona con il protocollo HTTP per reindirizzare le richieste GET, e sto gestendo più siti web, quindi ho bisogno del protocollo HTTP in haproxy, il che richiede la gestione SSL sul lato haproxy. Vorrei evitare un doppio gateway SSL.
Quindi haproxy (su un lxc) ascolta sulla porta 443: reindirizza alla porta 8080 (senza SSL) al mio container Discourse (lxc).
È curioso che il campione di configurazione nginx dato nella cartella discourse sia configurato senza SSL e sulla porta 80, quindi dovrebbe funzionare bene, ma riscontro i problemi sopra menzionati.

Devi farlo per dire a Discourse di inviare solo link https. Non sta cambiando i reindirizzamenti, ma la necessità di essi.

Devi attivare l’impostazione “force https”.

HTTPS: Sì, ma per favore dimmi come attivarlo?

OTTIME NOTIZIE! HO RISOLTO I PROBLEMI!!! La causa era il programma magick. Dopo aver installato correttamente magick dalla versione sorgente 7 (non installare imagemagick con apt, è la versione 6), il mio sito discourse visualizza tutte le pagine correttamente! Email, registrazione admin, ecc. funzionano! Dopo aver risolto il problema del “contenuto misto”, pubblicherò il mio script per l’installazione su un container lxd-lxc (Debian 12) dietro haproxy. su un altro container lxd-lxc.

1 Mi Piace