DockerなしでDiscourseをデプロイする

公式インストールガイドに従って Discourse をデプロイする方が便利で安全ですが、コンテナ内部を深く探り、Docker なしで Linux にデプロイする方法を確認したいと考えています。参考までに手順を共有します。これらは自己責任で適用・利用してください。

コンテナ内で Discourse がどのように実行されているかを探る

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

次に /sbin/boot/etc/service/unicorn/run を確認し、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

システムの準備

参考までに、私は Ubuntu 24.04 と zsh を使用しています。

PG の公式インストールガイド に従って、PostgreSQL Apt リポジトリから PostgreSQL をインストールします。私はバージョン 18 をインストールしましたが、これは非常にうまく機能しています(執筆時点では公式インストールではバージョン 15 が使用されています)。

redis(執筆時点では 8.2、公式インストールでは 7.0)、nginx をインストールし、専用ユーザー 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

ImageMagick 7 をインストールし(私は IMEI を使用しています)、バージョンを確認します。私の環境では以下の通りです:

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

次に、ユーザーを切り替え(su - discourse)、pnpmrvm をインストールします。

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

その後、以下の設定を .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'

# スクリプト用に RVM を PATH に追加します。これは 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

.zshrc を反映させるため、一度ログアウトし、discourse ユーザーとして再度ログインします。

Node と Ruby をインストールします:

pnpm env use --global latest # 執筆時点では Node 24.9 がインストールされます。公式インストールでは 22 が使用されています
rvm get master
rvm install 3.4 # 執筆時点では Ruby 3.4.6 がインストールされます。公式インストールでは 3.3 が使用されています
rvm use 3.4 --default

データベースの準備(およびバックアップの復元)

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;
# バックアップから抽出したデータベースを復元する場合:
$ gunzip < dump.sql.gz | psql discourse

バックアップを復元する際は、public フォルダと plugins フォルダもコピーする必要があります。

Discourse のインストール

discourse_docker/templates/web.template.yml at 20e33fbfd98d3b8d9c57f7a111beff8aa51a5b98 · discourse/discourse_docker · GitHub を参照しました。

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

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'

bundle / pnpm のインストール、データベースのマイグレーション、アセットのプリコンパイルなどを行います。これらは Discourse とプラグインをアップグレードする際の手順と同じです。

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

私は unicorn を使用したくありません。Heroku は Unicorn の代わりに Puma ウェブサーバー を使用することを推奨しています。config/unicorn.conf.rb を参考に作成した私の config/puma.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"

# 有効な場合、logstash logger を読み込みます
if enable_logstash_logger
  require_relative "../lib/discourse_logstash_logger"
  FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
  # 注意:Puma 用の logger 初期化を適応する必要がある場合があります
  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", 6).to_i

# ディレクトリを設定
directory discourse_path

