Implantar Discourse sem Docker

Embora seja mais conveniente e seguro implantar o Discourse seguindo o guia de instalação oficial, quero mergulhar mais fundo no contêiner e ver como ele pode ser implantado no Linux sem Docker. Quero compartilhar os passos apenas para sua informação. Adapte e use por sua conta e risco.

Mergulhe em como o Discourse é executado no contêiner

Eu examino a saída 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

então examino /sbin/boot e /etc/service/unicorn/run e obtenho o comando principal para iniciar o 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 o sistema

Para sua informação, uso Ubuntu 24.04 e zsh.

Siga o guia de instalação oficial do PG para instalar o postgres do repositório Apt do PostgreSQL. Instalei a versão 18, que funciona muito bem, embora a instalação oficial use a 15 no momento da escrita.

Instale redis (8.2 no momento da escrita, a instalação oficial usa 7.0), nginx e crie o usuário 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 o ImageMagick 7 (uso IMEI) e verifique a versão. A minha é:

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

então, altere o usuário (su - discourse) e instale o pnpm e o rvm.

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

então adapte e adicione a seguinte configuração ao seu .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'

# Adicione o RVM ao PATH para scripts. Certifique-se de que esta seja a última alteração de variável 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

Saia e entre novamente como discourse para que o .zshrc tenha efeito.

Instale o node e o ruby:

pnpm env use --global latest # instalará o node 24.9 no momento da escrita. a instalação oficial usa 22
rvm get master
rvm install 3.4 # instalará o ruby 3.4.6 no momento da escrita. a instalação oficial usa 3.3
rvm use 3.4 --default

