Desplegar Discourse sin Docker

Aunque es más conveniente y seguro desplegar Discourse siguiendo la guía oficial de instalación, quiero profundizar en el contenedor y ver cómo se puede desplegar en Linux sin Docker. Quiero compartir los pasos solo para su información. Adáptelos y úselos bajo su propio riesgo.

Profundizando en cómo se ejecuta Discourse en un contenedor

Observo la salida de ./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

luego reviso /sbin/boot y /etc/service/unicorn/run y obtengo el comando principal para iniciar 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

Preparar el sistema

Para su información, uso Ubuntu 24.04 y zsh.

Siga la guía oficial de instalación de PG para instalar PostgreSQL desde el repositorio Apt de PostgreSQL. Instalé la versión 18, que funciona muy bien, aunque la instalación oficial utiliza la 15 al momento de escribir esto.

Instale redis (8.2 al momento de escribir esto, la instalación oficial usa 7.0), nginx y cree el usuario dedicado 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

Instale ImageMagick 7 (uso IMEI) y verifique la versión. La mía es:

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

luego, cambie de usuario (su - discourse) e instale pnpm y rvm.

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

luego adapte y agregue la siguiente configuración a su .zshrc:

/home/discourse/.zshrc

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

# Agregar RVM al PATH para scripting. Asegúrese de que este sea el último cambio de variable 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

Cierre sesión y vuelva a iniciarla como discourse para que .zshrc surta efecto.

Instale node y ruby:

pnpm env use --global latest # instalará node 24.9 al momento de escribir esto. La instalación oficial usa 22
rvm get master
rvm install 3.4 # instalará ruby 3.4.6 al momento de escribir esto. La instalación oficial usa 3.3
rvm use 3.4 --default

Preparar la base de datos (y restaurar una copia de seguridad)

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;
# para restaurar la base de datos extraída de una copia de seguridad:
$ gunzip < dump.sql.gz | psql discourse

Para restaurar una copia de seguridad, también debe copiar las carpetas public y plugins.

Instalar Discourse

Consulté discourse_docker/templates/web.template.yml at 20e33fbfd98d3b8d9c57f7a111beff8aa51a5b98 · discourse/discourse_docker · GitHub

como usuario 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

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

Ejecute bundle / pnpm install, migración de la base de datos, precompilación de activos, etc. Esto también es cómo se actualiza Discourse y los plugins.

as user 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

No quiero usar unicorn. Heroku recomienda usar el servidor web Puma en lugar de Unicorn. Aquí está mi config/puma.rb escrito después de consultar 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"

# Cargar el logger de logstash si está habilitado
if enable_logstash_logger
  require_relative "../lib/discourse_logstash_logger"
  FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
  # Nota: Puede que necesite adaptar la inicialización del logger para 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

# Número de workers (procesos)
workers ENV.fetch("PUMA_WORKERS", 6).to_i

# Establecer el directorio
directory discourse_path

# Vincular a la dirección y puerto especificados
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'

# Ubicación del archivo PID
FileUtils.mkdir_p("#{discourse_path}/tmp/pids")
pidfile ENV.fetch("PUMA_PID_PATH", "#{discourse_path}/tmp/pids/puma.pid")

# Archivo de estado - utilizado por pumactl
state_path "#{discourse_path}/tmp/pids/puma.state"

# Configuración específica del entorno
if ENV["RAILS_ENV"] == "production"
  # Tiempo de espera en producción
  worker_timeout 30
else
  # Tiempo de espera en desarrollo
  worker_timeout ENV.fetch("PUMA_TIMEOUT", 60).to_i
end

# Precargar la aplicación
preload_app!

# Manejar el inicio y apagado de workers
before_fork do
  Discourse.preload_rails!
  Discourse.before_fork

  # Verificación del 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

  # Workers de 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
        # Retrasar la reapertura del registro de Sidekiq
        sleep 1
        Demon::Sidekiq.kill("USR2")
      end
    end
  end

  # Demonio de sincronización de correo
  if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
    puts "starting up EmailSync demon"
    Demon::EmailSync.start(1)
  end

  # Procesos demonio de plugins
  DiscoursePluginRegistry.demon_processes.each do |demon_class|
    puts "starting #{demon_class.prefix} demon"
    demon_class.start(1)
  end

  # Hilo de monitoreo del demonio
  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

  # Cerrar conexión a Redis
  Discourse.redis.close