# 指定されたアドレスとポートにバインド
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 ファイルの場所
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 "Kill self supervisor is gone"
          Process.kill "TERM", Process.pid
        end
        sleep 2
      end
    end
  end

  # 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
        # Sidekiq ログの再オープン遅延
        sleep 1
        Demon::Sidekiq.kill("USR2")
      end
    end
  end

  # メール同期デーモン
  if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
    puts "starting up EmailSync demon"
    Demon::EmailSync.start(1)
  end

  # プラグインデーモン
  DiscoursePluginRegistry.demon_processes.each do |demon_class|
    puts "starting #{demon_class.prefix} demon"
    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(
          "Error in demon processes heartbeat check: #{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

Discourse を実行するには、puma -C config/puma.rb を実行します。

systemd を使用すると、起動時に実行し、失敗時に再起動させることができます。サービスファイルは以下の通りです:

/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
# このサービスを実行する前に `rvm 3.4.6 --default` を実行する必要があります
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=always
RestartSec=5s

# 基本的なセキュリティ対策
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only

[Install]
WantedBy=multi-user.target

これで Puma サーバーは 127.0.0.1:3000 でリッスンします。Docker から nginx 設定ファイルを適応します:

/etc/nginx/sites-enabled/discourse.conf
# nginx が処理する追加の MIME タイプをここに記述します
types {
    text/csv csv;
    #application/wasm wasm;
}

upstream discourse { server 127.0.0.1:3000; }

# inactive は、最終アクセスに関係なく 1440 分(1 週間)間データを保持することを意味します
# levels は、多数のファイルが存在し得るため、2 段階の階層構造であることを意味します
# max_size はキャッシュのサイズを制限します
proxy_cache_path /var/nginx/cache inactive=1440m levels=1:2 keys_zone=one:10m max_size=600m;

# oAuth2 フロー中の大きなクッキーや、大きな CSP および Link(preload)ヘッダーに対応するため、デフォルト値から増やしました
# https://meta.discourse.org/t/x/74060 などを参照
proxy_buffer_size 32k;
proxy_buffers 4 32k;

# リクエストヘッダー内の大量のクッキーを許可するため、デフォルト値から増やしました
# Discourse 自体はクッキーのサイズを最小化しようとしますが、同じドメイン上の他のツールが設定するクッキーは制御できません
large_client_header_buffers 4 32k;

# プロトコルを保持しようとする試み、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"';

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

  # HTTPS サポートのためにこのセクションのコメントを外して設定してください
  # 注意:SSL 証明書はメインの 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;

  # 最大ファイルアップロードサイズ(対応するサイト設定を変更する際は最新に保ってください)
  client_max_body_size 128m ;


  # Discourse の public ディレクトリへのパス
  set $public /var/www/discourse/public;

  # 弱い etags がなければ、動的に圧縮されたコンテンツにおける etags の恩恵はゼロです
  # さらに、etags はデータのスハではなく、nginx 内のファイルに基づいています
  # 日付を使用すれば、サーバー間でも問題なく解決できます
  etag off;

  # バックアップの直接ダウンロードを防止
  location ^~ /backups/ {
    internal;
  }

  # favicon.ico リクエストに対して安価な 204 で Rails スタックをバイパス
  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;
    }

    # 頻繁な問い合わせを防ぐための最小限のキャッシュ
    # 長期的にはおそらく 1 年に増やすべき
    location ~ ^/javascripts/ {
      expires 1d;
      add_header Cache-Control public,immutable;
      add_header Access-Control-Allow-Origin *;
    }

    location ~ ^/assets/(?<asset_path>.+)$ {
      expires 1y;
      # アセットパイプラインがこれを有効化
      brotli_static on;
      gzip_static on;
      add_header Cache-Control public,immutable;
      # アセットロケーションへのフック(拡張性のために使用)
      # TODO この break は不要だと思われます。rewrite から抜け出すためだけのものです
      break;
    }

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

    # イモージをキャッシュ
    location ~ /images/emoji/ {
      expires 1y;
      add_header Cache-Control public,immutable;
      add_header Access-Control-Allow-Origin *;
    }

    location ~ ^/uploads/ {

      # 注意:ヘッダーをトップレベルで定義して継承させることができないのが本当に厄介です
      #
      # proxy_set_header は設計上継承しないため、繰り返す必要があります
      # そうしないとヘッダーが正しく設定されません
      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;

      ## オプションのアップロードホットリンク防止ルール
      #valid_referers none blocked mysite.com *.mysite.com;
      #if ($invalid_referer) { return 403; }

      # カスタム CSS
      location ~ /stylesheet-cache/ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # これにより Rails をバイパスできます
      location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp|avif)$ {
          add_header Access-Control-Allow-Origin *;
          try_files $uri =404;
      }
      # SVG には追加のヘッダーが必要です
      location ~* \.(svg)$ {
      }
      # サムネイルと最適化された画像
      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;
    }

    # この大きなブロックは、バックアップ、アバター、スプライトなどに対して選択的に加速を有効にするために必要です
    # 上記の繰り返しについての注意を参照
    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;

      # レスポンスに Set-Cookie がある場合、キャッシュは機能しません
      # これは二重に悪く、last modified を渡していないためです
      proxy_ignore_headers "Set-Cookie";
      proxy_hide_header "Set-Cookie";
      proxy_hide_header "X-Discourse-Username";
      proxy_hide_header "X-Runtime";

      # x-accel-redirect は 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;
    }

    # メッセージバスにはバッファリングをオフにする必要があります
    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;
    }

    # これは public 内のすべてのファイルが最初に試されることを意味します
    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;
  }

}

これで Discourse に example.com:443 からアクセスできるようになりました。

メンテナンス

