نشر Discourse بدون Docker

Though it is more convenient and safer to deploy Discourse following the official install guide, I want to dive deeper into the container and see how it can be deployed in Linux without Docker. I want to share the steps just for your information. You adapt it and use it at your own risk.

Dive into how Discourse is run in container

I take a look at the output of ./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

then look at /sbin/boot and /etc/service/unicorn/run, I get the core cmd to start 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

prepare system

FYI, I use Ubuntu 24.04 and zsh.

Follow PG’s offical installation guide to install postgres from PostgreSQL Apt Repository. I installed the version 18, which works quite well, though the offical install uses 15 at time of writing.

Install redis(8.2 at time of writing, official install uses 7.0), nginx and create dedicated user 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

Install ImageMagick 7 (I use IMEI) and check the version. Mine is:

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

then, change user(su - discourse) and install pnpm, rvm.

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

then adapt and add the following config to your .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'

# Add RVM to PATH for scripting. Make sure this is the last PATH variable change.
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

logout and login again as discourse for .zshrc to take effect.

Install node and ruby:

pnpm env use --global latest # will install node 24.9 at time of writing. offical install uses 22
rvm get master
rvm install 3.4 # will install ruby 3.4.6 at time of writing. official install uses 3.3
rvm use 3.4 --default

prepare db (and restore a 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;
# to restore database extracted from a backup:
$ gunzip < dump.sql.gz | psql discourse

To restore backup, you also need to copy the public and plugins folder as well.

install Discourse

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

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

Do the bundle / pnpm install, db migration, assets precompile stuff. This is also how you upgrade discourse and 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

I don’t want to use unicorn. Heroku recommends using the Puma web server instead of Unicorn. Here is my config/puma.rb written after consulting 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"

# Load logstash logger if enabled
if enable_logstash_logger
  require_relative "../lib/discourse_logstash_logger"
  FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
  # Note: You may need to adapt the logger initialization for 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

# Number of workers (processes)
workers ENV.fetch("PUMA_WORKERS", 6).to_i

# Set the directory
directory discourse_path

# Bind to the specified address and port
bind ENV.fetch(
       "PUMA_BIND",
       "tcp://#{ENV["PUMA_BIND_ALL"] ? "" : "127.0.0.1:"}#{ENV.fetch("PUMA_PORT", 3000)}",
     )
#bind 'tcp://0.0.0.0:80'
#plugin :acme
#acme_server_name 'example.com'
#acme_tos_agreed true
#bind 'acme://0.0.0.0:443'

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

# State file - used by pumactl
state_path "#{discourse_path}/tmp/pids/puma.state"

# Environment-specific configuration
if ENV["RAILS_ENV"] == "production"
  # Production timeout
  worker_timeout 30
else
  # Development timeout
  worker_timeout ENV.fetch("PUMA_TIMEOUT", 60).to_i
end

# Preload application
preload_app!

# Handle worker boot and shutdown
before_fork do
  Discourse.preload_rails!
  Discourse.before_fork

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

  # Sidekiq workers
  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
        # Delay Sidekiq log reopening
        sleep 1
        Demon::Sidekiq.kill("USR2")
      end
    end
  end

  # Email sync demon
  if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
    puts "starting up EmailSync demon"
    Demon::EmailSync.start(1)
  end

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

  # Demon monitoring thread
  Thread.new do
    loop do
      begin
        sleep 60

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

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

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

  # Close Redis connection
  Discourse.redis.close
end

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

# Worker timeout handling
worker_timeout 30

# Low-level worker options
threads 8, 32

To run Discourse, run puma -C config/puma.rb

Using systemd, you can run it on boot and restart it on failure. Here is the service file:

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

[Service]
Type=simple
User=discourse
Group=discourse
WorkingDirectory=/var/www/discourse
# requires running `rvm 3.4.6 --default` before this service is run
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'

# Restart configuration
Restart=always
RestartSec=5s

# Basic security measures
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only

[Install]
WantedBy=multi-user.target

Now the puma server listens on 127.0.0.1:3000. Adapt the nginx config file from Docker:

/etc/nginx/sites-enabled/discourse.conf
# Additional MIME types that you'd like nginx to handle go in here
types {
    text/csv csv;
    #application/wasm wasm;
}

upstream discourse { server 127.0.0.1:3000; }

# inactive means we keep stuff around for 1440m minutes regardless of last access (1 week)
# levels means it is a 2 deep hierarchy cause we can have lots of files
# max_size limits the size of the cache
proxy_cache_path /var/nginx/cache inactive=1440m levels=1:2 keys_zone=one:10m max_size=600m;

# Increased from the default value to acommodate large cookies during oAuth2 flows
# like in https://meta.discourse.org/t/x/74060 and large CSP and Link (preload) headers
proxy_buffer_size 32k;
proxy_buffers 4 32k;

# Increased from the default value to allow for a large volume of cookies in request headers
# Discourse itself tries to minimise cookie size, but we cannot control other cookies set by other tools on the same domain.
large_client_header_buffers 4 32k;

# attempt to preserve the proto, must be in http context
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"';

# Allow bypass cache from 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;

  # Uncomment and configure this section for HTTPS support
  # NOTE: Put your ssl cert in your main nginx config directory (/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;

  # maximum file upload size (keep up to date when changing the corresponding site setting)
  client_max_body_size 128m ;


  # path to discourse's public directory
  set $public /var/www/discourse/public;

  # without weak etags we get zero benefit from etags on dynamically compressed content
  # further more etags are based on the file in nginx not sha of data
  # use dates, it solves the problem fine even cross server
  etag off;

  # prevent direct download of backups
  location ^~ /backups/ {
    internal;
  }

  # bypass rails stack with a cheap 204 for favicon.ico requests
  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;
    }

    # some minimal caching here so we don't keep asking
    # longer term we should increase probably to 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 enables this
      brotli_static on;
      gzip_static on;
      add_header Cache-Control public,immutable;
      # HOOK in asset location (used for extensibility)
      # TODO I don't think this break is needed, it just breaks out of rewrite
      break;
    }

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

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

    location ~ ^/uploads/ {

      # NOTE: it is really annoying that we can't just define headers
      # at the top level and inherit.
      #
      # proxy_set_header DOES NOT inherit, by design, we must repeat it,
      # otherwise headers are not set correctly
      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;

      ## optional upload anti-hotlinking rules
      #valid_referers none blocked mysite.com *.mysite.com;
      #if ($invalid_referer) { return 403; }

      # custom CSS
      location ~ /stylesheet-cache/ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # this allows us to bypass rails
      location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp|avif)$ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # SVG needs an extra header attached
      location ~* \.(svg)$ {
      }
      # thumbnails & optimized images
      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;
    }

    # This big block is needed so we can selectively enable
    # acceleration for backups, avatars, sprites and so on.
    # see note about repetition above
    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;

      # if Set-Cookie is in the response nothing gets cached
      # this is double bad cause we are not passing last modified in
      proxy_ignore_headers "Set-Cookie";
      proxy_hide_header "Set-Cookie";
      proxy_hide_header "X-Discourse-Username";
      proxy_hide_header "X-Runtime";

      # note x-accel-redirect can not be used with 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;
    }

    # we need buffering off for 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;
    }

    # this means every file in public is tried first
    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;
  }

}

