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)し、pnpm と rvm をインストールします。
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
ログアウトして再度 discourse としてログインし、.zshrc の変更を反映させます。
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 のインストール
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 ロガーを読み込む
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", 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 フロー中(例: https://meta.discourse.org/t/x/74060)の大きなクッキーや、大きな CSP および Link (preload) ヘッダーに対応するため、デフォルト値から増やしました
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"';
# ローカルホストからキャッシュをバイパスする
#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;
# 弱い ETag では、動的に圧縮されたコンテンツに対して ETag の恩恵を全く受けられません
# さらに、ETag は nginx 上のファイルに基づいて作成され、データの SHA ではありません
# 日付を使用すれば、サーバー間でも問題なく解決します
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 は不要だと思います。単にリライトから抜け出すだけです
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 がある場合、何もキャッシュされません
# これは最悪なことに、最終変更時刻を渡していないため、二重に悪いです
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/discourse で discourse ユーザーとして 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 を参照してください。