# Deploy Discourse without Docker

**URL:** https://meta.discourse.org/t/deploy-discourse-without-docker/351194
**Category:** Sysadmins
**Tags:** unsupported-install, advanced-setup
**Created:** [February 8, 2025, 3:56am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194 "2025-02-08T03:56:12Z")
**Posts on this page:** 20
**Page:** 1

<div class="post-metadata">

### Author: ![fokx](https://avatars.discourse-cdn.com/v4/letter/f/958977/32.png) [@fokx](https://meta.discourse.org/u/fokx)
#### Post date: [February 8, 2025, 3:56am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/1 "2025-02-08T03:56:12Z")

</div>

Though it is more convenient and safer to deploy Discourse following the [official install](https://meta.discourse.org/t/142537?silent=true) guide, I want to dive deeper into the container and see how it can be deployed in Linux without Docker. I want to share the steps just for your information. You adapt it and use it **at your own risk**.

> **Dive into how Discourse is run in container**
>
> I take a look at the output of `./launcher start-cmd webonly`:
> 
> ```plaintext
> true run --shm-size=512m --link data:data -d --restart=always -e LANG=en_US.UTF-8 -e RAILS_ENV=production … --name webonly -t -v /var/discourse/shared/webonly:/shared … local_discourse/webonly /sbin/boot
> 
> ```
> 
> then look at `/sbin/boot` and `/etc/service/unicorn/run`, I get the core cmd to start Discourse:
> 
> ```plaintext
> LD_PRELOAD=$RUBY_ALLOCATOR HOME=/home/discourse USER=discourse exec thpoff chpst -u discourse:www-data -U discourse:www-data bundle exec config/unicorn_launcher -E production -c config/unicorn.conf.rb
> 
> ```

### prepare system

FYI, I use Ubuntu 24.04 and `zsh`.

Follow [PG’s offical installation guide](https://www.postgresql.org/download/) to install postgres from PostgreSQL Apt Repository. I installed the version 18, which works quite well, though the offical install uses 15 at time of writing.

Install `redis`(8.2 at time of writing, [official install](https://meta.discourse.org/t/142537?silent=true) uses 7.0), `nginx` and create dedicated user `discourse`:

```plaintext
apt install nginx libnginx-mod-http-brotli-static redis zsh zsh-autosuggestions zsh-syntax-highlighting
systemctl enable --now postgresql redis nginx
useradd -m -s /bin/zsh discourse

```

Install ImageMagick 7 (I use [IMEI](https://github.com/SoftCreatR/imei?tab=readme-ov-file#alternative-install-method)) and check the version. Mine is:

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

```

then, change user(`su - discourse`) and install [pnpm](https://pnpm.io/installation), [rvm](https://rvm.io/rvm/install).

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

```

then adapt and add the following config to your `.zshrc`

> **/home/discourse/.zshrc**
>
> ```sh
> 
> # pnpm
> export PNPM_HOME="/home/discourse/.local/share/pnpm"
> case ":$PATH:" in
> *":$PNPM_HOME:"*) ;;
> *) export PATH="$PNPM_HOME:$PATH" ;;
> esac
> # pnpm end
> alias npm='pnpm'
> alias npx='pnpx'
> 
> # Add RVM to PATH for scripting. Make sure this is the last PATH variable change.
> export PATH="$PATH:$HOME/.rvm/bin"
> 
> export ALLOW_EMBER_CLI_PROXY_BYPASS=1
> 
> export RAILS_ENV=production
> 
> export UNICORN_SIDEKIQ_MAX_RSS=1000
> export UNICORN_WORKERS=4
> export UNICORN_SIDEKIQS=1
> 
> export PUMA_SIDEKIQ_MAX_RSS=1000
> export PUMA_WORKERS=4
> export PUMA_SIDEKIQS=1
> 
> #export RUBY_YJIT_ENABLE=1
> #export RUBY_CONFIGURE_OPTS="--enable-yjit"
> export DISCOURSE_HOSTNAME=example.com
> export DISCOURSE_DEVELOPER_EMAILS=discourse-admin@example.com
> 
> export DISCOURSE_MAXMIND_ACCOUNT_ID=<id>
> export DISCOURSE_MAXMIND_LICENSE_KEY=<key>
> 
> export DISCOURSE_ENABLE_CORS=true
> export DISCOURSE_MAX_REQS_PER_IP_MODE=none
> export DISCOURSE_MAX_REQS_PER_IP_PER_MINUTE=20000
> export DISCOURSE_MAX_REQS_PER_IP_PER_10_SECONDS=5000
> export DISCOURSE_MAX_ASSET_REQS_PER_IP_PER_10_SECONDS=20000
> export DISCOURSE_MAX_REQS_RATE_LIMIT_ON_PRIVATE=false
> export DISCOURSE_MAX_USER_API_REQS_PER_MINUTE=200
> export DISCOURSE_MAX_USER_API_REQS_PER_DAY=28800
> export DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE=600
> export DISCOURSE_MAX_DATA_EXPLORER_API_REQ_MODE=none
> 
> export DISCOURSE_MAX_REQS_PER_IP_EXCEPTIONS="127.0.0.1 ::1"
> cd /var/www/discourse
> 
> ```

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

Install node and ruby:

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

```

### prepare db (and restore a backup)

```plaintext
sudo -u postgres createuser -s discourse                                                   
sudo -u postgres createdb discourse 

$sudo -u postgres psql discourse
psql>
ALTER USER discourse WITH PASSWORD 'xxx';
CREATE EXTENSION hstore;CREATE EXTENSION pg_trgm;
CREATE EXTENSION plpgsql;
CREATE EXTENSION unaccent;
CREATE EXTENSION vector;
# to restore database extracted from a backup:
$ gunzip < dump.sql.gz | psql discourse

```

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

### install Discourse

I consulted [discourse\_docker/templates/web.template.yml at 20e33fbfd98d3b8d9c57f7a111beff8aa51a5b98 · discourse/discourse\_docker · GitHub](https://github.com/discourse/discourse_docker/blob/20e33fbfd98d3b8d9c57f7a111beff8aa51a5b98/templates/web.template.yml#L65)

as user `root`:

```plaintext
cd /var/www/
git clone https://github.com/discourse/discourse
mkdir -p /var/www/discourse/public
chown -R discourse:discourse /var/www/discourse/     
chown -R discourse:www-data /var/www/discourse/public

```

configure config/discourse.conf:

> **config/discourse.conf**
>
> ```plaintext
> max_data_explorer_api_req_mode = 'none'
> max_user_api_reqs_per_day = '28800'
> hostname = '127.0.0.1'
> hostname = 'example.com'
> redis_host = '127.0.0.1'
> db_password = '<password>'
> db_socket = ''
> max_reqs_per_ip_per_10_seconds = '5000'
> max_asset_reqs_per_ip_per_10_seconds = '20000'
> max_reqs_rate_limit_on_private = 'false'
> developer_emails = 'discourse-admin@example.com'
> max_user_api_reqs_per_minute = '200'
> maxmind_license_key = '<key>'
> maxmind_account_id = '<id>'
> max_reqs_per_ip_per_minute = '20000'
> db_host = '127.0.0.1'
> enable_cors = 'true'
> db_port = ''
> max_reqs_per_ip_mode = 'none'
> max_admin_api_reqs_per_minute = '600'
> 
> smtp_user_name = '<name>'
> smtp_address = 'postal.example.com'
> smtp_port = '25'
> smtp_password = '<password>'
> smtp_domain = 'postalsend.example.com'
> notification_email = 'noreply@postalsend.example.com'
> 
> ```

Do the bundle / pnpm install, db migration, assets precompile stuff. This is also how you upgrade discourse and plugins.  
  
as user `discourse`:

```plaintext
cd /var/www/discourse
git stash
git pull
git checkout tests-passed 
cd plugins
for plugin in *
do
    echo $plugin; cd ${plugin}; git pull; cd ..
done
cd ../
sed -i '/gem "rails_multisite"/i gem "rails"' Gemfile
bundle install --jobs $(($(nproc) - 1))
pnpm i
bundle exec rake db:migrate
bundle exec rake themes:update
bundle exec rake assets:precompile

```

I don’t want to use `unicorn`. Heroku recommends using the [Puma web server](https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server) instead of Unicorn. Here is my `config/puma.rb` written after consulting `config/unicorn.conf.rb`:

> **config/puma.rb**
>
> ```rb
> # frozen_string_literal: true
> 
> require "fileutils"
> #require 'puma/acme'
> 
> discourse_path = File.expand_path(File.expand_path(File.dirname( __FILE__ )) + "/../")
> 
> enable_logstash_logger = ENV["ENABLE_LOGSTASH_LOGGER"] == "1"
> puma_stderr_path = "#{discourse_path}/log/puma.stderr.log"
> puma_stdout_path = "#{discourse_path}/log/puma.stdout.log"
> 
> # Load logstash logger if enabled
> if enable_logstash_logger
> require_relative "../lib/discourse_logstash_logger"
> FileUtils.touch(puma_stderr_path) if !File.exist?(puma_stderr_path)
> # Note: You may need to adapt the logger initialization for Puma
> log_formatter =
> proc do |severity, time, progname, msg|
> event = {
> "@timestamp" => Time.now.utc,
> "message" => msg,
> "severity" => severity,
> "type" => "puma",
> }
> "#{event.to_json}\n"
> end
> else
> stdout_redirect puma_stdout_path, puma_stderr_path, true
> end
> 
> # Number of workers (processes)
> workers ENV.fetch("PUMA_WORKERS", 6).to_i
> 
> # Set the directory
> directory discourse_path
> 
> # Bind to the specified address and port
> bind ENV.fetch(
> "PUMA_BIND",
> "tcp://#{ENV["PUMA_BIND_ALL"] ? "" : "127.0.0.1:"}#{ENV.fetch("PUMA_PORT", 3000)}",
> )
> #bind 'tcp://0.0.0.0:80'
> #customization:plugin :acme
> #acme_server_name 'example.com'
> #acme_tos_agreed true
> #bind 'acme://0.0.0.0:443'
> 
> # PID file location
> FileUtils.mkdir_p("#{discourse_path}/tmp/pids")
> pidfile ENV.fetch("PUMA_PID_PATH", "#{discourse_path}/tmp/pids/puma.pid")
> 
> # State file - used by pumactl
> state_path "#{discourse_path}/tmp/pids/puma.state"
> 
> # Environment-specific configuration
> if ENV["RAILS_ENV"] == "production"
> # Production timeout
> worker_timeout 30
> else
> # Development timeout
> worker_timeout ENV.fetch("PUMA_TIMEOUT", 60).to_i
> end
> 
> # Preload application
> preload_app!
> 
> # Handle worker boot and shutdown
> before_fork do
> Discourse.preload_rails!
> Discourse.before_fork
> 
> # Supervisor check
> supervisor_pid = ENV["PUMA_SUPERVISOR_PID"].to_i
> if supervisor_pid > 0
> Thread.new do
> loop do
> unless File.exist?("/proc/#{supervisor_pid}")
> puts "Kill self supervisor is gone"
> Process.kill "TERM", Process.pid
> end
> sleep 2
> end
> end
> end
> 
> # Sidekiq workers
> sidekiqs = ENV["PUMA_SIDEKIQS"].to_i
> if sidekiqs > 0
> puts "starting #{sidekiqs} supervised sidekiqs"
> 
> require "demon/sidekiq"
> Demon::Sidekiq.after_fork { DiscourseEvent.trigger(:sidekiq_fork_started) }
> Demon::Sidekiq.start(sidekiqs)
> 
> if Discourse.enable_sidekiq_logging?
> Signal.trap("USR1") do
> # Delay Sidekiq log reopening
> sleep 1
> Demon::Sidekiq.kill("USR2")
> end
> end
> end
> 
> # Email sync demon
> if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
> puts "starting up EmailSync demon"
> Demon::EmailSync.start(1)
> end
> 
> # Plugin demons
> DiscoursePluginRegistry.demon_processes.each do |demon_class|
> puts "starting #{demon_class.prefix} demon"
> demon_class.start(1)
> end
> 
> # Demon monitoring thread
> Thread.new do
> loop do
> begin
> sleep 60
> 
> if sidekiqs > 0
> Demon::Sidekiq.ensure_running
> Demon::Sidekiq.heartbeat_check
> Demon::Sidekiq.rss_memory_check
> end
> 
> if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true"
> Demon::EmailSync.ensure_running
> Demon::EmailSync.check_email_sync_heartbeat
> end
> 
> DiscoursePluginRegistry.demon_processes.each(&:ensure_running)
> rescue => e
> Rails.logger.warn(
> "Error in demon processes heartbeat check: #{e}\n#{e.backtrace.join("\n")}",
> )
> end
> end
> end
> 
> # Close Redis connection
> Discourse.redis.close
> end
> 
> on_worker_boot do
> DiscourseEvent.trigger(:web_fork_started)
> Discourse.after_fork
> end
> 
> # Worker timeout handling
> worker_timeout 30
> 
> # Low-level worker options
> threads 8, 32
> 
> ```

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

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

> **/etc/systemd/system/discourse.service**
>
> ```plaintext
> [Unit]
> Description=Discourse with Puma Server
> After=network.target postgresql.service
> Requires=postgresql.service
> 
> [Service]
> Type=simple
> User=discourse
> Group=discourse
> WorkingDirectory=/var/www/discourse
> # requires running `rvm 3.4.6 --default` before this service is run
> ExecStart=/usr/bin/zsh -lc 'source /home/discourse/.zshrc && /home/discourse/.rvm/gems/ruby-3.4.6/bin/puma -C config/puma.rb'
> ExecReload=/usr/bin/zsh -lc 'source /home/discourse/.zshrc && /home/discourse/.rvm/gems/ruby-3.4.6/bin/pumactl restart'
> 
> # Restart configuration
> Restart=always
> RestartSec=5s
> 
> # Basic security measures
> NoNewPrivileges=true
> ProtectSystem=full
> ProtectHome=read-only
> 
> [Install]
> WantedBy=multi-user.target
> 
> ```

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

> **/etc/nginx/sites-enabled/discourse.conf**
>
> ```sh
> # Additional MIME types that you'd like nginx to handle go in here
> types {
> text/csv csv;
> #application/wasm wasm;
> }
> 
> upstream discourse { server 127.0.0.1:3000; }
> 
> # inactive means we keep stuff around for 1440m minutes regardless of last access (1 week)
> # levels means it is a 2 deep hierarchy cause we can have lots of files
> # max_size limits the size of the cache
> proxy_cache_path /var/nginx/cache inactive=1440m levels=1:2 keys_zone=one:10m max_size=600m;
> 
> # Increased from the default value to acommodate large cookies during oAuth2 flows
> # like in https://meta.discourse.org/t/x/74060 and large CSP and Link (preload) headers
> proxy_buffer_size 32k;
> proxy_buffers 4 32k;
> 
> # Increased from the default value to allow for a large volume of cookies in request headers
> # Discourse itself tries to minimise cookie size, but we cannot control other cookies set by other tools on the same domain.
> large_client_header_buffers 4 32k;
> 
> # attempt to preserve the proto, must be in http context
> map $http_x_forwarded_proto $thescheme {
> default $scheme;
> "~https$" https;
> }
> 
> log_format log_discourse '[$time_local] "$http_host" $remote_addr "$request" "$http_user_agent" "$sent_http_x_discourse_route" $status $bytes_sent "$http_referer" $upstream_response_time $request_time "$upstream_http_x_discourse_username" "$upstream_http_x_discourse_trackview" "$upstream_http_x_queue_time" "$upstream_http_x_redis_calls" "$upstream_http_x_redis_time" "$upstream_http_x_sql_calls" "$upstream_http_x_sql_time"';
> 
> # Allow bypass cache from localhost
> #geo $bypass_cache {
> # default 0;
> # 127.0.0.1 1;
> # ::1 1;
> #}
> 
> limit_req_zone $binary_remote_addr zone=flood:10m rate=12r/s;
> limit_req_zone $binary_remote_addr zone=bot:10m rate=200r/m;
> limit_req_status 429;
> limit_conn_zone $binary_remote_addr zone=connperip:10m;
> limit_conn_status 429;
> server {
> access_log /var/log/nginx/access.log log_discourse;
>   
> #listen unix:/var/nginx/nginx.http.sock;
> listen 443 ssl;
> listen [::]:443 ssl;
> server_name example.com;
> ssl_certificate /etc/nginx/ssl/example.com.cer;
> ssl_certificate_key /etc/nginx/ssl/example.com.key;
> ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
> ssl_ciphers HIGH:!aNULL:!MD5;
> 
> set_real_ip_from unix:;
> set_real_ip_from 127.0.0.1/32;
> set_real_ip_from ::1/128;
> real_ip_header X-Forwarded-For;
> real_ip_recursive on;
> 
> gzip on;
> gzip_vary on;
> gzip_min_length 1000;
> gzip_comp_level 5;
> gzip_types application/json text/css text/javascript application/x-javascript application/javascript image/svg+xml application/wasm;
> gzip_proxied any;
> 
> # Uncomment and configure this section for HTTPS support
> # NOTE: Put your ssl cert in your main nginx config directory (/etc/nginx)
> #
> # rewrite ^/(.*) https://enter.your.web.hostname.here/$1 permanent;
> #
> # listen 443 ssl;
> # ssl_certificate your-hostname-cert.pem;
> # ssl_certificate_key your-hostname-cert.key;
> # ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
> # ssl_ciphers HIGH:!aNULL:!MD5;
> #
> 
> server_tokens off;
> 
> sendfile on;
> 
> keepalive_timeout 65;
> 
> # maximum file upload size (keep up to date when changing the corresponding site setting)
> client_max_body_size 128m ;
> 
> # path to discourse's public directory
> set $public /var/www/discourse/public;
> 
> # without weak etags we get zero benefit from etags on dynamically compressed content
> # further more etags are based on the file in nginx not sha of data
> # use dates, it solves the problem fine even cross server
> etag off;
> 
> # prevent direct download of backups
> location ^~ /backups/ {
> internal;
> }
> 
> # bypass rails stack with a cheap 204 for favicon.ico requests
> location /favicon.ico {
> return 204;
> access_log off;
> log_not_found off;
> }
> 
> location / {
> root $public;
> add_header ETag "";
> 
> # auth_basic on;
> # auth_basic_user_file /etc/nginx/htpasswd;
> 
> location ~ ^/uploads/short-url/ {
> proxy_set_header Host $http_host;
> proxy_set_header X-Real-IP $remote_addr;
> proxy_set_header X-Request-Start "t=${msec}";
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
> proxy_set_header X-Forwarded-Proto $thescheme;
> proxy_pass http://discourse;
> break;
> }
> 
> location ~ ^/(secure-media-uploads/|secure-uploads)/ {
> proxy_set_header Host $http_host;
> proxy_set_header X-Real-IP $remote_addr;
> proxy_set_header X-Request-Start "t=${msec}";
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
> proxy_set_header X-Forwarded-Proto $thescheme;
> proxy_pass http://discourse;
> break;
> }
> 
> location ~* (fonts|assets|plugins|uploads)/.*\.(eot|ttf|woff|woff2|ico|otf)$ {
> expires 1y;
> add_header Cache-Control public,immutable;
> add_header Access-Control-Allow-Origin *;
> }
> 
> location = /srv/status {
> access_log off;
> log_not_found off;
> proxy_set_header Host $http_host;
> proxy_set_header X-Real-IP $remote_addr;
> proxy_set_header X-Request-Start "t=${msec}";
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
> proxy_set_header X-Forwarded-Proto $thescheme;
> proxy_pass http://discourse;
> break;
> }
> 
> # some minimal caching here so we don't keep asking
> # longer term we should increase probably to 1y
> location ~ ^/javascripts/ {
> expires 1d;
> add_header Cache-Control public,immutable;
> add_header Access-Control-Allow-Origin *;
> }
> 
> location ~ ^/assets/(?<asset_path>.+)$ {
> expires 1y;
> # asset pipeline enables this
> brotli_static on;
> gzip_static on;
> add_header Cache-Control public,immutable;
> # HOOK in asset location (used for extensibility)
> # TODO I don't think this break is needed, it just breaks out of rewrite
> break;
> }
> 
> location ~ ^/plugins/ {
> expires 1y;
> add_header Cache-Control public,immutable;
> add_header Access-Control-Allow-Origin *;
> }
> 
> # cache emojis
> location ~ /images/emoji/ {
> expires 1y;
> add_header Cache-Control public,immutable;
> add_header Access-Control-Allow-Origin *;
> }
> 
> location ~ ^/uploads/ {
> 
> # NOTE: it is really annoying that we can't just define headers
> # at the top level and inherit.
> #
> # proxy_set_header DOES NOT inherit, by design, we must repeat it,
> # otherwise headers are not set correctly
> proxy_set_header Host $http_host;
> proxy_set_header X-Real-IP $remote_addr;
> proxy_set_header X-Request-Start "t=${msec}";
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
> proxy_set_header X-Forwarded-Proto $thescheme;
> proxy_set_header X-Sendfile-Type X-Accel-Redirect;
> proxy_set_header X-Accel-Mapping $public/=/downloads/;
> expires 1y;
> add_header Cache-Control public,immutable;
> 
> ## optional upload anti-hotlinking rules
> #valid_referers none blocked mysite.com *.mysite.com;
> #if ($invalid_referer) { return 403; }
> 
> # custom CSS
> location ~ /stylesheet-cache/ {
> add_header Access-Control-Allow-Origin *;
> try_files $uri =404;
> }
> # this allows us to bypass rails
> location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp|avif)$ {
> add_header Access-Control-Allow-Origin *;
> try_files $uri =404;
> }
> # SVG needs an extra header attached
> location ~* \.(svg)$ {
> }
> # thumbnails & optimized images
> location ~ /_?optimized/ {
> add_header Access-Control-Allow-Origin *;
> try_files $uri =404;
> }
> 
> proxy_pass http://discourse;
> break;
> }
> 
> location ~ ^/admin/backups/ {
> proxy_set_header Host $http_host;
> proxy_set_header X-Real-IP $remote_addr;
> proxy_set_header X-Request-Start "t=${msec}";
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
> proxy_set_header X-Forwarded-Proto $thescheme;
> proxy_set_header X-Sendfile-Type X-Accel-Redirect;
> proxy_set_header X-Accel-Mapping $public/=/downloads/;
> proxy_pass http://discourse;
> break;
> }
> 
> # This big block is needed so we can selectively enable
> # acceleration for backups, avatars, sprites and so on.
> # see note about repetition above
> location ~ ^/(svg-sprite/|letter_avatar/|letter_avatar_proxy/|user_avatar|highlight-js|stylesheets|theme-javascripts|favicon/proxied|service-worker) {
> proxy_set_header Host $http_host;
> proxy_set_header X-Real-IP $remote_addr;
> proxy_set_header X-Request-Start "t=${msec}";
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
> proxy_set_header X-Forwarded-Proto $thescheme;
> 
> # if Set-Cookie is in the response nothing gets cached
> # this is double bad cause we are not passing last modified in
> proxy_ignore_headers "Set-Cookie";
> proxy_hide_header "Set-Cookie";
> proxy_hide_header "X-Discourse-Username";
> proxy_hide_header "X-Runtime";
> 
> # note x-accel-redirect can not be used with proxy_cache
> proxy_cache one;
> proxy_cache_key "$scheme,$host,$request_uri";
> proxy_cache_valid 200 301 302 7d;
> #proxy_cache_bypass $bypass_cache;
> proxy_pass http://discourse;
> break;
> }
> 
> # we need buffering off for message bus
> location /message-bus/ {
> proxy_set_header X-Request-Start "t=${msec}";
> proxy_set_header Host $http_host;
> proxy_set_header X-Real-IP $remote_addr;
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
> proxy_set_header X-Forwarded-Proto $thescheme;
> proxy_http_version 1.1;
> proxy_buffering off;
> proxy_pass http://discourse;
> break;
> }
> 
> # this means every file in public is tried first
> try_files $uri @discourse;
> }
> 
> location /downloads/ {
> internal;
> alias $public/;
> }
> 
> location @discourse {
> limit_conn connperip 20;
> limit_req zone=flood burst=12 nodelay;
> limit_req zone=bot burst=100 nodelay;
> proxy_set_header Host $http_host;
> proxy_set_header X-Request-Start "t=${msec}";
> proxy_set_header X-Real-IP $remote_addr;
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
> proxy_set_header X-Forwarded-Proto $thescheme;
> proxy_pass http://discourse;
> }
> 
> }
> 
> ```

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

### maintenance

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

To upgrade Discourse, consult [#upgrade-cmd](#upgrade-cmd), then restart puma using either `puma restart` or `puma phased-restart`. For the difference, consult [puma/docs/restart.md at main · puma/puma · GitHub](https://github.com/puma/puma/blob/master/docs/restart.md) .

---

<div class="post-metadata">

### Author: ![NateDhaliwal](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/natedhaliwal/32/313494_2.png) [@NateDhaliwal](https://meta.discourse.org/u/NateDhaliwal)
#### Post date: [March 22, 2025, 11:57pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/2 "2025-03-22T23:57:03Z")

</div>

Should this be moved to #community-wiki:sysadmins, perhaps?

---

<div class="post-metadata">

### Author: ![lion](https://avatars.discourse-cdn.com/v4/letter/l/57b2e6/32.png) [@lion](https://meta.discourse.org/u/lion)
#### Post date: [September 14, 2025, 11:33pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/3 "2025-09-14T23:33:56Z")

</div>

Hi. Thanks for sharing.

I am writing a script for this to installed on debian 12 in a lxc container. It is almost finished and works fine. I will publish when it is ready.

I’ve managed to display the first page for the admin registration. But the confirmation email is sent to discourse@myhostname, and to myhostname as smtp\_server, so it is absurd. The varaibles in .bashrc (or .zshrc), neither in the discourse.conf are taken in account for sending the email. The email\_adress for the developper is correct but all other parameters are wrong and I could not change them. Would you have any idea to know how to achieve this?

---

<div class="post-metadata">

### Author: ![fokx](https://avatars.discourse-cdn.com/v4/letter/f/958977/32.png) [@fokx](https://meta.discourse.org/u/fokx)
#### Post date: [September 15, 2025, 12:19pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/4 "2025-09-15T12:19:15Z")

</div>

According to code

> <https://github.com/discourse/discourse/blob/ccf379900d2496a8612171637740761765a403dc/app/models/global_setting.rb#L380>

SMTP should be configured in `config/discourse.conf`.  
For me, I have these lines in it:

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

```

Have you [checked logs](https://meta.discourse.org/t/troubleshoot-email-on-a-new-discourse-install/16326#p-55742-what-do-the-discourse-logs-say-5)? For this custom install, the log is in production.log, production.log, puma.stdout.log under log dir.

---

<div class="post-metadata">

### Author: ![lion](https://avatars.discourse-cdn.com/v4/letter/l/57b2e6/32.png) [@lion](https://meta.discourse.org/u/lion)
#### Post date: [September 15, 2025, 5:43pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/5 "2025-09-15T17:43:06Z")

</div>

Thanks a lot for your response.

I have already these settings in config/discourse.conf. I did exactly what you wrote except with zsh.  
My logs don’t speak about smtp, and the only from (production.log) it is

> 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](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)

My mail server logs do not show any entries from discourse server (works for all my other servers (all lxc containers). I can send successfully a mail via `mail` terminal command.

with `puma -C config/puma.rb`:

> Use ‘before\_worker\_boot’, ‘on\_worker\_boot’ is deprecated and will be removed in v8  
> [498] Puma starting in cluster mode…  
> [498] \* Puma version: 7.0.0 (“Romantic Warrior”)
> 
> 498\] \* Ruby version: ruby 3.3.9 (2025-07-24 revision f5c772fc7c) \[x86\_64-linux
> 
> [498] \* Min threads: 8  
> [498] \* Max threads: 32  
> [498] \* Environment: production  
> [498] \* Master PID: 498  
> [498] \* Workers: 8  
> [498] \* Restarts: (✔) hot (✖) phased (✖) refork  
> [498] \* Preloading application  
> [498] \* Listening on [http://127.0.0.1:3000](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 is like your .zhrc  
modifying developper\_emails or db\_password entries works (correct email is displayed on the website admin registration page), but other smtp parameters are ignored.

---

<div class="post-metadata">

### Author: ![lion](https://avatars.discourse-cdn.com/v4/letter/l/57b2e6/32.png) [@lion](https://meta.discourse.org/u/lion)
#### Post date: [September 17, 2025, 12:31am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/6 "2025-09-17T00:31:08Z")

</div>

Your config/puma.rb file contains some errors (around line with le port 3000). Could you please provide it again?

I registered the admin with rails c and after that i get the “Oops…” page on the start page. No pages are rendered correctly.

Please help me.

---

<div class="post-metadata">

### Author: ![dodger](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/dodger/32/522280_2.png) [@dodger](https://meta.discourse.org/u/dodger)
#### Post date: [September 18, 2025, 6:02pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/7 "2025-09-18T18:02:52Z")

</div>

@lion , try this one:

```ruby
# 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

```

---

<div class="post-metadata">

### Author: ![lion](https://avatars.discourse-cdn.com/v4/letter/l/57b2e6/32.png) [@lion](https://meta.discourse.org/u/lion)
#### Post date: [September 18, 2025, 11:46pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/8 "2025-09-18T23:46:06Z")

</div>

Thanks. I’ve forgot to say that it is behind haproxy for multiple servers.

1. I get “mix content” from the console of the browser. What can i change in the nginx file?
2. I get this log about magick at boot of puma command. It is although installed at begin.

> ==\> /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!’  
> I have read it can be a cause for not showing pages.

---

<div class="post-metadata">

### Author: ![fokx](https://avatars.discourse-cdn.com/v4/letter/f/958977/32.png) [@fokx](https://meta.discourse.org/u/fokx)
#### Post date: [September 19, 2025, 6:11am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/9 "2025-09-19T06:11:10Z")

</div>

> [@lion](#):
>
> I get “mix content” from the console of the browser. What can i change in the nginx file?

Do you force ssl in Discourse settings?

> [@lion](#):
>
> /var/www/discourse/lib/letter\_avatar.rb:112:in ``‘: No such file or directory - magick (Errno::ENOENT)

Can you run `magick --version` as discourse user? For me the output is:  
Version: ImageMagick 7.1.1-45 Q16-HDRI x86\_64 3cbce5696:20250308 [https://imagemagick.org](https://imagemagick.org)  
Copyright: (C) 1999 ImageMagick Studio LLC  
License: [ImageMagick | License](https://imagemagick.org/script/license.php)  
Features: Cipher DPC HDRI Modules OpenMP(4.5)  
Delegates (built-in): bzlib cairo djvu fftw fontconfig freetype gslib gvc heic jbig jng jp2 jpeg jxl lcms lqr ltdl lzma openexr pangocairo png ps raqm raw rsvg tiff webp wmf x xml zip zlib zstd  
Compiler: gcc (13.3)

---

<div class="post-metadata">

### Author: ![dodger](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/dodger/32/522280_2.png) [@dodger](https://meta.discourse.org/u/dodger)
#### Post date: [September 19, 2025, 6:32am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/10 "2025-09-19T06:32:15Z")

</div>

Yup, exactly what I was thinking.

I’ve found a lot of un-met dependencies while “manually” installing. I’m writing an ansible to be able to deploy discourse on multiple environments.

My systems are almalinux/redhat but I’ll share what I’ve found when I consider it almost done.

---

<div class="post-metadata">

### Author: ![lion](https://avatars.discourse-cdn.com/v4/letter/l/57b2e6/32.png) [@lion](https://meta.discourse.org/u/lion)
#### Post date: [September 19, 2025, 10:21am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/11 "2025-09-19T10:21:46Z")

</div>

In haproxy.cfg 443 is with crt, so encrypted, redirected to the container through 8080:

> backend BACKEND\_XXX  
> option forwardfor  
> http-request set-header X-Forwarded-Port %[dst\_port]  
> http-request add-header X-Forwarded-Proto https if { ssl\_fc }  
> server xxx 192.168.1.48:8080 weight 1 #adding “check ssl verify none” if ssl on nginx discourse

1. In nginx/…/discourse.conf, i copied the version of this post and modified the port and the servername and the certificate files location,  
=\> PB1: got mixed contents and logs with same problem of magick  
=\> PB2: on “/confirm-email” page i get an error "site not found " of browser.

2. Then I modified the file removing the ssl on port and commented all lines with “$thescheme”, to only encrypt in haproxy.  
=\> 502 error on first page, same logs with magick

3. Then I installed magick from source (7.1.2.3), added binary folder to PATH in .bashrc for root and for discourse users. It did not work with via apt install (no effect like this one).  
=\> 502 error went away, still mixed content and "site not found " of browser.  
=\> logs still magick not found but different.

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

Conclusion:  
How to make magick foundable by puma?  
How to disable ssl in nginx because of haproxy?

---

<div class="post-metadata">

### Author: ![lion](https://avatars.discourse-cdn.com/v4/letter/l/57b2e6/32.png) [@lion](https://meta.discourse.org/u/lion)
#### Post date: [September 19, 2025, 7:41pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/12 "2025-09-19T19:41:41Z")

</div>

I’ve managed to get it work like described in my first post, but here without ssl on nginx by copying the nginx.config.sample with just changing the hostname and port.  
So both with ssl or without on nginx, I get the first page for admin registration until resend email page, but BOTH:

1. display “mixed content” (for images files for instance),
2. other pages than admin registration shows “Ooops…”
3. no confirmation emails are sent
4. magick is still not found

---

<div class="post-metadata">

### Author: ![dodger](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/dodger/32/522280_2.png) [@dodger](https://meta.discourse.org/u/dodger)
#### Post date: [September 20, 2025, 6:42am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/13 "2025-09-20T06:42:36Z")

</div>

Somewhere in one of your posts you mention container.

Are you using containers for any of the involved parts?

Today i wanna finalise my deployment in the preprod environment and maybe I’ll have more info to share.

---

<div class="post-metadata">

### Author: ![dodger](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/dodger/32/522280_2.png) [@dodger](https://meta.discourse.org/u/dodger)
#### Post date: [September 20, 2025, 11:11am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/14 "2025-09-20T11:11:30Z")

</div>

Same as you 😒

- Not sending confirmation email (I’m using mailtrap, I’m sure it works).
- Created admin user with rake
- Oops on `/`after that…
- Also seen magick issue and magick is installed (I’m pretty sure there’s some rubygem missing…)

I’m deploying it using docker and then compare. I hate docker 😑 but I’ll wasting a lot of precious time…

---

<div class="post-metadata">

### Author: ![NateDhaliwal](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/natedhaliwal/32/313494_2.png) [@NateDhaliwal](https://meta.discourse.org/u/NateDhaliwal)
#### Post date: [September 20, 2025, 11:50am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/15 "2025-09-20T11:50:15Z")

</div>

> [@dodger](#):
>
> I hate docker 😑

I barely know Docker (if any at all!) and I didn’t have to know it to run a Discourse forum in production. Just by following the supported instructions, it’ll get you what you need.

---

<div class="post-metadata">

### Author: ![dodger](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/dodger/32/522280_2.png) [@dodger](https://meta.discourse.org/u/dodger)
#### Post date: [September 20, 2025, 1:54pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/16 "2025-09-20T13:54:55Z")

</div>

Yup, I believe you. But i need to know what I’m deploying and how it works.

It’s the best way to find and solve problems 😝

Because you’ll have problems, **always** maybe not now, but I’ll sure the future you will find someone.

Anyway, as i won’t get support using alternative install method, I’ll follow the official instructions and try to help others 😜

---

<div class="post-metadata">

### Author: ![lion](https://avatars.discourse-cdn.com/v4/letter/l/57b2e6/32.png) [@lion](https://meta.discourse.org/u/lion)
#### Post date: [September 20, 2025, 2:50pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/17 "2025-09-20T14:50:57Z")

</div>

I also spent a lot of time on these issues.  
About Docker, look at this issue on Docker (a mistake or a backdoor ?..), corrected at the moment: [https://youtu.be/dTqxNc1MVLE](https://youtu.be/dTqxNc1MVLE)  
That is one of my reasons why I would not use Docker.  
I am using lxd (lxc) containers, and i feel fine with it. I am going to install Docker on an lxc container at first, and export the database later when the install without Docker on an lxc is possible.

---

<div class="post-metadata">

### Author: ![lion](https://avatars.discourse-cdn.com/v4/letter/l/57b2e6/32.png) [@lion](https://meta.discourse.org/u/lion)
#### Post date: [September 20, 2025, 3:00pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/18 "2025-09-20T15:00:05Z")

</div>

> [@Deploy Discourse without Docker](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/9):
>
> Do you force ssl in Discourse settings? Can you run magick --version as discourse user? For me the output is: Version: ImageMagick 7.1.1-45 Q16-HDRI x86\_64 3cbce5696:20250308 [https://imagemagick.org](https://imagemagick.org) Copyright: (C) 1999 ImageMagick Studio LLC License: [ImageMagick | License](https://imagemagick.org/script/license.php) Features: Cipher DPC HDRI Modules OpenMP(4.5) Delegates (built-in): bzlib cairo djvu fftw fontconfig freetype gslib gvc heic jbig jng jp2 jpeg jxl lcms lqr ltdl lzma openexr pangocairo png ps raqm raw rsvg tiff webp wmf…

Well I am not forcing Discourse to ssl because I want to let my haproxy do it because haproxy passtrough does not work with http protocol to redirect GET requests, and i am handling multiple websites so i need http protocol in haproxy, so it requires ssl handling on haproxy side. I would like to avoid double ssl gateway.  
So haproxy (on one lxc) listens 443: redirect to 8080 (no ssl) to my discourse container (lxc).  
Curious is that the nginx-config-sample given in the discourse folder is configured without ssl and port 80, so it should work well, but i get the above mentioned problems.

---

<div class="post-metadata">

### Author: ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)
#### Post date: [September 20, 2025, 9:52pm UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/19 "2025-09-20T21:52:39Z")

</div>

> [@lion](#):
>
> Well I am not forcing Discourse to ssl

You need to do that to tell discourse to send only https links. It’s not changing the redirects, but the need for them.

You need to turn on the force https setting.

---

<div class="post-metadata">

### Author: ![lion](https://avatars.discourse-cdn.com/v4/letter/l/57b2e6/32.png) [@lion](https://meta.discourse.org/u/lion)
#### Post date: [September 21, 2025, 1:45am UTC](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/21 "2025-09-21T01:45:15Z")

</div>

> [@Deploy Discourse without Docker](https://meta.discourse.org/t/deploy-discourse-without-docker/351194/19):
>
> You need to do that to tell discourse to send only https links. It’s not changing the redirects, but the need for them. You need to turn on the force https setting.

HTTPS: Yes, but please tell me how to turn it on?

A GREAT NEWS! I HAVE RESOLVED THE PROBLEMS!!! The source was magick programm. After correct magick install from source version 7 (do not install imagemagick with apt, it is version 6), my discourse website displays all pages correct! Emails, admin registration, etc. work! After “mixed content” problem resolved, i will publish my script for installation on a lxd-lxc container (Debian 12) behind haproxy.on another lxd-lxc container.

[Next page](https://meta.discourse.org/t/deploy-discourse-without-docker/351194.md?page=2)