Now your Discourse can be accessed from example.com:443.

maintenance

To access rails console, simply run rails c as user discourse in /var/www/discourse. The discourse cmd which can be found in official doc is basically bundle exec script/discourse.

To upgrade Discourse, consult #upgrade-cmd, then restart puma using either puma restart or puma phased-restart. For the difference, consult puma/docs/restart.md at master · puma/puma · GitHub .

8 إعجابات

هل يجب نقل هذا إلى #community-wiki:sysadmins، ربما؟

مرحباً. شكراً للمشاركة.

أنا أكتب نصًا برمجيًا لتثبيته على دبيان 12 في حاوية LXC. لقد انتهيت منه تقريبًا ويعمل بشكل جيد. سأنشره عندما يكون جاهزًا.

لقد تمكنت من عرض الصفحة الأولى لتسجيل المسؤول. ولكن يتم إرسال بريد التأكيد إلى discourse@myhostname، وإلى myhostname كـ smtp_server، لذلك هو أمر سخيف. المتغيرات في .bashrc (أو .zshrc)، ولا في discourse.conf تؤخذ في الاعتبار لإرسال البريد الإلكتروني. عنوان البريد الإلكتروني للمطور صحيح ولكن جميع المعلمات الأخرى خاطئة ولم أتمكن من تغييرها. هل لديك أي فكرة لمعرفة كيفية تحقيق ذلك؟

إعجاب واحد (1)

وفقًا للكود

يجب تكوين SMTP في config/discourse.conf.
بالنسبة لي، لدي هذه الأسطر فيها:

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

هل قمت بالتحقق من السجلات؟ بالنسبة لهذا التثبيت المخصص، يوجد السجل في production.log، production.log، puma.stdout.log تحت دليل log.