preparar o banco de dados (e restaurar um 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;
# para restaurar o banco de dados extraído de um backup:
$ gunzip < dump.sql.gz | psql discourse

Para restaurar o backup, você também precisa copiar as pastas public e plugins.

instalar o Discourse

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

como usuário 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 o 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'

Faça o bundle / pnpm install, migração do banco de dados, pré-compilação de assets, etc. Isso também é como você atualiza o Discourse e os plugins.

como usuário 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

Eu não quero usar o unicorn. O Heroku recomenda usar o servidor web Puma em vez do Unicorn. Aqui está meu config/puma.rb escrito após 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"

# Carregue o logger logstash se estiver habilitado
if enable_logstash_logger
  require_relative "../lib/discourse_logstash_logger"
  FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
  # Nota: Você pode precisar adaptar a inicialização do logger para o 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 (processos)
workers ENV.fetch("PUMA_WORKERS", 6).to_i

# Defina o diretório
directory discourse_path

# Vincule ao endereço e porta 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'

# Localização do arquivo PID
FileUtils.mkdir_p("#{discourse_path}/tmp/pids")
pidfile ENV.fetch("PUMA_PID_PATH", "#{discourse_path}/tmp/pids/puma.pid")

# Arquivo de estado - usado pelo pumactl
state_path "#{discourse_path}/tmp/pids/puma.state"

# Configuração específica do ambiente
if ENV["RAILS_ENV"] == "production"
  # Timeout de produção
  worker_timeout 30
else
  # Timeout de desenvolvimento
  worker_timeout ENV.fetch("PUMA_TIMEOUT", 60).to_i
end

# Pré-carregue a aplicação
preload_app!

# Lidere com a inicialização e desligamento do worker
before_fork do
  Discourse.preload_rails!
  Discourse.before_fork

  # Verificação do 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 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
        # Atraso na reabertura do log do Sidekiq
        sleep 1
        Demon::Sidekiq.kill("USR2")
      end
    end
  end

  # Demon de sincronização de e-mail
  if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
    puts "starting up EmailSync demon"
    Demon::EmailSync.start(1)
  end

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

  # Thread de monitoramento do demon
  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

  # Feche a conexão Redis
  Discourse.redis.close
end

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

# Tratamento de timeout do worker
worker_timeout 30

# Opções de baixo nível do worker
threads 8, 32

Para executar o Discourse, execute puma -C config/puma.rb

Usando systemd, você pode executá-lo na inicialização e reiniciá-lo em caso de falha. Aqui está o arquivo de serviço:

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

[Service]
Type=simple
User=discourse
Group=discourse
WorkingDirectory=/var/www/discourse
# requer a execução de `rvm 3.4.6 --default` antes que este serviço seja executado
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'

# Configuração de reinício
Restart=always
RestartSec=5s

# Medidas básicas de segurança
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only

[Install]
WantedBy=multi-user.target

Agora o servidor Puma está escutando em 127.0.0.1:3000. Adapte o arquivo de configuração do nginx a partir do Docker:

/etc/nginx/sites-enabled/discourse.conf
# Tipos MIME adicionais que você deseja que o nginx processe vão aqui
types {
    text/csv csv;
    #application/wasm wasm;
}

upstream discourse { server 127.0.0.1:3000; }

# inativo significa que mantemos as coisas por 1440m minutos independentemente do último acesso (1 semana)
# níveis significa que é uma hierarquia de 2 profundidade porque podemos ter muitos arquivos
# max_size limita o tamanho do cache
proxy_cache_path /var/nginx/cache inactive=1440m levels=1:2 keys_zone=one:10m max_size=600m;

# Aumentado do valor padrão para acomodar cookies grandes durante fluxos oAuth2
# como em https://meta.discourse.org/t/x/74060 e cabeçalhos CSP e Link (preload) grandes
proxy_buffer_size 32k;
proxy_buffers 4 32k;

# Aumentado do valor padrão para permitir um grande volume de cookies nos cabeçalhos de solicitação
# O próprio Discourse tenta minimizar o tamanho do cookie, mas não podemos controlar outros cookies definidos por outras ferramentas no mesmo domínio.
large_client_header_buffers 4 32k;

# tentar preservar o protocolo, deve estar no 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 contornar o cache do 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 e configure esta seção para suporte HTTPS
  # NOTA: Coloque seu certificado ssl no seu diretório principal de configuração do 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;

  # tamanho máximo de upload de arquivo (mantenha atualizado ao alterar a configuração do site correspondente)
  client_max_body_size 128m ;


  # caminho para o diretório público do discourse
  set $public /var/www/discourse/public;

  # sem etags fracos, não temos benefício algum de etags em conteúdo dinamicamente comprimido
  # além disso, os etags são baseados no arquivo no nginx, não no sha dos dados
  # use datas, isso resolve o problema bem mesmo entre servidores
  etag off;

  # prevenir download direto de backups
  location ^~ /backups/ {
    internal;
  }

  # contorne a pilha rails com um 204 barato para solicitações 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;
    }

    # algum cache mínimo aqui para não ficarmos perguntando
    # a longo prazo, devemos aumentar provavelmente para 1y
    location ~ ^/javascripts/ {
      expires 1d;
      add_header Cache-Control public,immutable;
      add_header Access-Control-Allow-Origin *;
    }

    location ~ ^/assets/(?<asset_path>.+)$ {
      expires 1y;
      # o pipeline de ativos habilita isso
      brotli_static on;
      gzip_static on;
      add_header Cache-Control public,immutable;
      # GANCHO no local de ativos (usado para extensibilidade)
      # TODO Não acho que este break seja necessário, ele apenas sai do rewrite
      break;
    }

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

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

    location ~ ^/uploads/ {

      # NOTA: é realmente irritante que não possamos definir cabeçalhos
      # no nível superior e herdar.
      #
      # proxy_set_header NÃO herda, por design, devemos repeti-lo,
      # caso contrário, os cabeçalhos não são definidos corretamente
      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;

      ## regras opcionais de anti-hotlinking de upload
      #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;
      }
      # isso nos permite contornar o rails
      location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp|avif)$ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # SVG precisa de um cabeçalho extra anexado
      location ~* \.(svg)$ {
      }
      # miniaturas e imagens otimizadas
      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 grande bloco é necessário para que possamos ativar seletivamente
    # aceleração para backups, avatares, sprites e assim por diante.
    # veja a nota sobre repetição acima
    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 estiver na resposta, nada será armazenado em cache
      # isso é duplamente ruim porque não estamos passando o último modificado
      proxy_ignore_headers "Set-Cookie";
      proxy_hide_header "Set-Cookie";
      proxy_hide_header "X-Discourse-Username";
      proxy_hide_header "X-Runtime";

      # observe que x-accel-redirect não pode ser usado com 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;
    }

    # precisamos desligar o buffering para 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;
    }

    # isso significa que cada arquivo em public é tentado primeiro
    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;
  }

}

Agora seu Discourse pode ser acessado em example.com:443.

manutenção

Para acessar o console do rails, simplesmente execute rails c como usuário discourse em /var/www/discourse. O comando discourse que pode ser encontrado na documentação oficial é basicamente bundle exec script/discourse.

Para atualizar o Discourse, consulte #upgrade-cmd, depois reinicie o puma usando puma restart ou puma phased-restart. Para a diferença, consulte puma/docs/restart.md at main · puma/puma · GitHub.

8 curtidas

Isso deveria ser movido para Community wiki > Sysadmins, talvez?

Olá. Obrigado por compartilhar.

Estou escrevendo um script para isso para ser instalado no Debian 12 em um contêiner LXC. Está quase pronto e funciona bem. Publicarei quando estiver pronto.