Rails コンソールにアクセスするには、/var/www/discoursediscourse ユーザーとして rails c を実行するだけです。公式ドキュメントに記載されている discourse コマンドは、基本的に bundle exec script/discourse です。

Discourse をアップグレードするには、#upgrade-cmd を参照し、その後 puma restart または puma phased-restart を使用して Puma を再起動します。違いについては puma/docs/restart.md at main · puma/puma · GitHub を参照してください。

「いいね!」 8

これは Community wiki > Sysadmins に移動すべきでしょうか?

こんにちは。共有ありがとうございます。

現在、Debian 12 の LXC コンテナにインストールするためのスクリプトを作成しており、ほぼ完成し、正常に動作しています。準備ができ次第公開します。

管理者の登録のための最初のページを表示させることはできましたが、確認メールが discourse@myhostname に送信され、smtp_server が myhostname となっているため、おかしなことになっています。.bashrc (または .zshrc) の変数も discourse.conf の変数も、メール送信のために考慮されていません。開発者の email_adress は正しいですが、他のすべてのパラメータが間違っており、変更することができませんでした。これを達成する方法について、何かアイデアはありますか?

「いいね!」 1

コードによると

SMTP は config/discourse.conf で設定する必要があります。
私の場合、それには次の行があります。

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

ログを確認しましたか? このカスタムインストールでは、ログは log ディレクトリの下の production.log、production.log、puma.stdout.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: (: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

my 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 で管理者を登録した後、「おっと…」というページがスタートページに表示されます。ページは正しくレンダリングされません。

助けてください。

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

# 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", 8).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:'}3000")

# 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

ありがとうございます。Haproxy の背後にある複数のサーバーについて言及するのを忘れました。

  1. ブラウザのコンソールで「混合コンテンツ」というエラーが表示されます。nginx ファイルで何を 変更できますか?
  2. puma コマンドの起動時に、magick に関するログが表示されます。これは、最初にインストールされたものでもあります。

==
/var/www/discourse/log/puma.stderr.log

=== puma startup: 2025-09-19 01:40:45 +0200 ===
unknown OID 16720: failed to recognize type of ‘embeddings’. It will be treated as String.
#<Thread:0x00007fc59a2f5a78 /var/www/discourse/lib/discourse.rb:1190 run> terminated with exception (report_on_exception is true):
/var/www/discourse/lib/letter_avatar.rb:112:in ``‘: No such file or directory - magick (Errno::ENOENT)
from /var/www/discourse/lib/letter_avatar.rb:112:in image_magick_version' from /var/www/discourse/lib/discourse.rb:1190:in block in preload_rails!’
ページが表示されない原因となる可能性があると読みました。

Discourseの設定でSSLを強制していますか?

discourseユーザーとして magick --version を実行できますか? 私の場合、出力は次のようになります。
バージョン: ImageMagick 7.1.1-45 Q16-HDRI x86_64 3cbce5696:20250308 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
ライセンス: ImageMagick | License
機能: Cipher DPC HDRI Modules OpenMP(4.5)
デリゲート (組み込み): 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
コンパイラ: gcc (13.3)

はい、まさに私が考えていた通りです。

「手動」でインストール中に多くの未解決の依存関係が見つかりました。複数の環境にDiscourseをデプロイできるように、Ansibleを書いています。

私のシステムはAlmaLinux/Redhatですが、ほぼ完了したと思ったら見つけたものを共有します。

「いいね!」 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 # ssl on nginx discourse の場合は “check ssl verify none” を追加

  1. nginx/…/discourse.conf で、この投稿のバージョンをコピーし、ポート、サーバー名、証明書ファイルの場所を変更しました。
    => PB1: マジックと同じ問題で、混在したコンテンツとログが表示されました。
    => PB2: “/confirm-email” ページでブラウザに “サイトが見つかりません” というエラーが表示されます。

  2. 次に、ポートの SSL を削除し、" $thescheme " のすべての行をコメントアウトして、haproxy でのみ暗号化するようにファイルを変更しました。
    => 最初のページで 502 エラー、マジックで同じログが表示されました。

  3. 次に、ソース (7.1.2.3) からマジックをインストールし、root および discourse ユーザーの .bashrc にバイナリフォルダを PATH に追加しました。apt install では機能しませんでした (このようには効果がありませんでした)。
    => 502 エラーは解消されましたが、混在したコンテンツとブラウザの “サイトが見つかりません” は引き続き表示されます。
    => ログは依然としてマジックが見つからないことを示していますが、以前とは異なります。

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

結論:
puma でマジックを見つけられるようにするにはどうすればよいですか?
haproxy のために nginx で SSL を無効にするにはどうすればよいですか?

nginxでsslなしで、nginx.config.sampleをコピーしてホスト名とポートを変更するだけで、最初の投稿で説明したとおりに動作させることができました。
そのため、nginxでsslありでもなしでも、管理者登録の最初のページから確認メール再送信ページまで表示されますが、両方とも:

  1. 「混合コンテンツ」(例えば画像ファイルの場合)が表示される。
  2. 管理者登録以外のページでは「おっと…」と表示される。
  3. 確認メールが送信されない。
  4. ImageMagickが見つからない。

投稿のどこかでコンテナについて言及していました。

関係する部分のいずれかにコンテナを使用していますか?

今日、プリプロダクション環境でのデプロイを最終決定したいと考えており、共有できる情報が増えるかもしれません。

あなたと同じです :unamused_face:

  • 確認メールが送信されない(mailtrap を使用しており、機能することは確かです)。
  • rake で管理者ユーザーを作成しました。
  • その後 / で oops…
  • magick の問題も発生しており、magick はインストールされています(何らかの rubygem が不足していると確信しています…)。

docker を使用してデプロイし、比較します。docker は嫌いですが :expressionless_face:、貴重な時間をたくさん無駄にすることになります…

Dockerについてはほとんど(あるいは全く)知りませんが、Discourseフォーラムを本番環境で実行するために知る必要はありませんでした。サポートされている手順に従うだけで、必要なものが手に入ります。

はい、信じます。しかし、何をデプロイしているのか、それがどのように機能するのかを知る必要があります。

それが問題を見つけて解決するための最良の方法です😝

なぜなら、あなたは問題に直面するでしょう、常に今はそうではないかもしれませんが、将来あなたは必ず誰かを見つけるでしょう。

とにかく、代替のインストール方法でサポートを受けることはできないので、公式の説明に従い、他の人を助けるようにします😜

これらの問題にも多くの時間を費やしました。
Dockerについては、Docker上のこの問題(間違いかバックドアか?)を見てください。現在は修正されています: https://youtu.be/dTqxNc1MVLE
これが私がDockerを使用しない理由の1つです。
私はlxd(lxc)コンテナを使用しており、問題なく動作しています。最初にlxcコンテナにDockerをインストールし、Dockerなしでのlxcへのインストールが可能になったら、後でデータベースをエクスポートする予定です。

さて、DiscourseにSSLを強制しないのは、haproxyにSSLを処理させたいからです。なぜなら、haproxyのパススルーはGETリクエストをリダイレクトするためにHTTPプロトコルでは機能せず、複数のウェブサイトを処理する必要があるため、haproxyでHTTPプロトコルが必要となり、haproxy側でSSL処理が必要になるからです。二重SSLゲートウェイは避けたいです。
そのため、haproxy(1つのlxc上)は443をリッスンし、8080(SSLなし)にリダイレクトして私のDiscourseコンテナ(lxc)に渡します。
興味深いのは、Discourseフォルダで提供されているnginx-config-sampleがSSLなしでポート80で設定されているため、うまく機能するはずですが、上記の問題が発生していることです。

DiscourseにHTTPSリンクのみを送信するように指示するには、それを行う必要があります。リダイレクトを変更するのではなく、リダイレクトの必要性を変更します。

HTTPSを強制する設定をオンにする必要があります。

HTTPS: はい、ただし、有効にする方法を教えていただけますか?

素晴らしいニュースです!問題を解決しました!原因は magick プログラムでした。ソースバージョン7から正しく magick をインストールした後(aptでimagemagickをインストールしないでください。バージョン6です)、私のディスコースウェブサイトはすべてのページを正しく表示します!メール、管理者登録なども機能します!「混合コンテンツ」の問題が解決したら、haproxyの背後にあるLXD-LXCコンテナ(Debian 12)にインストールするためのスクリプトを公開します。別のLXD-LXCコンテナで。

「いいね!」 1