شكراً جزيلاً على ردك.

لدي بالفعل هذه الإعدادات في config/discourse.conf. لقد فعلت بالضبط ما كتبته باستثناء استخدام zsh.
سجلاتي لا تتحدث عن smtp، والوحيد من (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)

لا تُظهر سجلات خادم البريد الخاص بي أي إدخالات من خادم discourse (يعمل لجميع خوادمي الأخرى (جميع حاويات lxc)). يمكنني إرسال بريد بنجاح عبر أمر الطرفية mail.

مع 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

ملفي 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 يشبه .zhrc الخاص بك
تعديل إدخالات developper_emails أو db_password يعمل (يتم عرض البريد الإلكتروني الصحيح في صفحة تسجيل المسؤول على الموقع)، ولكن يتم تجاهل معلمات smtp الأخرى.

ملف config/puma.rb الخاص بك يحتوي على بعض الأخطاء (حول السطر الذي يحتوي على المنفذ 3000). هل يمكنك تقديمه مرة أخرى؟

لقد سجلت المسؤول باستخدام rails c وبعد ذلك حصلت على صفحة “Oops…” في صفحة البداية. لا يتم عرض أي صفحات بشكل صحيح.

من فضلك ساعدني.

@lion ، جرب هذا:

# frozen_string_literal: true

require "fileutils"

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

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

# تحميل مسجل logstash إذا تم تمكينه
if enable_logstash_logger
  require_relative "../lib/discourse_logstash_logger"
  FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
  # ملاحظة: قد تحتاج إلى تكييف تهيئة المسجل لـ 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

# عدد العمال (العمليات)
workers ENV.fetch("PUMA_WORKERS", 8).to_i

# تحديد الدليل
directory discourse_path

# الربط بالعنوان والمنفذ المحددين
bind ENV.fetch("PUMA_BIND", "tcp://#{ENV['PUMA_BIND_ALL'] ? '' : '127.0.0.1:'}3000")

# موقع ملف PID
FileUtils.mkdir_p("#{discourse_path}/tmp/pids")
pidfile ENV.fetch("PUMA_PID_PATH", "#{discourse_path}/tmp/pids/puma.pid")

# ملف الحالة - يستخدم بواسطة pumactl
state_path "#{discourse_path}/tmp/pids/puma.state"

# التكوين الخاص بالبيئة
if ENV["RAILS_ENV"] == "production"
  # مهلة الإنتاج
  worker_timeout 30
else
  # مهلة التطوير
  worker_timeout ENV.fetch("PUMA_TIMEOUT", 60).to_i
end

# تحميل التطبيق مسبقًا
preload_app!

# التعامل مع بدء وإيقاف العامل
before_fork do
  Discourse.preload_rails!
  Discourse.before_fork

  # فحص المشرف
  supervisor_pid = ENV["PUMA_SUPERVISOR_PID"].to_i
  if supervisor_pid > 0
    Thread.new do
      loop do
        unless File.exist?("/proc/#{supervisor_pid}")
          puts "قتل المشرف الذاتي قد اختفى"
          Process.kill "TERM", Process.pid
        end
        sleep 2
      end
    end
  end

  # عمال Sidekiq
  sidekiqs = ENV["PUMA_SIDEKIQS"].to_i
  if sidekiqs > 0
    puts "بدء #{sidekiqs} من عمال Sidekiq الخاضعين للإشراف"

    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
        # تأخير إعادة فتح سجلات Sidekiq
        sleep 1
        Demon::Sidekiq.kill("USR2")
      end
    end
  end

  # شيطان مزامنة البريد الإلكتروني
  if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
    puts "بدء شيطان مزامنة البريد الإلكتروني"
    Demon::EmailSync.start(1)
  end

  # شياطين الإضافات
  DiscoursePluginRegistry.demon_processes.each do |demon_class|
    puts "بدء شيطان #{demon_class.prefix}"
    demon_class.start(1)
  end

  # خيط مراقبة الشياطين
  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("خطأ في فحص نبضات الشياطين: #{e}\n#{e.backtrace.join("\n")}")
      end
    end
  end

  # إغلاق اتصال Redis
  Discourse.redis.close
end

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

# التعامل مع مهلة العامل
worker_timeout 30

# خيارات العامل منخفضة المستوى
threads 8, 32