Consegui exibir a primeira página para o registro do administrador. Mas o e-mail de confirmação é enviado para discourse@myhostname e para myhostname como smtp_server, então é um absurdo. As variáveis em .bashrc (ou .zshrc), nem em discourse.conf são levadas em consideração para o envio do e-mail. O email_adress para o desenvolvedor está correto, mas todos os outros parâmetros estão errados e não consegui alterá-los. Você teria alguma ideia de como conseguir isso?

1 curtida

De acordo com o código

O SMTP deve ser configurado em config/discourse.conf.
Para mim, tenho estas linhas nele:

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

Você já verificou os logs? Para esta instalação personalizada, o log está em production.log, production.log, puma.stdout.log no diretório log.

Muito obrigado pela sua resposta.

Já tenho essas configurações em config/discourse.conf. Fiz exatamente o que você escreveu, exceto com zsh.
Meus logs não falam sobre smtp, e o único (production.log) é

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

Os logs do meu servidor de e-mail não mostram nenhuma entrada do servidor discourse (funciona para todos os meus outros servidores (todos os contêineres lxc). Posso enviar um e-mail com sucesso via comando de terminal mail.

com puma -C config/puma.rb:

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

meu discourse.conf:

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

.bashrc é como seu .zhrc
modificar as entradas de developer_emails ou db_password funciona (o e-mail correto é exibido na página de registro de administrador do site), mas outros parâmetros smtp são ignorados.

Seu arquivo config/puma.rb contém alguns erros (por volta da linha com a porta 3000). Você poderia fornecê-lo novamente?

Eu registrei o admin com rails c e depois disso recebo a página “Oops…” na página inicial. Nenhuma página é renderizada corretamente.

Por favor, me ajude.

@lion , tente 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”

Carrega o logger logstash se habilitado

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

Nota: Você pode precisar adaptar a inicialização do logger para o 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 (processos)

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

Define o diretório

directory discourse_path

Vincula ao endereço e porta especificados

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

Localização do arquivo PID

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

Arquivo de estado - usado pelo pumactl

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

Configuração específica do ambiente

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

Timeout de produção

worker_timeout 30
else

Timeout de desenvolvimento

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

Pré-carrega a aplicação

preload_app!

Lida com a inicialização e o desligamento do worker

before_fork do
Discourse.preload_rails!
Discourse.before_fork

Verificação do 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 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
    # Atraso na reabertura do log do Sidekiq
    sleep 1
    Demon::Sidekiq.kill("USR2")
  end
end

end

Demon de sincronização de e-mail

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

Demônios de plugins

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

Thread de monitoramento de demônios

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

Fecha a conexão Redis

Discourse.redis.close
end

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

Tratamento de timeout do worker

worker_timeout 30

Opções de worker de baixo nível

threads 8, 32

Obrigado. Esqueci de dizer que está atrás do haproxy para vários servidores.

  1. Recebo “mix content” do console do navegador. O que posso mudar no arquivo nginx?
  2. Recebo este log sobre magick na inicialização do comando puma. Ele também está instalado no início.

==
/var/www/discourse/log/puma.stderr.log
<==
=== inicialização do puma: 2025-09-19 01:40:45 +0200 ===
OID desconhecido 16720: falha ao reconhecer o tipo de ‘embeddings’. Ele será tratado como String.
#<Thread:0x00007fc59a2f5a78 /var/www/discourse/lib/discourse.rb:1190 run> terminou com exceção (report_on_exception é 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!’
Li que isso pode ser uma causa para as páginas não serem exibidas.

Você força o SSL nas configurações do Discourse?

Você pode executar magick --version como usuário discourse? Para mim, a saída é:
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)

Sim, exatamente o que eu estava pensando.

Encontrei muitas dependências não atendidas durante a instalação “manual”. Estou escrevendo um Ansible para poder implantar o Discourse em múltiplos ambientes.

Meus sistemas são almalinux/redhat, mas compartilharei o que encontrei quando considerar que está quase pronto.

1 curtida

Em haproxy.cfg, a porta 443 é com crt, portanto criptografada, redirecionada para o contêiner através da porta 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 #adicionando “check ssl verify none” se ssl no nginx discourse

  1. Em nginx/…/discourse.conf, copiei a versão desta postagem e modifiquei a porta, o servername e a localização dos arquivos de certificado.
    => PB1: obtive conteúdos misturados e logs com o mesmo problema do magick
    => PB2: na página “/confirm-email”, recebo um erro “site não encontrado” do navegador.

  2. Em seguida, modifiquei o arquivo removendo o ssl na porta e comentei todas as linhas com “$thescheme”, para criptografar apenas no haproxy.
    => Erro 502 na primeira página, mesmos logs com magick

  3. Em seguida, instalei o magick a partir do código fonte (7.1.2.3), adicionei a pasta de binários ao PATH em .bashrc para os usuários root e discourse. Não funcionou via apt install (sem efeito como este).
    => O erro 502 desapareceu, ainda com conteúdo misturado e “site não encontrado” do navegador.
    => Logs ainda indicam magick não encontrado, mas de forma diferente.

