Sebbene sia più conveniente e sicuro distribuire Discourse seguendo la guida ufficiale di installazione, voglio approfondire il contenitore e vedere come può essere distribuito in Linux senza Docker. Voglio condividere i passaggi solo a titolo informativo. Adattali e utilizzali a tuo rischio e pericolo.
Approfondisci come Discourse viene eseguito nel contenitore
Esamino l’output di ./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
poi guardo /sbin/boot e /etc/service/unicorn/run e ottengo il comando principale per avviare 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
preparare il sistema
Per tua informazione, uso Ubuntu 24.04 e zsh.
Segui la guida ufficiale di installazione di PG per installare PostgreSQL dal repository APT di PostgreSQL. Ho installato la versione 18, che funziona molto bene, anche se l’installazione ufficiale utilizza la 15 al momento della stesura.
Installa redis (8.2 al momento della stesura, l’installazione ufficiale utilizza la 7.0), nginx e crea l’utente dedicato 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
Installa ImageMagick 7 (uso IMEI) e controlla la versione. La mia è:
magick --version
Version: ImageMagick 7.1.2-3 Q16-HDRI
poi, cambia utente (su - discourse) e installa pnpm, rvm.
curl -fsSL https://get.pnpm.io/install.sh | zsh -
curl -sSL https://get.rvm.io | bash
poi adatta e aggiungi la seguente configurazione al tuo .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'
# Aggiungi RVM al PATH per lo scripting. Assicurati che questa sia l'ultima modifica alla variabile 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
Disconnettiti e riaccedi come discourse affinché .zshrc abbia effetto.
Installa node e ruby:
pnpm env use --global latest # installerà node 24.9 al momento della stesura. l'installazione ufficiale utilizza la 22
rvm get master
rvm install 3.4 # installerà ruby 3.4.6 al momento della stesura. l'installazione ufficiale utilizza la 3.3
rvm use 3.4 --default
preparare il db (e ripristinare un 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;
# per ripristinare il database estratto da un backup:
$ gunzip < dump.sql.gz | psql discourse
Per ripristinare un backup, devi anche copiare le cartelle public e plugins.
installare Discourse
Ho consultato discourse_docker/templates/web.template.yml at 20e33fbfd98d3b8d9c57f7a111beff8aa51a5b98 · discourse/discourse_docker · GitHub
come utente 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
configura 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'
Esegui bundle / pnpm install, migrazione db, precompilazione asset e così via. Questo è anche il modo per aggiornare Discourse e i plugin.
come utente 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
Non voglio usare unicorn. Heroku consiglia di utilizzare il server web Puma invece di Unicorn. Ecco il mio config/puma.rb scritto dopo aver consultato 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"
# Carica il logger logstash se abilitato
if enable_logstash_logger
require_relative "../lib/discourse_logstash_logger"
FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
# Nota: potresti dover adattare l'inizializzazione del logger per 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
# Numero di worker (processi)
workers ENV.fetch("PUMA_WORKERS", 6).to_i
# Imposta la directory
directory discourse_path
# Bind all'indirizzo e alla porta specificati
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'
# Percorso del file PID
FileUtils.mkdir_p("#{discourse_path}/tmp/pids")
pidfile ENV.fetch("PUMA_PID_PATH", "#{discourse_path}/tmp/pids/puma.pid")
# File di stato - utilizzato da pumactl
state_path "#{discourse_path}/tmp/pids/puma.state"
# Configurazione specifica per ambiente
if ENV["RAILS_ENV"] == "production"
# Timeout di produzione
worker_timeout 30
else
# Timeout di sviluppo
worker_timeout ENV.fetch("PUMA_TIMEOUT", 60).to_i
end
# Precarica l'applicazione
preload_app!
# Gestisci l'avvio e lo spegnimento dei worker
before_fork do
Discourse.preload_rails!
Discourse.before_fork
# Controllo 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
# Worker 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
# Ritarda la riapertura del log di Sidekiq
sleep 1
Demon::Sidekiq.kill("USR2")
end
end
end
# Demone di sincronizzazione email
if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
puts "starting up EmailSync demon"
Demon::EmailSync.start(1)
end
# Demoni dei plugin
DiscoursePluginRegistry.demon_processes.each do |demon_class|
puts "starting #{demon_class.prefix} demon"
demon_class.start(1)
end
# Thread di monitoraggio dei demoni
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
# Chiudi la connessione Redis
Discourse.redis.close
end
on_worker_boot do
DiscourseEvent.trigger(:web_fork_started)
Discourse.after_fork
end
# Gestione del timeout del worker
worker_timeout 30
# Opzioni worker di basso livello
threads 8, 32
Per eseguire Discourse, esegui puma -C config/puma.rb
Usando systemd, puoi avviarlo all’avvio e riavviarlo in caso di errore. Ecco il file del servizio:
/etc/systemd/system/discourse.service
[Unit]
Description=Discourse con Puma Server
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=discourse
Group=discourse
WorkingDirectory=/var/www/discourse
# richiede l'esecuzione di `rvm 3.4.6 --default` prima di avviare questo servizio
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'
# Configurazione riavvio
Restart=always
RestartSec=5s
# Misure di sicurezza di base
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
[Install]
WantedBy=multi-user.target
Ora il server Puma ascolta su 127.0.0.1:3000. Adatta il file di configurazione nginx da Docker:
/etc/nginx/sites-enabled/discourse.conf
# Altri tipi MIME che desideri che nginx gestisca vanno qui
types {
text/csv csv;
#application/wasm wasm;
}
upstream discourse { server 127.0.0.1:3000; }
# inactive significa che manteniamo i dati per 1440 minuti indipendentemente dall'ultimo accesso (1 settimana)
# levels significa che è una gerarchia profonda 2 perché possiamo avere molti file
# max_size limita la dimensione della cache
proxy_cache_path /var/nginx/cache inactive=1440m levels=1:2 keys_zone=one:10m max_size=600m;
# Aumentato dal valore predefinito per accommodating grandi cookie durante i flussi oAuth2
# come in https://meta.discourse.org/t/x/74060 e grandi intestazioni CSP e Link (preload)
proxy_buffer_size 32k;
proxy_buffers 4 32k;
# Aumentato dal valore predefinito per consentire un grande volume di cookie nelle intestazioni delle richieste
# Discourse stesso cerca di minimizzare la dimensione dei cookie, ma non possiamo controllare altri cookie impostati da altri strumenti sullo stesso dominio.
large_client_header_buffers 4 32k;
# tenta di preservare il protocollo, deve essere nel contesto 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"';
# Consenti di bypassare la cache da 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;
# Scommenta e configura questa sezione per il supporto HTTPS
# NOTA: inserisci il tuo certificato SSL nella directory principale di configurazione 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;
# dimensione massima del file caricato (mantienila aggiornata quando cambi l'impostazione del sito corrispondente)
client_max_body_size 128m ;
# percorso alla directory pubblica di discourse
set $public /var/www/discourse/public;
# senza etag deboli non otteniamo alcun vantaggio dagli etag sui contenuti compressi dinamicamente
# inoltre gli etag sono basati sul file in nginx e non sull'hash sha dei dati
# usa le date, risolve il problema bene anche tra server diversi
etag off;
# previeni il download diretto dei backup
location ^~ /backups/ {
internal;
}
# bypassa lo stack rails con un economico 204 per le richieste 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;
}
# una minima cache qui così non continuiamo a chiedere
# a lungo termine dovremmo aumentare probabilmente a 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 abilita questo
brotli_static on;
gzip_static on;
add_header Cache-Control public,immutable;
# HOOK nella posizione asset (usato per l'estendibilità)
# TODO non credo che questo break sia necessario, interrompe solo il rewrite
break;
}
location ~ ^/plugins/ {
expires 1y;
add_header Cache-Control public,immutable;
add_header Access-Control-Allow-Origin *;
}
# cache emoji
location ~ /images/emoji/ {
expires 1y;
add_header Cache-Control public,immutable;
add_header Access-Control-Allow-Origin *;
}
location ~ ^/uploads/ {
# NOTA: è davvero fastidioso che non possiamo semplicemente definire le intestazioni
# a livello superiore e ereditare.
#
# proxy_set_header NON eredita, per progettazione, dobbiamo ripeterlo,
# altrimenti le intestazioni non sono impostate correttamente
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;
## regole opzionali anti-hotlinking per il caricamento
#valid_referers none blocked mysite.com *.mysite.com;
#if ($invalid_referer) { return 403; }
# CSS personalizzato
location ~ /stylesheet-cache/ {
add_header Access-Control-Allow-Origin *;
try_files $uri =404;
}
# questo ci permette di bypassare rails
location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp|avif)$ {
add_header Access-Control-Allow-Origin *;
try_files $uri =404;
}
# SVG richiede un'intestazione aggiuntiva allegata
location ~* \.(svg)$ {
}
# miniature e immagini ottimizzate
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;
}
# Questo grande blocco è necessario per abilitare selettivamente
# l'accelerazione per backup, avatar, sprite e così via.
# vedi nota sulla ripetizione sopra
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 è nella risposta nulla viene memorizzato nella cache
# questo è doppiamente negativo perché non stiamo passando last modified
proxy_ignore_headers "Set-Cookie";
proxy_hide_header "Set-Cookie";
proxy_hide_header "X-Discourse-Username";
proxy_hide_header "X-Runtime";
# nota x-accel-redirect non può essere utilizzato con proxy_cache
proxy_cache one;
proxy_cache_key "$scheme,$host,$request_uri";
proxy_cache_valid 200 301 302 7d;
#proxy_cache_bypass $bypass_cache;
proxy_pass http://discourse;
break;
}
# dobbiamo disabilitare il buffering per 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;
}
# questo significa che ogni file in public viene provato per primo
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;
}
}
Ora il tuo Discourse è accessibile da example.com:443.
manutenzione
Per accedere alla console rails, esegui semplicemente rails c come utente discourse in /var/www/discourse. Il comando discourse che si trova nella documentazione ufficiale è essenzialmente bundle exec script/discourse.
Per aggiornare Discourse, consulta #upgrade-cmd, quindi riavvia puma usando puma restart o puma phased-restart. Per la differenza, consulta puma/docs/restart.md at main · puma/puma · GitHub .