شكراً لك. لقد نسيت أن أقول إنها خلف haproxy لعدة خوادم.

  1. أحصل على “mix content” من وحدة التحكم في المتصفح. ماذا يمكنني أن أغير في ملف nginx؟
  2. أحصل على هذا السجل حول magick عند تشغيل أمر puma. تم تثبيته أيضاً في البداية.

==
/var/www/discourse/log/puma.stderr.log
<==
=== بدء تشغيل puma: 2025-09-19 01:40:45 +0200 ===
OID غير معروف 16720: فشل في التعرف على نوع ‘embeddings’. سيتم التعامل معها كسلسلة نصية.
#<Thread:0x00007fc59a2f5a78 /var/www/discourse/lib/discourse.rb:1190 run> انتهى باستثناء (report_on_exception is true):
/var/www/discourse/lib/letter_avatar.rb:112:in ``‘: لا يوجد مثل هذا الملف أو الدليل - 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!’
لقد قرأت أنه يمكن أن يكون سبباً لعدم عرض الصفحات.

هل تفرض استخدام SSL في إعدادات Discourse؟

هل يمكنك تشغيل magick --version كمستخدم discourse؟ بالنسبة لي، المخرجات هي:
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)

نعم، هذا بالضبط ما كنت أفكر فيه.

لقد وجدت الكثير من التبعيات غير الملباة أثناء التثبيت “يدويًا”. أنا أكتب Ansible لأتمكن من نشر Discourse على بيئات متعددة.

أنظمتي هي AlmaLinux/Red Hat ولكنني سأشارك ما وجدته عندما أعتبره شبه جاهز.

إعجاب واحد (1)

في haproxy.cfg، المنفذ 443 مشفر باستخدام crt، ويتم إعادة توجيهه إلى الحاوية عبر 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 #adding “check ssl verify none” if ssl on nginx discourse

  1. في nginx/…/discourse.conf، نسخت إصدار هذا المنشور وعدلت المنفذ واسم الخادم وموقع ملفات الشهادة،
    => PB1: حصلت على محتويات مختلطة وسجلات بنفس مشكلة magick
    => PB2: في صفحة “/confirm-email” أحصل على خطأ “الموقع غير موجود” من المتصفح.

  2. ثم قمت بتعديل الملف مع إزالة ssl في المنفذ وعلقّت جميع الأسطر التي تحتوي على “$thescheme”، لتشفيرها فقط في haproxy.
    => خطأ 502 في الصفحة الأولى، نفس السجلات مع magick

  3. ثم قمت بتثبيت magick من المصدر (7.1.2.3)، وأضفت مجلد الملفات التنفيذية إلى PATH في .bashrc للمستخدمين root و discourse. لم يعمل مع التثبيت عبر apt (لم يكن له تأثير مثل هذا).
    => اختفى خطأ 502، لا تزال المحتويات مختلطة و “الموقع غير موجود” من المتصفح.
    => السجلات لا تزال تشير إلى أن magick غير موجود ولكن بشكل مختلف.

== /var/www/discourse/log/puma.stderr.log ==
=== puma startup: 2025-09-19 12:06:05 +0200 ===
unknown OID 16720: failed to recognize type of ‘embeddings’. It will be treated as String.
#<Thread:0x00007f611a21c0f0 /var/www/discourse/lib/discourse.rb:1190 run> terminated with exception (report_on_exception is true):
/var/www/discourse/lib/letter_avatar.rb:112:in ``‘: No such file or directory - magick (Errno::ENOENT)
from /var/www/discourse/lib/letter_avatar.rb:112:in image_magick_version' from /var/www/discourse/lib/discourse.rb:1190:in block in preload_rails!’
== /var/www/discourse/log/puma.stdout.log ==
[8967] WARNING hook before_fork failed with exception (Errno::ENOENT) No such file or directory - magick

الخلاصة:
كيف يمكن جعل magick قابلاً للاكتشاف بواسطة puma؟
كيف يمكن تعطيل ssl في nginx بسبب haproxy؟

لقد تمكنت من جعله يعمل كما هو موضح في منشوري الأول، ولكن هنا بدون ssl على nginx عن طريق نسخ nginx.config.sample مع تغيير اسم المضيف والمنفذ فقط.
لذلك، سواء مع ssl أو بدونه على nginx، أحصل على الصفحة الأولى لتسجيل المسؤول حتى صفحة إعادة إرسال البريد الإلكتروني، ولكن كلاهما:

  1. يعرض “محتوى مختلط” (لملفات الصور على سبيل المثال)،
  2. صفحات أخرى غير تسجيل المسؤول تظهر “عفوًا…”
  3. لا يتم إرسال أي رسائل تأكيد بالبريد الإلكتروني
  4. لا يزال يتعذر العثور على magick