end

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

# Manejo de tiempo de espera del worker
worker_timeout 30

# Opciones de bajo nivel del worker
threads 8, 32

Para ejecutar Discourse, ejecute puma -C config/puma.rb

Usando systemd, puede ejecutarlo al arrancar y reiniciarlo en caso de fallo. Aquí está el archivo de servicio:

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

[Service]
Type=simple
User=discourse
Group=discourse
WorkingDirectory=/var/www/discourse
# requiere ejecutar `rvm 3.4.6 --default` antes de ejecutar este servicio
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'

# Configuración de reinicio
Restart=always
RestartSec=5s

# Medidas básicas de seguridad
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only

[Install]
WantedBy=multi-user.target

Ahora el servidor Puma escucha en 127.0.0.1:3000. Adapte el archivo de configuración de nginx desde Docker:

/etc/nginx/sites-enabled/discourse.conf
# Tipos MIME adicionales que desea que nginx maneje van aquí
types {
    text/csv csv;
    #application/wasm wasm;
}

upstream discourse { server 127.0.0.1:3000; }

# inactivo significa que mantenemos las cosas durante 1440 minutos independientemente del último acceso (1 semana)
# niveles significa que es una jerarquía de 2 niveles porque podemos tener muchos archivos
# max_size limita el tamaño de la caché
proxy_cache_path /var/nginx/cache inactive=1440m levels=1:2 keys_zone=one:10m max_size=600m;

# Aumentado desde el valor predeterminado para acomodar cookies grandes durante flujos de oAuth2
# como en https://meta.discourse.org/t/x/74060 y encabezados CSP y Link (preload) grandes
proxy_buffer_size 32k;
proxy_buffers 4 32k;

# Aumentado desde el valor predeterminado para permitir un gran volumen de cookies en los encabezados de solicitud
# Discourse intenta minimizar el tamaño de las cookies, pero no podemos controlar otras cookies establecidas por otras herramientas en el mismo dominio.
large_client_header_buffers 4 32k;

# intentar preservar el protocolo, debe estar en el contexto 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"';

# Permitir omitir la caché desde 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;

  # Descomente y configure esta sección para soporte HTTPS
  # NOTA: Coloque su certificado ssl en su directorio principal de configuración de 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;

  # tamaño máximo de carga de archivo (mantener actualizado al cambiar la configuración del sitio correspondiente)
  client_max_body_size 128m ;


  # ruta al directorio público de discourse
  set $public /var/www/discourse/public;

  # sin etags débiles no obtenemos ningún beneficio de los etags en contenido comprimido dinámicamente
  # además, los etags se basan en el archivo en nginx, no en el hash de los datos
  # use fechas, resuelve el problema bien incluso entre servidores
  etag off;

  # prevenir la descarga directa de copias de seguridad
  location ^~ /backups/ {
    internal;
  }

  # omitir la pila de rails con un 204 barato para solicitudes de 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;
    }

    # algo de caché mínima aquí para no seguir preguntando
    # a largo plazo deberíamos aumentar probablemente a 1y
    location ~ ^/javascripts/ {
      expires 1d;
      add_header Cache-Control public,immutable;
      add_header Access-Control-Allow-Origin *;
    }

    location ~ ^/assets/(?<asset_path>.+)$ {
      expires 1y;
      # la tubería de activos habilita esto
      brotli_static on;
      gzip_static on;
      add_header Cache-Control public,immutable;
      # ENGAÑAR en la ubicación de activos (usado para extensibilidad)
      # TODO No creo que este break sea necesario, solo sale de rewrite
      break;
    }

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

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

    location ~ ^/uploads/ {

      # NOTA: Es realmente molesto que no podamos definir encabezados
      # en el nivel superior e heredar.
      #
      # proxy_set_header NO hereda, por diseño, debemos repetirlo,
      # de lo contrario los encabezados no se establecen correctamente
      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;

      ## reglas opcionales anti-hotlinking de carga
      #valid_referers none blocked mysite.com *.mysite.com;
      #if ($invalid_referer) { return 403; }

      # CSS personalizado
      location ~ /stylesheet-cache/ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # esto nos permite omitir rails
      location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp|avif)$ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # SVG necesita un encabezado adicional adjunto
      location ~* \.(svg)$ {
      }
      # miniaturas e imágenes optimizadas
      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;
    }

    # Este gran bloque es necesario para que podamos habilitar selectivamente
    # la aceleración para copias de seguridad, avatares, sprites, etc.
    # ver nota sobre repetición arriba
    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;

      # si Set-Cookie está en la respuesta, nada se almacena en caché
      # esto es doblemente malo porque no estamos pasando last modified
      proxy_ignore_headers "Set-Cookie";
      proxy_hide_header "Set-Cookie";
      proxy_hide_header "X-Discourse-Username";
      proxy_hide_header "X-Runtime";

      # note que x-accel-redirect no se puede usar 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;
    }

    # necesitamos buffering desactivado para el bus de mensajes
    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;
    }

    # esto significa que se prueba cada archivo en público primero
    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;
  }

}

