Skip to content

How to Deploy Discourse on a VPS

Updated Oct 2026

verified on Ubuntu 26.04 · Oct 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Discourse — the open-source community forum — on a VPS with the official discourse_docker launcher, Let's Encrypt HTTPS, a first administrator created from the command line, a backup you can inspect and a rebuild-based upgrade path.

Before you start
  • A VPS with 2 GB+ RAM (the catalog's absolute floor is 1 GB, and only with swap enabled)
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain or subdomain pointed at the server (Discourse will not work on a bare IP address)
  • An outgoing email (SMTP) account, because Discourse sends its sign-up and password-reset mail itself
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Discourse is

Discourse is a modern community forum: threaded topics, categories and tags, chat, polls, reactions, solved answers, notifications by email, and a plugin system. It is written in Ruby on Rails with an Ember.js front end, on PostgreSQL and Redis, is licensed under the GPL-2.0-or-later, and is rated 2 / 5 to deploy. It is the usual self-hosted answer when you want a real forum rather than a chat room or a comment box.

The one thing to know about the install: the only officially supported way to run Discourse is Docker through the discourse_docker launcher. It builds a single container that holds the web app, background workers, PostgreSQL and Redis, and it comes with its own nginx and Let's Encrypt support. That is a different shape from the other guides in this series. There is no separate reverse proxy; the Discourse container owns ports 80 and 443, which is also why this guide does not use Caddy.

The upstream one-line installer is an interactive wizard that asks for the hostname, an admin email and SMTP details, and then does the steps below. This guide writes those steps out by hand so each one is visible and repeatable. For help choosing a provider and tier, see Best VPS for self-hosting.

Before you start: three hard requirements

Discourse will not install without these, and each one is a common reason for a failed first build:

  1. A real hostname. Discourse refuses to run on a bare IP, and the build aborts with a message if the hostname is still the sample value.
  2. A working mail setup. The build also aborts if SMTP is left at the sample value. Discourse's sign-up flow is built on email: new members confirm their address by mail, and so does the first administrator who registers through the browser. Without a mail provider, this guide shows the command-line path for the first admin, but a forum that cannot send mail has limited use.
  3. Enough memory. The first build is the hungry part; see the next section.

Server sizing

Plan on 2 GB of RAM or more. The catalog's floor is 1 GB, and that works only with swap enabled; the installer script offers to create swap on small servers, and you can do it yourself with fallocate, mkswap and swapon if you take the manual route. In our verification run, on a server with 8 GB, the single Discourse container used between about 1.0 and 1.3 GB of RAM once it had booted, created its first admin and taken one backup, which already includes PostgreSQL, Redis, three web workers and a background worker. Every extra Unicorn worker, plugin and busy hour adds to that.

Disk matters more than people expect. The images are large: on our test box the discourse/base image was about 5 GB on disk and the application image built from it about 5.9 GB (they share layers, so the total is smaller than the sum). Plan at least 20 GB free before you start, and more once members begin uploading images, since uploads live under /var/discourse/shared and grow with the forum.

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Prepare the server

Update the system and install the few tools this guide uses (Git is needed to fetch the launcher):

sudo apt-get update && sudo apt-get -y upgrade
sudo apt-get install -y git curl openssl ufw dnsutils

If you're logged in as root, create a non-root sudo user and continue as that user:

adduser deploy
usermod -aG sudo deploy

Then open only SSH and the two ports the Discourse container serves:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable

Be aware that Docker publishes the container's ports around ufw, so the firewall does not hide ports 80 and 443 here. That is fine, since they are the public site. PostgreSQL and Redis inside the container are not exposed at all.

Install Docker

If Docker isn't on the server yet, the convenience script installs Docker Engine and the Compose plugin (the Docker and Compose on Ubuntu guide covers the details). Discourse uses plain Docker rather than Compose:

command -v docker >/dev/null || curl -fsSL https://get.docker.com | sudo sh
sudo docker --version

Point the domain at the server

Let's Encrypt validates your hostname when the container starts, so DNS must already be right. Create an A record for your forum's hostname (say forum.example.com) pointing at the server's public IP and confirm it resolves:

dig +short forum.example.com

Let's Encrypt limits how many certificates it will issue for the same hostname in a week, so get the hostname right before you start rather than rebuilding repeatedly on a wrong one.

Get the launcher and configure the container

The launcher lives in /var/discourse. Clone it, copy the sample single-container configuration and edit three things: the hostname, the developer email (accounts with an address listed there become administrators when they register) and the two lines that turn on HTTPS with Let's Encrypt. Set your own values first; the secrets for the first admin are generated into a small file only you can read:

DOMAIN=forum.example.com
ADMIN_EMAIL=admin@example.com
[ -f ~/discourse.env ] || ( umask 077
  {
    echo "DOMAIN=$DOMAIN"
    echo "ADMIN_EMAIL=$ADMIN_EMAIL"
    echo "ADMIN_PASSWORD=$(openssl rand -hex 12)"
  } > ~/discourse.env )
. ~/discourse.env
[ -d /var/discourse/.git ] || sudo git clone --depth 1 https://github.com/discourse/discourse_docker.git /var/discourse
cd /var/discourse
[ -f containers/app.yml ] || sudo cp samples/standalone.yml containers/app.yml
sudo sed -i \
  -e "s|^  DISCOURSE_HOSTNAME:.*|  DISCOURSE_HOSTNAME: \"$DOMAIN\"|" \
  -e "s|^  DISCOURSE_DEVELOPER_EMAILS:.*|  DISCOURSE_DEVELOPER_EMAILS: \"$ADMIN_EMAIL\"|" \
  -e 's|^  #- "templates/web.ssl.template.yml"|  - "templates/web.ssl.template.yml"|' \
  -e 's|^  #- "templates/web.letsencrypt.ssl.template.yml"|  - "templates/web.letsencrypt.ssl.template.yml"|' \
  containers/app.yml
sudo chmod 600 containers/app.yml
sudo grep -n -E 'HOSTNAME|DEVELOPER_EMAILS|ssl.template' containers/app.yml

containers/app.yml is the file you will edit for every later change, and it will hold your SMTP password, which is why it is restricted to root. Now set your mail provider. Open the file and replace the sample SMTP lines with the values from your provider (Mailgun, Postmark, SES and similar services all work; the port is usually 587):

  DISCOURSE_SMTP_ADDRESS: smtp.mailprovider.example
  DISCOURSE_SMTP_PORT: 587
  DISCOURSE_SMTP_USER_NAME: your-smtp-username
  DISCOURSE_SMTP_PASSWORD: "your-smtp-password"

Quote the password, because a # or $ in an unquoted YAML value is misread. Sending mail through a real provider was not tested in this guide's verification run, which has no mail account.

If you want to bring the forum up first and connect mail afterwards, tell Discourse to skip its mail check. The sample file has the setting commented out; this block uncomments it. Without it the build refuses to continue while the SMTP address is still the sample value. Comment the line out again and rebuild once you have real credentials:

cd /var/discourse
sudo sed -i 's|^  #DISCOURSE_SKIP_EMAIL_SETUP:|  DISCOURSE_SKIP_EMAIL_SETUP:|' containers/app.yml
sudo grep -n SKIP_EMAIL_SETUP containers/app.yml
sudo grep -q '^  DISCOURSE_SKIP_EMAIL_SETUP:' containers/app.yml

Build and start

bootstrap builds the application image: it pulls the base image, fetches the Discourse code, runs the database setup and compiles the assets. This is the slow, memory-hungry step; on our 2-vCPU test box it took about five to six minutes once the base image was downloaded (the first download adds a few minutes). The output is long, so it goes to a log file, and the block fails if the build does:

cd /var/discourse
sudo ./launcher bootstrap app > ~/discourse-bootstrap.log 2>&1
rc=$?
tail -n 3 ~/discourse-bootstrap.log
[ "$rc" -eq 0 ]

The last line of the log should read Successfully bootstrapped. If it says FAILED, read the lines above it: the cause is almost always one of the checks described under Troubleshooting. Then start the container and wait for the site. On the first start Discourse requests the certificate, so the loop waits for both a 200 answer and a verified certificate, and it is bounded:

. ~/discourse.env
cd /var/discourse
sudo ./launcher start app > ~/discourse-start.log 2>&1
out=000
for i in $(seq 1 90); do
  out=$(curl -s -o /dev/null -w '%{http_code} %{ssl_verify_result}' "https://$DOMAIN/srv/status")
  [ "$out" = "200 0" ] && break
  sleep 5
done
echo "HTTPS status and certificate check: $out"
curl -s "https://$DOMAIN/srv/status"; echo
sudo docker ps --format '{{.Names}}  {{.Status}}'
curl -s -o /dev/null -w 'plain HTTP: %{http_code} -> %{redirect_url}\n' "http://$DOMAIN/"

200 0 means the site answered over a certificate that verified (the second number is curl's certificate verdict, 0 for valid), and /srv/status should print ok. Expect a few 502 and 503 answers in the first minute while the workers boot; that is normal. The last line should show plain HTTP being redirected to HTTPS.

Create the first administrator

Discourse's browser sign-up emails a confirmation link, so on a forum with no mail yet you need another way in. The documented route is a built-in rake task that creates an account, marks it active and grants admin rights without any email. The task asks four questions (email, password twice, make admin), so this answers them on standard input, which keeps the password off the process list. It runs as the discourse user inside the container; run as root, the task cannot reach the database:

. ~/discourse.env
sudo docker exec -i app su discourse -c "cd /var/www/discourse && RAILS_ENV=production bundle exec rake admin:create" > ~/discourse-admin.log 2>&1 <<EOF
$ADMIN_EMAIL
$ADMIN_PASSWORD
$ADMIN_PASSWORD
Y
EOF
rc=$?
tail -n 4 ~/discourse-admin.log
[ "$rc" -eq 0 ] && grep -q 'Admin privileges' ~/discourse-admin.log

Discourse picks a username for the account (ours was user1); change it in your profile after you sign in. Then confirm the database agrees that the account is an administrator:

. ~/discourse.env
sudo docker exec app su discourse -c "cd /var/www/discourse && RAILS_ENV=production bundle exec rails runner 'puts User.find_by_email(%q($ADMIN_EMAIL)).admin?'" 2>&1 | tail -n 1

It should print true. Print the generated password with grep ADMIN_PASSWORD ~/discourse.env, sign in at https://forum.example.com, and move it into your password manager. Signing in through the browser, the setup wizard and every email Discourse sends were not part of this guide's automated verification.

Then work through the hardening basics in the admin area:

  1. Run through the setup wizard (site title, contact email, defaults).
  2. Under Settings, decide who may sign up: invite-only, or require administrator approval for new accounts, until you have a moderation team.
  3. Once mail is configured, use Admin → Email to send a test message, and comment out DISCOURSE_SKIP_EMAIL_SETUP in app.yml, then rebuild.
  4. Turn on two-factor authentication for your own account.

Backups

Discourse has its own backup tool, and it covers the two things that matter: the database and the uploads. Run it inside the container:

sudo docker exec app discourse backup 2>&1 | tail -n 4
sudo ls -lh /var/discourse/shared/standalone/backups/default/

The archive lands in /var/discourse/shared/standalone/backups/default/ on the host. It contains your members' data, including email addresses, and the tool creates it readable by other users on the server, so restrict the directory, then copy it off the server to encrypted storage:

sudo chmod -R o-rwx /var/discourse/shared/standalone/backups
sudo ls -l /var/discourse/shared/standalone/backups/default/

Discourse can also take scheduled backups itself and send them to S3-compatible storage; look under Admin → Backups and the backup settings in the admin area. A backup you have never checked is a guess, so check this one. The block below lists the newest archive, confirms it holds a database dump, tests that the dump decompresses cleanly and counts the table definitions in it:

f=$(sudo sh -c 'ls -1t /var/discourse/shared/standalone/backups/default/*.tar.gz | head -n 1')
echo "$f"
sudo tar tzf "$f" | grep -c '^dump.sql.gz$'
sudo tar xzOf "$f" dump.sql.gz | gzip -t && echo "dump decompresses cleanly"
sudo tar xzOf "$f" dump.sql.gz | gunzip | grep -c '^CREATE TABLE public.users '

That proves the archive is complete and the dump is intact. It does not prove a full restore: restoring onto a fresh server (install Docker and the launcher, copy the archive into the backups directory, run discourse enable_restore and then discourse restore <archive> inside the container, or restore from the admin area) was not tested in this guide's verification run. Rehearse it on a scratch server before you rely on it.

Upgrades

Discourse updates quickly and expects you to keep up. There are two routes. For routine updates, the Admin → Upgrade page (provided by the docker_manager plugin in the sample configuration) updates the application in place with a progress log. For changes to app.yml, for new base images and for the launcher itself, you rebuild. Take a backup first, then update the launcher repository and rebuild the container:

cd /var/discourse
sudo git pull
sudo ./launcher rebuild app > ~/discourse-rebuild.log 2>&1
rc=$?
tail -n 3 ~/discourse-rebuild.log
[ "$rc" -eq 0 ]

A rebuild stops the forum for the duration of the build, about five minutes on our test box, so announce it. Your data lives in /var/discourse/shared, outside the container, which is why a rebuild keeps it. Once it finishes, check the site and the administrator are still there:

. ~/discourse.env
out=000
for i in $(seq 1 90); do
  out=$(curl -s -o /dev/null -w '%{http_code} %{ssl_verify_result}' "https://$DOMAIN/srv/status")
  [ "$out" = "200 0" ] && break
  sleep 5
done
echo "after rebuild: $out"
sudo docker exec app su discourse -c "cd /var/www/discourse && RAILS_ENV=production bundle exec rails runner 'puts User.find_by_email(%q($ADMIN_EMAIL)).admin?'" 2>&1 | tail -n 1

The application version follows the Discourse release channel set in app.yml (the sample leaves the default); read the release notes in the Discourse project's announcements before major updates, and keep any plugins you add in the hooks section of app.yml current, since a plugin that does not support the new version can fail the build.

Troubleshooting

The build stops with Aborting! Mail is not configured!. The SMTP address in app.yml is still smtp.example.com. Set real credentials, or uncomment DISCOURSE_SKIP_EMAIL_SETUP as shown above to continue without mail.

The build stops with Aborting! Domain is not configured!. The hostname is still discourse.example.com. Fix DISCOURSE_HOSTNAME, then run the bootstrap again.

The build is killed or hangs. Run free -m and dmesg | grep -i oom. An out-of-memory kill means the server needs swap or a bigger tier; Discourse's compile step is the peak. Add swap, then run sudo ./launcher rebuild app.

The site shows 502 or 503 for more than a few minutes after start. Read the logs with sudo ./launcher logs app. A database or Redis error there usually follows a full disk (df -h) or an interrupted rebuild. ./discourse-doctor in /var/discourse collects the common checks.

No certificate, or the browser shows a warning. Confirm dig +short forum.example.com returns this server's IP and that your provider's network-level firewall allows inbound 80 and 443. Let's Encrypt must reach port 80. The log is in /var/discourse/shared/standalone/letsencrypt. Remember the weekly per-hostname issuance limit before you retry in a loop.

Mail does not arrive. SMTP is skipped, wrong or blocked by the provider. Check the credentials in app.yml, send a test from Admin → Email, and look at the mail logs there. Many cloud providers block outbound port 25, which is why a mail provider on 587 is the usual answer.

Verification + next steps

You're done when https://forum.example.com/srv/status returns ok over a valid certificate, you can sign in as the administrator, a backup archive exists and checks out, the container is set to restart on boot (the launcher starts it with --restart=always), and you have completed one rebuild without losing data.

From there, finish the setup wizard, create categories, connect mail, invite your first members and decide your moderation rules. See the Discourse entry for catalog details and Best VPS for self-hosting for the ranked server picks.

Next steps

How to self-host Discourse →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AFFiNE on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy copyparty on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Flarum on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghost on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy a GitHub Actions Self-Hosted Runner on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Infisical on a VPS →How to Deploy Invoice Ninja on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Kanboard on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy a Minecraft Server on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Odoo on a VPS →How to Deploy Ollama on a VPS →How to Deploy ONLYOFFICE Docs on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Plex Media Server on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy WordPress on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.