==> /var/www/discourse/log/puma.stderr.log <==
=== puma startup: 2025-09-19 12:06:05 +0200 ===
OID desconhecido 16720: falha ao reconhecer o tipo de ‘embeddings’. Será tratado como String.
#<Thread:0x00007f611a21c0f0 /var/www/discourse/lib/discourse.rb:1190 run> terminou com exceção (report_on_exception é 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] AVISO hook before_fork falhou com exceção (Errno::ENOENT) No such file or directory - magick

Conclusão:
Como tornar o magick encontrável pelo puma?
Como desativar o ssl no nginx por causa do haproxy?

Consegui fazer funcionar como descrito na minha primeira postagem, mas aqui sem SSL no Nginx, copiando o nginx.config.sample com apenas a alteração do nome do host e da porta.
Portanto, tanto com SSL quanto sem SSL no Nginx, eu chego à primeira página para registro de administrador até a página de reenvio de e-mail, mas AMBOS:

  1. exibem “conteúdo misto” (para arquivos de imagem, por exemplo),
  2. outras páginas além do registro de administrador mostram “Oops…”
  3. nenhum e-mail de confirmação é enviado
  4. o Magick ainda não é encontrado

Em algum lugar em uma de suas postagens você menciona contêiner.

Você está usando contêineres para alguma das partes envolvidas?

Hoje quero finalizar minha implantação no ambiente de pré-produção e talvez eu tenha mais informações para compartilhar.

O mesmo que você :unamused_face:

  • Não envia e-mail de confirmação (estou usando o mailtrap, tenho certeza que funciona).
  • Criou usuário administrador com rake
  • Ops em / depois disso…
  • Também vi o problema do magick e o magick está instalado (tenho certeza que falta algum rubygem…)

Estou implantando usando docker e depois comparando. Eu odeio docker :expressionless_face: mas vou perder muito tempo precioso…

Eu mal conheço o Docker (se é que conheço!) e não precisei dele para executar um fórum Discourse em produção. Basta seguir as instruções suportadas, que ele lhe dará o que você precisa.

Sim, acredito em você. Mas preciso saber o que estou implantando e como funciona.

É a melhor maneira de encontrar e resolver problemas :squinting_face_with_tongue:

Porque você terá problemas, sempre, talvez não agora, mas tenho certeza de que o futuro você encontrará alguém.

De qualquer forma, como não terei suporte usando um método de instalação alternativo, seguirei as instruções oficiais e tentarei ajudar os outros :winking_face_with_tongue:

Eu também gastei muito tempo nesses problemas.
Sobre o Docker, veja este problema no Docker (um erro ou um backdoor ?..), corrigido no momento: https://youtu.be/dTqxNc1MVLE
Essa é uma das minhas razões para não usar o Docker.
Estou usando contêineres lxd (lxc) e me sinto bem com isso. Vou instalar o Docker em um contêiner lxc primeiro e exportar o banco de dados depois, quando a instalação sem Docker em um lxc for possível.

Bem, eu não estou forçando o Discourse a usar SSL porque quero que meu haproxy faça isso, pois o passthrough do haproxy não funciona com o protocolo HTTP para redirecionar requisições GET, e eu estou gerenciando múltiplos websites, então preciso do protocolo HTTP no haproxy, o que requer o tratamento de SSL no lado do haproxy. Eu gostaria de evitar um gateway SSL duplo.
Então o haproxy (em um lxc) escuta na porta 443: redireciona para 8080 (sem SSL) para o meu container Discourse (lxc).
Curiosamente, o nginx-config-sample fornecido na pasta do Discourse está configurado sem SSL e na porta 80, então deveria funcionar bem, mas estou tendo os problemas mencionados acima.

Você precisa fazer isso para dizer ao Discourse para enviar apenas links HTTPS. Não está mudando os redirecionamentos, mas a necessidade deles.

Você precisa ativar a configuração de forçar HTTPS.

HTTPS: Sim, mas por favor, diga-me como ativá-lo?

ÓTIMAS NOTÍCIAS! RESOLVI OS PROBLEMAS!!! A origem foi o programa magick. Após a instalação correta do magick a partir da versão 7 da fonte (não instale o imagemagick com apt, é a versão 6), meu site discourse exibe todas as páginas corretamente! E-mails, registro de administrador, etc. funcionam! Após a resolução do problema de "conteúdo misto", publicarei meu script para instalação em um container lxd-lxc (Debian 12) atrás do haproxy. em outro container lxd-lxc.

1 curtida