Discourse is the heaviest of the mainstream open-source forum platforms and the least forgiving about how you install it. It is a Ruby on Rails application backed by PostgreSQL and Redis, with a Sidekiq job queue and an Ember frontend. The Discourse team supports exactly one installation method: their Docker container, driven by the launcher script in /var/discourse. Anything else (bare-metal Ruby, cPanel, Plesk, a hand-rolled Compose file) is unsupported and will cost you time on every upgrade.
This guide deploys Discourse on a single RamNode KVM VPS with Let's Encrypt TLS, an external SMTP relay, automated backups, and a tuned memory profile.
1. Choose the right RamNode instance
Discourse is memory-bound before it is CPU-bound. The container bootstrap alone compiles assets and will fail or thrash on an underprovisioned box.
| Community size | RAM | vCPU | Disk | Notes |
|---|---|---|---|---|
| Testing / under 20 active users | 2 GB | 1 | 30 GB | Requires swap. Bootstrap is slow but completes. |
| Small production, under 200 active users | 4 GB | 2 | 50 GB | Recommended floor for anything real. |
| 200 to 1000 active users | 8 GB | 4 | 80 GB+ | Room for plugins, uploads, and Sidekiq concurrency. |
Notes specific to RamNode:
- Pick KVM, not a container plan. Discourse needs its own kernel-level Docker daemon. Do not attempt this on an OpenVZ-style container.
- Pick a location near your users. RamNode has US and EU locations. Discourse is chatty over WebSockets, so latency shows up in the live-update experience.
- Ubuntu 24.04 LTS x86_64 is the template to select. Discourse's install script targets Ubuntu LTS.
- Budget disk for uploads and backups. Backups land on the same volume by default. A forum with heavy image uploads will outgrow a 30 GB plan faster than you expect.
2. Prerequisites you must have before touching the server
- A domain or subdomain (for example
forum.example.com) with an A record pointed at your RamNode IPv4. Add an AAAA record if you are using the IPv6 allocation. Let the DNS propagate fully before you run the installer, because Let's Encrypt validation happens during bootstrap. - An external SMTP relay. This is not optional and it is the single most important RamNode-specific constraint in this guide. See the next section.
- SSH access as root or a sudo user.
The SMTP situation on RamNode
RamNode does not permit mail services on its VPS platform, and outbound port 25 is not available to you. Discourse cannot function without working email: account activation, password resets, digests, and notification replies all depend on it. Registration will silently stall at "check your inbox" if SMTP is broken.
So you relay through a transactional email provider over port 587 (STARTTLS) or 465 (implicit TLS). Any of these work:
- Postmark
- SendGrid
- Mailgun
- Amazon SES
- Brevo
- Scaleway TEM
Before you start the install, have these five values in hand:
SMTP host e.g. smtp.postmarkapp.com
SMTP port 587
SMTP username provider-supplied
SMTP password provider-supplied
Notification from address e.g. forum@example.comSet up SPF, DKIM, and DMARC records for your sending domain at the provider before your first user registers. Discourse sends a lot of mail, and a cold domain with no DKIM will land in spam and generate support tickets you did not need to have.
3. Base server preparation
SSH in and bring the box current.
apt update && apt upgrade -yIf the kernel was updated, reboot before continuing.
Set the hostname and timezone:
hostnamectl set-hostname forum.example.com
timedatectl set-timezone UTCAdd swap
Mandatory on 2 GB plans, recommended on 4 GB. Discourse's asset compilation step is the peak memory consumer and swap keeps the OOM killer away from it.
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
sysctl -w vm.swappiness=10
echo 'vm.swappiness=10' >> /etc/sysctl.d/99-discourse.confVerify with free -h.
Firewall
Discourse's container binds 80 and 443 directly on the host. Nothing else needs to be exposed.
apt install -y ufw
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
ufw status verboseIf you changed your SSH port, adjust before enabling. Locking yourself out of a RamNode VPS means a console session through the panel.
Unattended security updates and fail2ban
apt install -y unattended-upgrades fail2ban
dpkg-reconfigure -plow unattended-upgrades
systemctl enable --now fail2banNote that unattended-upgrades patches the host. It does not update Discourse itself, which lives inside the container. That is a separate process covered in section 8.
4. Install Discourse
There are two paths. Both end in the same place.
Path A: the official one-line installer (recommended)
Discourse now ships a bootstrapping script that installs Docker and git if missing, clones the Docker repo, and launches the setup wizard:
wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/main/install-discourse | sudo bashThe installer auto-tunes UNICORN_WORKERS and db_shared_buffers based on detected RAM and CPU, which is a meaningful improvement over hand-editing those values.
If you would rather read a script before piping it to bash (you should), download it first:
wget https://raw.githubusercontent.com/discourse/discourse_docker/main/install-discourse
less install-discourse
sudo bash install-discoursePath B: manual clone and setup
More transparent, and closer to what you will be doing on every subsequent maintenance task anyway.
# Install Docker
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
docker version
# Clone the Discourse Docker repo
git clone https://github.com/discourse/discourse_docker.git /var/discourse
cd /var/discourse
chmod 700 containers
./discourse-setupdiscourse-setup prompts for:
Hostname for your Discourse? forum.example.com
Email address for admin account(s)? you@example.com
SMTP server address? smtp.postmarkapp.com
SMTP port? 587
SMTP user name? <provider token>
SMTP password? <provider token>
notification email address? forum@example.com
Optional email address for Let's Encrypt warnings? you@example.comAnswer the Let's Encrypt prompt with a real address. Skipping it means you fall back to HTTP only and have to wire TLS in manually later.
The script writes /var/discourse/containers/app.yml and then runs the bootstrap. Bootstrap takes anywhere from 3 minutes on a 4 GB / 4 vCPU plan to 15 minutes on a 2 GB single-core plan. It is compiling assets, so do not interrupt it.
When it finishes:
./launcher start appBrowse to https://forum.example.com. A 502 for the first minute or two is normal while Unicorn spins up. Register the admin account using the email you supplied.
5. Understanding app.yml
Everything about your instance lives in /var/discourse/containers/app.yml. Read it once, properly. The parts that matter:
templates:
- "templates/postgres.template.yml"
- "templates/redis.template.yml"
- "templates/web.template.yml"
- "templates/web.ratelimited.template.yml"
- "templates/web.ssl.template.yml"
- "templates/web.letsencrypt.ssl.template.yml"
expose:
- "80:80"
- "443:443"
params:
db_default_text_search_config: "pg_catalog.english"
db_shared_buffers: "1024MB"
UNICORN_WORKERS: 4
env:
LANG: en_US.UTF-8
DISCOURSE_HOSTNAME: forum.example.com
DISCOURSE_DEVELOPER_EMAILS: 'you@example.com'
DISCOURSE_SMTP_ADDRESS: smtp.postmarkapp.com
DISCOURSE_SMTP_PORT: 587
DISCOURSE_SMTP_USER_NAME: <token>
DISCOURSE_SMTP_PASSWORD: <token>
DISCOURSE_SMTP_ENABLE_START_TLS: true
DISCOURSE_NOTIFICATION_EMAIL: forum@example.com
LETSENCRYPT_ACCOUNT_EMAIL: you@example.comTwo rules to internalize:
- Every change to
app.ymlrequires a rebuild, not a restart../launcher rebuild apptears the container down, rebuilds it, and brings it back. Expect several minutes of downtime. - The two SSL templates must both be present and in that order, and
web.letsencrypt.ssl.template.ymlmust come afterweb.ssl.template.yml. Getting this backwards produces certificate errors that are annoying to diagnose.
Tuning by plan size
The auto-tuner does a decent job, but if you are hand-editing:
| Plan RAM | UNICORN_WORKERS | db_shared_buffers |
|---|---|---|
| 2 GB | 2 | 128MB |
| 4 GB | 3 to 4 | 512MB |
| 8 GB | 6 to 8 | 1024MB |
Each Unicorn worker costs roughly 250 to 400 MB resident once warm. Over-provisioning workers on a small RamNode plan is the number one cause of self-inflicted OOM kills.
6. TLS certificates
If you enabled the Let's Encrypt templates during setup, the container handles issuance and renewal internally via its own cron. You do not run certbot on the host, and you should not install nginx on the host either, because the container owns 80 and 443.
Verify:
cd /var/discourse
./launcher enter app
ls -la /shared/letsencrypt/
exitIf issuance failed, the near-universal causes are:
- DNS not propagated when bootstrap ran. Fix DNS, then
./launcher rebuild app. - ufw blocking 80. Let's Encrypt HTTP-01 validation needs port 80 reachable from the internet.
- The A record points somewhere other than your RamNode IP.
Behind Cloudflare
If you front the forum with Cloudflare, set the proxy status to DNS-only during the initial bootstrap so HTTP-01 validation succeeds, then turn the proxy on afterward and set SSL mode to Full (strict). Leaving the orange cloud on during first bootstrap is a common failure.
7. Post-install hardening and configuration
Log in as admin and go through /admin:
- Set
force_httpsso all traffic is redirected. Under Settings, Security. - Enable 2FA for staff. Settings, Login,
enforce_second_factorset tostaff. - Turn off
login_requiredor turn it on depending on whether you want a public or gated forum. - Review
max_image_size_kbandmax_attachment_size_kbagainst your RamNode disk allocation. - Set up the setup wizard at
/wizardif you skipped it.
Email deliverability check
Discourse ships a diagnostic at /admin/email. Send a test message and check /admin/email/sent and /admin/logs/screened_emails. If mail is not moving:
cd /var/discourse
./launcher enter app
rails c
# in the console:
Rails.application.config.action_mailer.smtp_settings
exit
exitAlso check Sidekiq at /sidekiq. Stuck jobs in the retry queue almost always mean SMTP credentials are wrong or the provider is rejecting your sending domain.
MaxMind (optional)
For geolocation features, register for a free MaxMind GeoLite2 key and add it to app.yml:
env:
DISCOURSE_MAXMIND_LICENSE_KEY: "your_key_here"Then rebuild.
8. Backups
Discourse has a competent built-in backup system. Use it, and then get the backups off the VPS.
Configure in-app backups
At /admin/backups, set:
backup_frequencyto 1 (daily)maximum_backupsto something your disk can hold, typically 5 on a small planinclude_thumbnails_in_backupsoff if disk is tight
Backups write to /var/discourse/shared/standalone/backups/default/.
Ship them off-box
An on-server backup is not a backup. RamNode plans are single-node, so a host failure takes your forum and your backups together. Push to object storage or a second box nightly:
#!/bin/bash
# /usr/local/bin/discourse-backup-sync.sh
set -euo pipefail
SRC=/var/discourse/shared/standalone/backups/default
DEST=user@backup-host:/srv/backups/discourse/
rsync -az --delete "$SRC/" "$DEST"chmod +x /usr/local/bin/discourse-backup-sync.shAdd to root's crontab, offset from Discourse's own backup window:
30 4 * * * /usr/local/bin/discourse-backup-sync.sh >> /var/log/discourse-backup-sync.log 2>&1Alternatively, configure S3-compatible backup storage directly in /admin/backups and skip the rsync entirely. That is the cleaner option if you already have object storage.
Restore drill
Test a restore at least once before you need one:
- Enable
allow_restoreat/admin/backups. - Upload the backup file to
/var/discourse/shared/standalone/backups/default/on the target box. - It appears in the admin backups list. Click Restore.
- Watch the log tab. The instance logs you out when it completes.
9. Upgrades
Two paths, and you need to know when each applies.
Web UI upgrades (routine)
Visit https://forum.example.com/admin/upgrade and click Upgrade. This handles Discourse core and plugins without a container rebuild. Use this for the majority of updates.
Command-line rebuild (required for app.yml changes and Docker image updates)
cd /var/discourse
git pull
./launcher rebuild appDo this when:
- You edited
app.yml(added a plugin, changed SMTP, changed workers) - The web upgrade tells you a rebuild is required
- You are moving between major base image versions
The rebuild takes the site down for several minutes. On a 2 GB plan it can take considerably longer, and this is where the swap you configured earns its keep.
Adding plugins
Plugins go in the after_code hook in app.yml:
hooks:
after_code:
- exec:
cd: $home/plugins
cmd:
- git clone --depth 1 https://github.com/discourse/docker_manager.git
- git clone --depth 1 https://github.com/discourse/discourse-solved.git
- git clone --depth 1 https://github.com/discourse/discourse-calendar.gitKeep docker_manager in that list. It is what powers the web upgrade UI. Then:
./launcher rebuild appEvery plugin you add increases bootstrap time and memory footprint. On a 2 GB RamNode plan, be disciplined about which ones actually earn their place.
10. Operations reference
cd /var/discourse
./launcher start app # start
./launcher stop app # stop
./launcher restart app # restart without rebuilding
./launcher rebuild app # rebuild after app.yml changes
./launcher enter app # shell inside the container
./launcher logs app # container logs
./launcher destroy app # stop and remove the container (data in shared/ survives)
./launcher cleanup # prune old Docker images, reclaims real disk./launcher cleanup matters on RamNode. Old images accumulate across rebuilds and will quietly consume 10 GB or more. Run it after every few rebuilds.
Check resource usage:
docker stats --no-stream
free -h
df -h11. Troubleshooting
502 Bad Gateway that does not clear. Unicorn failed to start. ./launcher logs app and look for the actual exception. Usually OOM during boot on undersized plans.
Bootstrap killed partway through. Almost always memory. Confirm with dmesg | grep -i oom. Add swap, reduce UNICORN_WORKERS, retry.
Registration emails never arrive. Check /admin/email/skipped and /sidekiq/retries. Confirm your provider is not blocking the send. Remember that port 25 is not a path available to you on RamNode, so if you configured a plain sendmail somewhere it will never work.
Let's Encrypt renewal fails. Confirm port 80 is open and not proxied by Cloudflare. ./launcher rebuild app re-triggers issuance.
Disk full. Run ./launcher cleanup. Then check backup retention and /var/discourse/shared/standalone/uploads/.
Site is slow under normal load. Look at /sidekiq queue depth first, then docker stats. If Sidekiq is backed up and RAM is fine, you are CPU-bound and want more vCPU. If RAM is pinned, reduce workers or size up.
12. When Discourse is the wrong choice
Be honest about the tradeoff. Discourse gives you the best-in-class moderation tooling, trust levels, and community management features of any open-source forum. It also demands 4 GB of RAM to be comfortable, mandates a working transactional email provider, and its rebuild cycle is slow.
If you want a forum on a 1 GB RamNode plan, or you want something you can host next to five other PHP apps on one box, look at Flarum instead. If you want real-time and a JavaScript stack with a smaller footprint, look at NodeBB. Both have companion guides.
Quick reference
# Install
wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/main/install-discourse | sudo bash
# Config
/var/discourse/containers/app.yml
# Data
/var/discourse/shared/standalone/
# Rebuild after config change
cd /var/discourse && ./launcher rebuild app
# Reclaim disk
cd /var/discourse && ./launcher cleanup