في مكان ما في أحد منشوراتك ذكرت حاوية (container).

هل تستخدم حاويات (containers) لأي من الأجزاء المعنية؟

اليوم أريد إنهاء النشر الخاص بي في بيئة ما قبل الإنتاج وربما سأشارك المزيد من المعلومات.

مثلك تماماً :unamused_face:

  • عدم إرسال البريد الإلكتروني للتأكيد (أستخدم mailtrap، أنا متأكد من أنه يعمل).
  • تم إنشاء مستخدم مسؤول باستخدام rake
  • عفوًا عند / بعد ذلك…
  • شوهدت أيضاً مشكلة magick وmagick مثبتة (أنا متأكد من أن هناك بعض rubygem المفقودة…)

أقوم بنشره باستخدام docker ثم المقارنة. أكره docker :expressionless_face: ولكني سأضيع الكثير من الوقت الثمين…

أنا بالكاد أعرف دوكر (إن كنت أعرفها على الإطلاق!) ولم أكن بحاجة لمعرفتها لتشغيل منتدى ديسكورس في بيئة إنتاجية. فقط باتباع التعليمات المدعومة، ستحصل على ما تحتاجه.

نعم، أصدقك. لكنني بحاجة لمعرفة ما أقوم بنشره وكيف يعمل.

إنها أفضل طريقة للعثور على المشاكل وحلها :squinting_face_with_tongue:

لأنك ستواجه مشاكل، دائمًا ربما ليس الآن، لكنني متأكد من أنك في المستقبل ستجد شخصًا ما.

على أي حال، بما أنني لن أحصل على دعم باستخدام طريقة تثبيت بديلة، سأتبع التعليمات الرسمية وأحاول مساعدة الآخرين :winking_face_with_tongue:

لقد أمضيت أيضًا الكثير من الوقت في هذه المشكلات.
حول دوكر، انظر إلى هذه المشكلة في دوكر (خطأ أم باب خلفي؟ …)، تم تصحيحها في الوقت الحالي: https://youtu.be/dTqxNc1MVLE
هذا أحد أسباب عدم استخدامي لدوكر.
أنا أستخدم حاويات lxd (lxc)، وأشعر بالرضا عن ذلك. سأقوم بتثبيت دوكر في حاوية lxc في البداية، ثم تصدير قاعدة البيانات لاحقًا عندما يكون التثبيت بدون دوكر في lxc ممكنًا.

حسنًا، أنا لا أجبر Discourse على استخدام SSL لأنني أريد السماح لـ haproxy الخاص بي بالقيام بذلك لأن تمرير haproxy لا يعمل مع بروتوكول HTTP لإعادة توجيه طلبات GET، وأنا أتعامل مع مواقع ويب متعددة لذا أحتاج إلى بروتوكول HTTP في haproxy، لذا فهو يتطلب معالجة SSL على جانب haproxy. أود تجنب بوابة SSL مزدوجة.
لذا يستمع haproxy (على حاوية lxc واحدة) إلى 443: يعيد التوجيه إلى 8080 (بدون SSL) إلى حاوية Discourse الخاصة بي (lxc).
من المثير للاهتمام أن عينة nginx-config الموجودة في مجلد Discourse تم تكوينها بدون SSL والمنفذ 80، لذا يجب أن تعمل بشكل جيد، ولكنني أواجه المشاكل المذكورة أعلاه.

تحتاج إلى القيام بذلك لإخبار Discourse بإرسال روابط HTTPS فقط. إنه لا يغير عمليات إعادة التوجيه، ولكنه يغير الحاجة إليها.

تحتاج إلى تشغيل إعداد فرض HTTPS.

HTTPS: نعم، ولكن من فضلك أخبرني كيف أقوم بتشغيله؟

أخبار رائعة! لقد قمت بحل المشكلات!!! كان المصدر هو برنامج magick. بعد تثبيت magick بشكل صحيح من المصدر الإصدار 7 (لا تقم بتثبيت imagemagick باستخدام apt، فهو الإصدار 6)، يعرض موقع discourse الخاص بي جميع الصفحات بشكل صحيح! تعمل رسائل البريد الإلكتروني، وتسجيل المسؤول، وما إلى ذلك! بعد حل مشكلة “المحتوى المختلط”، سأنشر برنامجي للتثبيت في حاوية lxd-lxc (Debian 12) خلف haproxy. على حاوية lxd-lxc أخرى.

إعجاب واحد (1)