Ahora su Discourse se puede acceder desde example.com:443.

Mantenimiento

Para acceder a la consola de rails, simplemente ejecute rails c como usuario discourse en /var/www/discourse. El comando discourse que se puede encontrar en la documentación oficial es básicamente bundle exec script/discourse.

Para actualizar Discourse, consulte #upgrade-cmd, luego reinicie puma usando puma restart o puma phased-restart. Para la diferencia, consulte puma/docs/restart.md at main · puma/puma · GitHub.

8 Me gusta

¿Debería moverse esto a Community wiki > Sysadmins, quizás?

Hola. Gracias por compartir.

Estoy escribiendo un script para instalar esto en Debian 12 en un contenedor LXC. Casi está terminado y funciona bien. Lo publicaré cuando esté listo.

He logrado mostrar la primera página para el registro de administradores. Pero el correo electrónico de confirmación se envía a discourse@myhostname y a myhostname como smtp_server, por lo que es absurdo. Las variables en .bashrc (o .zshrc), ni en discourse.conf, se tienen en cuenta para enviar el correo electrónico. La dirección de correo electrónico para el desarrollador es correcta, pero todos los demás parámetros son incorrectos y no he podido cambiarlos. ¿Tendrías alguna idea de cómo lograr esto?

1 me gusta

Según el código

SMTP debe configurarse en config/discourse.conf.
Para mí, tengo estas líneas en él:

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

¿Has revisado los registros? Para esta instalación personalizada, el registro está en production.log, production.log, puma.stdout.log en el directorio log.

Muchas gracias por tu respuesta.

Ya tengo esta configuración en config/discourse.conf. Hice exactamente lo que escribiste, excepto con zsh.
Mis logs no hablan de smtp, y el único (production.log) es

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)

