Nginx config:
Behind a reverse proxy (haproxy), you need to copy the /var/discourse/config/nginx-config-sample.conf to /etc/nginx/sites-enabled/discourse.conf or similar target, then replace the listen <yourreverseproxyport> (if needed), and the _ of server_name by your hostname (subdmain.example.com). Nothing else is to be changed.
Pages not displayed (“Oops” message) and email admin registration not working:
Magick was the cause of not displaying pages after admin registration pages at first boot. I followed the file indicated in the log: /var/www/discourse/lib/letter_avatar.rb:112 at line 112, there were two magick commands that indeed did not responded. convert responded, so i replaced magick at this line with convert. After these corrections, another error was logged. Trying the same procedure with the given command from file, magick command did not work, and convert was failing. NB: magick --version was 7.x and convert --version gave 6.x. The cause was that i installed first imagemagick with apt, then magick 7 from source. There were some conflicts and magick told convert command to be out of date. So I relaunched my script with magick 7 only. This solved immediately the problem and I could see the brand new pages awaited for days, and email worked too! Magick is really magic.
It still remains the problem of mixed content, but the website although works fine.
NB: in puma.rb, the line bind ENV.fetch("PUMA_BIND", "tcp://#{ENV['PUMA_BIND_ALL'] ? '' : '127.0.0.1:'}3000") must be like this (correction of first post.)
EDIT “3.”, about https force for discourse: I don’t know why exactly, but my browsers don’t show ‘mixed content’ warnings any more (maybe because of the cache update), so it is all good now. I just need to finish my script.
What I did for testing was set the environment variable PUMA_BIND when running puma.
Regarding ImageMagick, for some reason Discourse is failing to execute an image conversion on the webUI, when I upload a big image it kindly refuse to conver it… I’m still debugging the problem.
Any advance in debug? On my side, I am working on magick.
On some around 50ko image, in browser a pop-up displays: timeout -k 40.0 20 magick gif:/tmp/RackMultipart20250927-23598-xrrp6e.gif -auto-orient -background white -interlace none -flatten -debug all -quality 90 jpg:/tmp/image20250927-23598-9ujq3d.jpg and no image is loaded
If bigger size, no pop-up but loading wheel is turning in loop infinitely with no result. No error is logged in /var/www/discourse/log.
The only way i was able to get it running was using imagemagick from brew repo.
BUT it fails when the image was more than 3MB… maybe i setup a very restrictive policy config.
Give it a try!!!
I’m testing the docker install but I think or pretty stupid deploy nginx, unicorn, redis, postgresql and the rest in ONLY one container… it doesn’t make sense at all. And there’s no infrastructure documentation for big deployments… I’ve work on IT for more than 20 years and I only see problem coming in the near future using such infra.
Without talking about “docker The Space Eater” ( like Dormammu)
This issue is gone with the following solution described at the end:
Debug 1: bundle db:create outputs, also in logs at first boot:
…
unknown OID 21096: failed to recognize type of ‘embeddings’. It will be treated as String.
pngquant worker: pngquant not found; please provide proper binary or disable this worker (–no-pngquant argument or :pngquant => false through options)
oxipng worker: oxipng not found; please provide proper binary or disable this worker (–no-oxipng argument or :oxipng => false through options)
jhead worker: jhead not found, jpegtran not found; please provide proper binary or disable this worker (–no-jhead argument or :jhead => false through options)
jpegoptim worker: jpegoptim not found; please provide proper binary or disable this worker (–no-jpegoptim argument or :jpegoptim => false through options)
…
After this, my upload size can be up to 518KB. Above not. This is only for images. All others documents, audio, video uploads work.
Temporary solution for the resting issue:
I’ve looked in the admin settings, discourse.conf, nginx/sites-enables/discourse.conf, in the git folder. Finally in AdminPanel/Parameters/Files I’ve disabled “Composer media optimization image enabled”, then everything is working fine. I can upload any image.
Why don’t you install a mail server ? I have my own and I scripted the install. Tell me if you are interested. You just need to pay yearly for a domain name.
bundler: failed to load command: puma (/home/mry/.rbenv/versions/3.4.6/bin/puma)
/home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/puma-7.0.4/lib/puma/cluster.rb:472:in 'Puma::Cluster#run': undefined method 'wait_readable' for nil (NoMethodError)
if read.wait_readable([0, @next_check - Time.now].max)
^^^^^^^^^^^^^^
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/puma-7.0.4/lib/puma/launcher.rb:202:in 'Puma::Launcher#run'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/puma-7.0.4/lib/puma/cli.rb:73:in 'Puma::CLI#run'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/puma-7.0.4/bin/puma:10:in '<top (required)>'
from /home/mry/.rbenv/versions/3.4.6/bin/puma:25:in 'Kernel#load'
from /home/mry/.rbenv/versions/3.4.6/bin/puma:25:in '<top (required)>'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/cli/exec.rb:59:in 'Kernel.load'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/cli/exec.rb:59:in 'Bundler::CLI::Exec#kernel_load'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/cli/exec.rb:23:in 'Bundler::CLI::Exec#run'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/cli.rb:452:in 'Bundler::CLI#exec'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/vendor/thor/lib/thor/command.rb:28:in 'Bundler::Thor::Command#run'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in 'Bundler::Thor::Invocation#invoke_command'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/vendor/thor/lib/thor.rb:538:in 'Bundler::Thor.dispatch'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/cli.rb:35:in 'Bundler::CLI.dispatch'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/vendor/thor/lib/thor/base.rb:584:in 'Bundler::Thor::Base::ClassMethods#start'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/cli.rb:29:in 'Bundler::CLI.start'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/exe/bundle:28:in 'block in <top (required)>'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/lib/bundler/friendly_errors.rb:117:in 'Bundler.with_friendly_errors'
from /home/mry/.rbenv/versions/3.4.6/lib/ruby/gems/3.4.0/gems/bundler-2.6.4/exe/bundle:20:in '<top (required)>'
from /home/mry/.rbenv/versions/3.4.6/bin/bundle:25:in 'Kernel#load'
from /home/mry/.rbenv/versions/3.4.6/bin/bundle:25:in '<main>'
then Puma restarts itself. What should I do to prevent this?
Hi all,
Here is my script for a full install in three shots, without docker, on a Debian 12 container (lxc for me).
Use the if blocks to only run the parts you need.
Set your parameter and run it as root with the required arguments. Be careful: the script will exit at the exit point before “### MANUAL” part.
You have to pass these commands manually in the container terminal. I did not succeed to make it work in my lxc unprivileged container. (default user is root)
Then run the rest.
I tested it many times and it works but there may be some buggy parts after i adapted it to publication.
install_script
#!/bin/bash
########## SCRIPT TO INSTALL DISCOURSE ON DEBIAN 12 WITHOUT DOCKER ##################
#- last tested in december 2025
############################## RUN IT AS ROOT, best in a lxc container!!!!!!!
#- discussion source : https://meta.discourse.org/t/deploy-discourse-without-docker/351194
##############################################################################
# MANUAL PARAMETERS
certbot_contact_email=hello@domain.tld
certbot_renew_port=9785
nginx_discourse_port=8080
DISCOURSE_DEVELOPER_EMAILS='emailaddress'
DISCOURSE_SMTP_ADDRESS=smtp_server_address
DISCOURSE_SMTP_PORT=465
DISCOURSE_SMTP_USER_NAME=xxx
DISCOURSE_SMTP_PASSWORD=xxx
DISCOURSE_SMTP_DOMAIN=xxx
DISCOURSE_NOTIFICATION_EMAIL=no-reply-xxxx
DISCOURSE_MAXMIND_ACCOUNT_ID=50
# END MANUAL PARAMETERS
shopt -s expand_aliases
source /etc/bash.bashrc
container=$1
cont_haproxy=$2
pool=$3
lxc_image=$4
hostname=$5
db_password=$6
maxmind_license_key=$7
DISCOURSE_HOSTNAME=$hostname
DISCOURSE_MAXMIND_LICENSE_KEY=$maxmind_license_key
### INSTALL MAGICK 7 and its plugins
# https://github.com/SoftCreatR/imei/?tab=readme-ov-file
if [ 1 == 1 ]; then
t=$(mktemp) && \
wget 'https://dist.1-2.dev/imei.sh' -qO "$t" && \
bash "$t" && \
rm "$t"
fi
### INSTALL BASICS ###
if [ 1 == 1 ]; then
apt install -y apt-utils postgresql nginx libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static redis
systemctl enable --now postgresql redis-server nginx
useradd -m -s /bin/bash discourse
usermod -aG sudo discourse
passwd -d discourse
# RVM (RUBY)
apt install -y ruby-full # nécessaire
su - discourse -c 'curl -sSL https://get.rvm.io | bash'
su - discourse -c 'source /home/discourse/.bashrc'
su - discourse -c 'source /home/discourse/.rvm/scripts/rvm' # nécessaire après rvm install
su - discourse -c 'rvm install 3.3.7' # will install node 3.3.7 as of today
source /home/discourse/.rvm/scripts/rvm
# fi
# configure POSTGRESQL
apt update && apt upgrade -y
apt autoremove -y
# YOU HAVE TO ACCEPT THE UPGRADE
/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
apt install -y postgresql-18-pgvector
fi
alias mydb="sudo -u postgres psql discourse -c "
if [ 1 == 1 ]; then
su - discourse -c 'sudo -u postgres createuser -s discourse'
# fi
su - discourse -c 'sudo -u postgres createdb discourse'
su - discourse -c 'sudo -u postgres psql -c "ALTER USER discourse WITH ENCRYPTED PASSWORD '\''password'\'';"'
alias mydb="sudo -Hiu discourse psql -c "
su - discourse -c 'psql -c "GRANT CONNECT ON DATABASE discourse TO root;"'
su - discourse -c 'psql -c "CREATE EXTENSION pg_trgm;CREATE EXTENSION hstore;"'
su - discourse -c 'psql -c "ALTER DATABASE discourse OWNER TO discourse;"'
su - discourse -c 'psql -c "CREATE EXTENSION unaccent;CREATE EXTENSION plpgsql;"'
su - discourse -c 'psql -c "CREATE EXTENSION vector;"'
echo "OK"
### INSTALL DISCOURSE FROM GITHUB
cd /var/www/
git clone https://github.com/discourse/discourse
git config --global --add safe.directory /var/www/discourse
# exit
### NÉCESSAIRE POUR bundle, ruby, etc. pas pour discourse lui-même
tee -a /home/discourse/.bashrc > /dev/null <<EOT
export RAILS_ENV=production
export UNICORN_SIDEKIQ_MAX_RSS=800
export UNICORN_WORKERS=8
export UNICORN_SIDEKIQS=1
export PUMA_SIDEKIQ_MAX_RSS=800
export PUMA_WORKERS=8
export PUMA_SIDEKIQS=1
export RUBY_GC_HEAP_GROWTH_MAX_SLOTS=40000
export RUBY_GC_HEAP_INIT_SLOTS=400000
export RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR=1.5
export DISCOURSE_HOSTNAME=$hostname
export DISCOURSE_DEVELOPER_EMAILS=$DISCOURSE_DEVELOPER_EMAILS
export DISCOURSE_SMTP_ADDRESS=$DISCOURSE_SMTP_ADDRESS
export DISCOURSE_SMTP_PORT=$DISCOURSE_SMTP_PORT
export DISCOURSE_SMTP_USER_NAME=$DISCOURSE_SMTP_USER_NAME
export DISCOURSE_SMTP_PASSWORD=$DISCOURSE_SMTP_PASSWORD
export DISCOURSE_SMTP_DOMAIN=$DISCOURSE_SMTP_DOMAIN
export DISCOURSE_NOTIFICATION_EMAIL=$DISCOURSE_NOTIFICATION_EMAIL
export DISCOURSE_MAXMIND_ACCOUNT_ID=$DISCOURSE_MAXMIND_ACCOUNT_ID
export DISCOURSE_MAXMIND_LICENSE_KEY=$maxmind_license_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
EOT
cd /var/www/discourse
git stash
git pull
git checkout tests-passed
cd plugins
for plugin in *
do
echo "ok"
echo $plugin; cd ${plugin}; git pull; cd ..
done
# fi
chown -R discourse:discourse /var/www/discourse/
mkdir -p /var/www/discourse/public
chown -R discourse:www-data /var/www/discourse/public
tee -a /var/www/discourse/config/discourse.conf > /dev/null <<EOT
max_data_explorer_api_req_mode = 'none'
max_user_api_reqs_per_day = '28800'
hostname = '$hostname'
redis_host = 'localhost'
smtp_user_name = $$DISCOURSE_SMTP_USER_NAME
db_password = '$db_password'
smtp_address = '$DISCOURSE_SMTP_ADDRESS'
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_DEVELOPER_EMAILS'
max_user_api_reqs_per_minute = '200'
maxmind_license_key = '$maxmind_license_key'
smtp_port = '$DISCOURSE_SMTP_PORT'
maxmind_account_id = '$DISCOURSE_MAXMIND_ACCOUNT_ID'
smtp_password = '$DISCOURSE_SMTP_PASSWORD'
max_reqs_per_ip_per_minute = '20000'
notification_email = '$DISCOURSE_NOTIFICATION_EMAIL'
db_host = 'localhost'
enable_cors = 'true'
db_port = ''
max_reqs_per_ip_mode = 'none'
smtp_domain = '$DISCOURSE_SMTP_DOMAIN'
#max_admin_api_reqs_per_minute = '600'
smtp_force_tls='true'
EOT
cd /var/www/discourse && sed -i '/gem "rails_multisite"/i gem "rails"' Gemfile
cd /var/www/discourse && sed -i '/gem "image_optim"/i gem "image_optim_pack"' Gemfile
su - discourse -c 'cd /var/www/discourse && bundle install'
# fi
### PNPM (NODEJS)
su - discourse -c 'curl -fsSL https://get.pnpm.io/install.sh' | su - discourse -c 'sh -'
su - discourse -c 'source /home/discourse/.bashrc'
source /home/discourse/.rvm/scripts/rvm
su - discourse -c 'sed -i "s/~> 3.3/3.3.7/" /var/www/discourse/Gemfile'
chown -R discourse:discourse /var/www/discourse/Gemfile Gemfile.lock
apt-get -yqq install brotli # is it needed ????????????????
# fi
su - discourse -c 'cd /var/www/discourse && bin/rails db:create'
# fi
su - discourse -c 'source /home/discourse/.rvm/scripts/rvm'
tee -a .bashrc > /dev/null <<EOT
# pnpm
export PNPM_HOME="/home/discourse/.local/share/pnpm"
case ":\$PATH:" in
*":\$PNPM_HOME:"*) ;;
*) export PATH="\$PNPM_HOME:\$PATH" ;;
esac
# pnpm end
EOT
which pnpm
source .bashrc
pnpm env use --global lts # will install node 22 as of today
cd /var/www/discourse && pnpm i
chown -R discourse:discourse .
chown -R discourse:www-data public
exit
#################################
######### MANUAL ################
export PATH="$PATH:/home/discourse/.rvm/bin"
rvm install "ruby-3.3.7" # install in root
cd /var/www/discourse
bundle install
bundle exec rake db:migrate
bundle exec rake themes:update
bundle exec rake assets:precompile
######### MANUAL #################
##################################
fi
### puma.rb
#cd /var/www/discourse
cp /var/www/discourse/config/puma.rb /var/www/discourse/config/puma.rb_original
tee /var/www/discourse/config/puma.rb > /dev/null <<EOT
# 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
EOT
### CERTBOT
apt install -y certbot
certbot certonly --non-interactive --agree-tos --renew-with-new-domains --standalone --email $certbot_contact_email --rsa-key-size 4096 --http-01-port $certbot_renew_port -d $hostname
# fi
### NGINX
cp /var/www/discourse/config/nginx.sample.conf /etc/nginx/sites-enabled/discourse.conf
sed -i "s/80/$nginx_discourse_port/" /etc/nginx/sites-enabled/discourse.conf
sed -i "s/server_name _/server_name $hostname/" /etc/nginx/sites-enabled/discourse.conf
mkdir -p /var/nginx/cache
systemctl enable nginx
systemctl restart nginx
systemctl status nginx
### SYSTEMD SERVICE AUTOSTART
sudo tee /etc/systemd/system/discourse.service > /dev/null <<EOT
[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.3.7 --default` before this service is run
ExecStart=/usr/bin/bash -lc '/home/discourse/.rvm/gems/ruby-3.3.7/bin/puma -C config/puma.rb'
ExecReload=/usr/bin/bash -lc '/home/discourse/.rvm/gems/ruby-3.3.7/bin/pumactl restart'
# il faut cette ligne dessous
Environment=RAILS_ENV=production
# Restart configuration
Restart=always
RestartSec=5s
# Basic security measures
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
[Install]
WantedBy=multi-user.target
EOT
systemctl enable discourse
systemctl start discourse
# OR MANUAL START
# puma -C config/puma.rb
### SETTINGS
echo "LAST MANUAL SETTINGS:"
echo ""
echo "GO in Admin>files and add wanted file extensions - vidéos, audio et documents
echo "TO FORCE HTTPS: https://domain.com/admin/config/security"