Los logs de mi servidor de correo no muestran ninguna entrada del servidor discourse (funciona para todos mis otros servidores (todos los contenedores lxc). Puedo enviar un correo electrónico correctamente a través del comando de terminal 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: (:check_mark:) hot (:multiply:) phased (:multiply:) 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

mi 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 es como tu .zhrc
modificar las entradas de developper_emails o db_password funciona (el correo electrónico correcto se muestra en la página de registro de administración del sitio web), pero otros parámetros smtp son ignorados.

Tu archivo config/puma.rb contiene algunos errores (alrededor de la línea con el puerto 3000). ¿Podrías proporcionarlo de nuevo?

Registré al administrador con rails c y después de eso obtengo la página “Oops…” en la página de inicio. Ninguna página se renderiza correctamente.

Por favor, ayúdame.

@lion , prueba este:\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”

Cargar el logger de logstash si está habilitado

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

Nota: Es posible que necesites adaptar la inicialización del logger para 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

Número de workers (procesos)

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

Establecer el directorio

directory discourse_path

Vincular a la dirección y puerto especificados

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

Ubicación del archivo PID

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

Archivo de estado - utilizado por pumactl

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

Configuración específica del entorno

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

Tiempo de espera de producción

worker_timeout 30
else

Tiempo de espera de desarrollo

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

Preload de la aplicación

preload_app!

Manejo del arranque y apagado de workers

before_fork do
Discourse.preload_rails!
Discourse.before_fork

Comprobación del 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 “Matar mi supervisor se ha ido”
Process.kill “TERM”, Process.pid
end
sleep 2
end
end
end

Workers de Sidekiq

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

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
    # Retrasar la reapertura del log de Sidekiq
    sleep 1
    Demon::Sidekiq.kill("USR2")
  end
end

end

Demonio de sincronización de correo electrónico

if ENV[“DISCOURSE_ENABLE_EMAIL_SYNC_DEMON”] == “true”
puts “iniciando demonio EmailSync”
Demon::EmailSync.start(1)
end

Demonios de plugins

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

Hilo de monitoreo de demonios

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 en la comprobación del latido de los procesos demonio: #{e}\n#{e.backtrace.join("\n")}")
  end
end

end

Cerrar la conexión de Redis

Discourse.redis.close
end

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

Manejo del tiempo de espera del worker

worker_timeout 30

Opciones de worker de bajo nivel

threads 8, 32

Gracias. Olvidé decir que está detrás de haproxy para múltiples servidores.

  1. Recibo “mix content” de la consola del navegador. ¿Qué puedo cambiar en el archivo nginx?
  2. Recibo este registro sobre magick al arrancar el comando puma. Aunque está instalado desde el principio.

==
/var/www/discourse/log/puma.stderr.log
<==
=== inicio de puma: 2025-09-19 01:40:45 +0200 ===
OID desconocido 16720: no se pudo reconocer el tipo de ‘embeddings’. Se tratará como String.
#<Thread:0x00007fc59a2f5a78 /var/www/discourse/lib/discourse.rb:1190 run> terminado con excepción (report_on_exception es true):
/var/www/discourse/lib/letter_avatar.rb:112:in ``‘: No existe tal archivo o directorio - 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!’
He leído que puede ser una causa de que no se muestren las páginas.

¿Fuerzas SSL en la configuración de Discourse?

¿Puedes ejecutar magick --version como usuario de discourse? Para mí, la salida es:
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í, exactamente lo que estaba pensando.

He encontrado muchas dependencias no cumplidas mientras las instalaba “manualmente”. Estoy escribiendo un Ansible para poder desplegar Discourse en múltiples entornos.

Mis sistemas son AlmaLinux/Red Hat, pero compartiré lo que encuentre cuando considere que está casi hecho.

1 me gusta

En haproxy.cfg, el puerto 443 está cifrado con crt y redirigido al contenedor a través de 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 # agregando “check ssl verify none” si ssl en nginx discourse

  1. En nginx/…/discourse.conf, copié la versión de esta publicación y modifiqué el puerto, el servername y la ubicación de los archivos del certificado.
    => PB1: obtuve contenido mixto y registros con el mismo problema de magick
    => PB2: en la página “/confirm-email” obtengo un error del navegador “sitio no encontrado”.

  2. Luego modifiqué el archivo eliminando el ssl en el puerto y comenté todas las líneas con “$thescheme”, para cifrar solo en haproxy.
    => Error 502 en la primera página, mismos registros con magick

  3. Luego instalé magick desde la fuente (7.1.2.3), agregué la carpeta binaria a PATH en .bashrc para los usuarios root y discourse. No funcionó con la instalación a través de apt (sin efecto como este).
    => El error 502 desapareció, todavía contenido mixto y “sitio no encontrado” del navegador.
    => Los registros todavía indican que magick no se encuentra, pero de manera diferente.

== /var/www/discourse/log/puma.stderr.log ==
=== inicio de puma: 2025-09-19 12:06:05 +0200 ===
OID desconocido 16720: no se pudo reconocer el tipo de ‘embeddings’. Se tratará como String.
#<Thread:0x00007f611a21c0f0 /var/www/discourse/lib/discourse.rb:1190 run> terminado con excepción (report_on_exception es true):
/var/www/discourse/lib/letter_avatar.rb:112:in ``‘: No existe tal archivo o directorio - 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] ADVERTENCIA hook before_fork falló con excepción (Errno::ENOENT) No existe tal archivo o directorio - magick

Conclusión:
¿Cómo hacer que puma encuentre magick?
¿Cómo deshabilitar ssl en nginx debido a haproxy?

He logrado que funcione como se describe en mi primera publicación, pero aquí sin ssl en nginx copiando el nginx.config.sample con solo cambiar el nombre de host y el puerto.
Entonces, tanto con ssl como sin él en nginx, obtengo la primera página para el registro de administrador hasta la página de reenvío de correo electrónico, pero AMBAS:

  1. Muestra “contenido mixto” (para archivos de imágenes, por ejemplo),
  2. Otras páginas que no son el registro de administrador muestran “Ooops…”
  3. No se envían correos electrónicos de confirmación
  4. Magick todavía no se encuentra

En alguna de tus publicaciones mencionas un contenedor.

¿Estás utilizando contenedores para alguna de las partes involucradas?

Hoy quiero finalizar mi implementación en el entorno de preproducción y quizás tenga más información para compartir.

Igual que tú :unamused_face:

  • No envía el correo electrónico de confirmación (estoy usando mailtrap, estoy seguro de que funciona).
  • Creado usuario administrador con rake
  • Ups en / después de eso…
  • También he visto un problema con magick y magick está instalado (estoy bastante seguro de que falta algún rubygem…)

Lo estoy desplegando usando docker y luego compararé. Odio docker :expressionless_face: pero estoy perdiendo mucho tiempo valioso…

Apenas conozco Docker (¡si es que lo conozco!) y no tuve que conocerlo para ejecutar un foro de Discourse en producción. Simplemente siguiendo las instrucciones admitidas, obtendrás lo que necesitas.

Sí, te creo. Pero necesito saber qué estoy implementando y cómo funciona.

Es la mejor manera de encontrar y resolver problemas :squinting_face_with_tongue:

Porque tendrás problemas, siempre, quizás no ahora, pero estoy seguro de que el futuro tú encontrarás a alguien.

De todos modos, como no obtendré soporte usando métodos de instalación alternativos, seguiré las instrucciones oficiales y trataré de ayudar a otros :winking_face_with_tongue:

También dediqué mucho tiempo a estos problemas.
Sobre Docker, mira este problema en Docker (¿un error o una puerta trasera?..), corregido por el momento: https://youtu.be/dTqxNc1MVLE
Esa es una de las razones por las que no usaría Docker.
Estoy usando contenedores lxd (lxc) y me siento bien con ellos. Voy a instalar Docker en un contenedor lxc al principio y exportaré la base de datos más tarde cuando la instalación sin Docker en un lxc sea posible.

Bueno, no estoy forzando a Discourse a usar SSL porque quiero que mi haproxy lo haga, ya que el traspaso de haproxy no funciona con el protocolo HTTP para redirigir las solicitudes GET, y estoy manejando varios sitios web, por lo que necesito el protocolo HTTP en haproxy, lo que requiere el manejo de SSL en el lado de haproxy. Me gustaría evitar una doble pasarela SSL.
Entonces, haproxy (en un lxc) escucha en 443: redirige a 8080 (sin SSL) a mi contenedor de Discourse (lxc).
Lo curioso es que la muestra de configuración de nginx dada en la carpeta de Discourse está configurada sin SSL y en el puerto 80, por lo que debería funcionar bien, pero tengo los problemas mencionados anteriormente.

Necesitas hacer eso para indicarle a Discourse que envíe solo enlaces https. No se trata de cambiar las redirecciones, sino de la necesidad de ellas.

Necesitas activar la configuración de forzar https.

HTTPS: Sí, pero por favor, dime cómo activarlo.

¡GRANDES NOTICIAS! ¡HE RESUELTO LOS PROBLEMAS! La fuente era el programa magick. Después de la correcta instalación de magick desde la versión fuente 7 (no instales imagemagick con apt, es la versión 6), ¡mi sitio web de discourse muestra todas las páginas correctamente! ¡Los correos electrónicos, el registro de administradores, etc. funcionan! Después de resolver el problema de “contenido mixto”, publicaré mi script para la instalación en un contenedor lxd-lxc (Debian 12) detrás de haproxy.en otro contenedor lxd-lxc.

1 me gusta