# MKJ's Opinionated Discourse Deployment Configuration

**URL:** https://meta.discourse.org/t/mkjs-opinionated-discourse-deployment-configuration/193355
**Category:** Sysadmins
**Tags:** explanation, install
**Created:** [June 9, 2021, 11:34pm UTC](https://meta.discourse.org/t/mkjs-opinionated-discourse-deployment-configuration/193355 "2021-06-09T23:34:00Z")
**Posts on this page:** 1
**Showing post:** 1

<div class="post-metadata">

### Author: ![mcdanlj](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/mcdanlj/32/131829_2.png) [@mcdanlj](https://meta.discourse.org/u/mcdanlj)
#### Post date: [June 9, 2021, 11:34pm UTC](https://meta.discourse.org/t/mkjs-opinionated-discourse-deployment-configuration/193355/1 "2021-06-09T23:34:01Z")

</div>

I have been running a Discourse forum with a substantial amount of content and plenty of images over the past few years. [Maker Forums](https://forum.makerforums.info/) has over 100GB of images and over 400,000 posts, of which a substantial amount was imported, primarily from Google+, and the rest was created on the site. This post describes elements of how I eventually configured Maker Forums and, later, a few other Discourse instances. This is what I wish I knew when I got started, and which I’ve used to help others avoid some of the same pitfalls for their own Discourse instances.

Time for a wider audience.

> ⚠ **Warning:** If you are not comfortable working as a Linux systems administrator, this guide is probably not for you. I may not even be aware of all the ways that it presumes knowledge about Linux. If this feels enlightening to read, you may be the target audience. If it feels confusing to read, you are probably not the target audience. If this feels like work, please consider paying CDCK or @pfaffman to run Discourse for you; they know what they are doing. Or start with a [free discourse.group site](https://www.discourse.org/free), and then pay for what you grow into. ⚠
> 
> ⚠ As if that weren’t enough: I have more Linux expertise than Discourse expertise. My opinions come with no warranty. If trying to follow my advice causes anything of yours to break (your Discourse forum, your host system, or your heart) you get to keep both pieces, with all the sharp edges. I have no plans to provide any form of support for the content in this post. ⚠

I plan (but do not promise) to keep this document up to date with my practices covering the Discourse instances that I participate in maintaining. This is written in the form of advice, but I intend it primarily as advice to myself and to any administrators who inherit Discourse deployments that I have been responsible for. Otherwise, you should consider it as one jumping-off point for _your own research_ to determine how you would like to deploy Discourse.

# System Setup

Use a CentOS-derived or Ubuntu LTS OS. Anything that supports Docker can probably be made to work, but I’ve used those two.

## Docker

I’m a Fedora user. I was the first Fedora Project Lead at Red Hat, and I’d much rather run Discourse on top of Podman because I opine that its security model is preferable to Docker’s. However, Discourse deployments are supported only on Docker, and you will be [quite a pioneer if you try to run on top of anything else](https://meta.discourse.org/t/any-interest-in-podman/118182). (It may, someday, work with Podman using podman-compose if docker-compose is ever supported.)

Now that Docker supports cgroups v2, you can install the official Docker builds on a CentOS-derived system:

```plaintext
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf install --allowerasing docker-ce docker-ce-cli

```

(Include `--alloweraseing` because of a conflict with `podman`, `runc`, and `buildah` that may already be installed; they need to be erased to install docker.)

```plaintext
systemctl enable --now docker

```

I have tested this with AlmaLinux 9.

## Security

This section really has nothing to do with Discourse _per se,_ but it’s part of my normal security practice. **Don’t allow password-only shell access to any system on the network, including a VM running Discourse.** Set up SSH-based access using a passphrase-encrypted SSH key, and configure the ssh server on your VM not to allow password access.

```plaintext
laptop$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/.../.ssh/id_rsa):
Enter passphrase (empty for no passphrase): SOME LONG PHRASE
Enter same passphrase again: SOME LONG PHRASE
Your identification has been saved in .ssh/id_rsa
Your public key has been saved in .ssh/id_rsa.pub

```

Linux distributions are normally set up to remember the passphrase in memory, so you only have to type it once per boot. Windows is not as convenient; you might consider [using Pageant with PuTTY](https://www.digitalocean.com/community/tutorials/how-to-use-pageant-to-streamline-ssh-key-authentication-with-putty) to do the same.

First, validate that incoming SSH works without a password. Only after doing that, on the server, modify the file /etc/ssh/sshd\_config and find the `PasswordAuthentication` line. Set it to `no` to disable incoming password access.

```plaintext
PasswordAuthentication no

```

### Firewall

You will need to leave ports 80 and 443 generally open for letsencrypt to generate and renew your SSL certificates, even before your Discourse is open to the public.

If you are using firewalld, these commands will accomplish this:

```plaintext
firewall-cmd --add-service http --add-service https --zone public
firewall-cmd --runtime-to-permanent

```

## Separate device and file system

Make /var/discourse/shared a separate **device** with its own file system, with 20GB of space plus at least twice as much room as you need for images; add more space if you will be using prometheus. If the device will be easy to expand later (such as LVM or any cloud block storage like AWS elastic block storage) you can monitor and grow it as you need to; otherwise be generous at the start. If you are using a network storage block device, do not put a partition table on it. Using it without a partition table will make it easier to expand; you will not have to modify a partition table. In many cases you will be able to expand without any system downtime.

On Maker Forums, this is a network storage block device attached to the VM on which Maker Forums is running. On another Discourse forum, it is a Digital Ocean Block Storage Volume. In Amazon, this would be AWS Elastic Block Storage. On my test system running a KVM VM under libvirt on Fedora, it is an LVM volume on the Fedora host exported to the AlmaLinux VM as a virtual disk. In each case, I could create a new VM, copy key files across to it, stop the old VM, attach the /var/discourse/shared volume to the new VM, and be back up and running in minutes. This makes operating system upgrades on the VM relatively low risk.

Make sure that you start out with at least 25GB on the root filesystem for your VM, not including any space for /var/discourse/shared. This will be used for all the docker containers, and the discourse launcher will fail if less than 5GB is free at any time. You want plenty of space available for system updates, too. If you don’t have enough disk space, this is hard to recover from.

In site configuration, do set `force_https` but heed the warnings. Set it up in test, before taking a Discourse site public. Note that even with `force_https` you need port 80 open, both to redirect to SSL on port 443 and to renew your letsencrypt SSL certificate. (However, if you are using cloudflare, use its feature instead; it is reported to be not compatible with `force_https` in Discourse.)

## Kernel configuration

Redis (one of the key components on which Discourse is built) [strongly recommends disabling transparent huge pages when using on-disk persistence](https://redis.io/docs/management/optimization/latency/#latency-induced-by-transparent-huge-pages) (which Discourse does), and I also allow memory overcommit.

```plaintext
echo 'w /sys/kernel/mm/transparent_hugepage/enabled - - - - never
w /sys/kernel/mm/transparent_hugepage/defrag - - - - never' > /etc/tmpfiles.d/thp.conf
systemd-tmpfiles --create
echo 'vm.overcommit_memory=1' > /etc/sysctl.d/90-vm_overcommit_memory.conf
sysctl --system

```

# Discourse Installation

While the [default installation](https://github.com/discourse/discourse/blob/main/docs/INSTALL-cloud.md) is a single container, this makes every upgrade, recommended monthly, typically a 10-15 minute downtime if done from the command line, which is necessary for some updates, including those updating the tools on top of which Discourse is built, for security or new features, or when the live update from the UI fails for any reason. You can reduce that downtime in practice with the two-container installation.

## Two-container installation

Start the configuration with two containers.

```plaintext
./discourse-setup --two-container --skip-rebuild
${EDITOR:-nano} containers/data.yml
./launcher rebuild data
# my preference to use app.yml but you might stick with web_only.yml, read text
mv containers/web_only.yml containers/app.yml
${EDITOR:-nano} containers/app.yml
./launcher rebuild app

```

This makes required system downtime every few months quite short, rarely noticeable; many users won’t notice at all if they don’t click or scroll past content during the outage. This makes it easier to apply most security updates; it’s just a blip rather than approximately 15 minutes of rebuilding everything. The following process works for most updates and typically gives approximately 30-90 seconds of downtime, depending primarily on the performance of the host system and the set of plugins installed.

```plaintext
cd /var/discourse
git pull
./launcher bootstrap app
./launcher destroy app && ./launcher start app
./launcher cleanup

```

Do not delay between the bootstrap and the destroy/start invocations. Infrequently (maybe once or twice a year in practice), the database migrations done near the end of the bootstrap phase will cause more or less serious errors to present to users of the app, due to the older code accessing the updated database.

This **does** mean that when you update Discourse, you have to also check whether to update the data container as well, but this is rarely required (typically expect once or twice per year). Depending on the contents of your data and app containers and the speed of the system, this will typically result in a downtime between 5 and 20 minutes.

```plaintext
cd /var/discourse
git pull
./launcher stop app
./launcher rebuild data
./launcher rebuild app

```

For more on knowing when to update the data container, see:

> [@Migrate quickly to separate web and data containers](https://meta.discourse.org/t/how-to-migrate-quickly-to-separate-web-and-data-containers/153355#what-to-do-on-future-updates-4?u=mcdanlj):
>
> warning Warning: If you are not comfortable working as a Linux systems administrator, and do not have experience with docker containers, moving to a multi-container deployment will cause you difficulty, and both staff and volunteer help here will appropriately ask you to return to a standalone single-container deployment fully managed by the launcher script. If you move to a multi-container deployment and your system breaks as a result, you are likely to experience the opportunity to keep b…

(In my own deployments, I personally chose to call the `web_only` container `app` both because it’s easier to type and because it makes most instructions easier to follow. This is non-standard but I keep appreciating the ease of use. However, it was extra work, and it works for me because I know what is going on. If that sounds bad to you, stick with the default `web_only` instead for a multi-container deployment.)

Note that at some point in the future, Docker may force you to do a migration to a new configuration for connecting your containers:

> [@Move from standalone container to separate web and data containers](https://meta.discourse.org/t/how-to-move-from-standalone-container-to-separate-web-and-data-containers/29413/95):
>
> That sounds like a good idea. Unless someone beats me to it, I’ll see about subunits /creating a PR to switch to networks and /or sockets (which some prefer anyway) and creating a howto to convert an existing setup to the new configuration.

If you have 4GB or more of memory, or multiple CPUs, read advice at:

> [@Configure Discourse Docker on servers with more RAM and CPU](https://meta.discourse.org/t/how-to-configure-discourse-docker-on-higher-end-servers/18569):
>
> When you install Discourse on an instance with 4GB or more you should consider the following: Monitor your setup If you elect to use a higher end setup we strongly recommend you set up monitoring using a service such as [Prometheus](https://prometheus.io/), [Datadog](https://www.datadoghq.com/), or [New Relic](https://newrelic.com/). You will need to analyze the results of configuration changes to reach an optimal setup. Out of the box Discourse Docker ships with 3 web workers Web workers are served via [Pitchfork](https://github.com/Shopify/pitchfork) (a modern fork of Unicorn), each worker process is capable…

## Update Schedule

Watch the #release-notes tag (Click on #release-notes and click on the bell at the upper right; I use “Watching First Post”) and/or add `https://meta.discourse.org/tag/release-notes.rss` to your RSS feed to know when there are releases. Read the release notes before updating. If there is a database change, the release notes will mention it. They will also call out releases that contain security updates. Read all release notes even if you skip actually updating to some version; if you don’t read the release notes for a release that updates the database, you might miss database update instructions in the release notes you didn’t read.

## Mail

Mail is still one of the key ways to keep connecting people. Set up outgoing and incoming mail to make mail work for you. If you have trouble see:

> [@Troubleshoot email on a new Discourse install](https://meta.discourse.org/t/troubleshooting-email-on-a-new-discourse-install/16326):
>
> You just installed Discourse via [the install guide](https://github.com/discourse/discourse/blob/main/docs/INSTALL.md), but email doesn’t seem to work. Unfortunately this means you can’t log in as an admin to finalize the install. cry Let’s troubleshootize! Try the doctor woman_health_workerIf you run ./discourse-doctor it will check several ways that your mail configuration might be broken, and offer advice. Try that first. Did you enter email settings correctly? The simplest way is to run ./discourse-setup again. Did you enter everything correctly? B…

### Keep reaching out

Maker Forums has seen occasional visitors who are gone for long stretches before returning. By default, Discourse stops sending digest emails after a year. Consider setting the `suppress_digest_email_after_days` to something longer than the default 365 days if you want to encourage occasional visitors to come back when they see something new and interesting. I made it substantially longer for Maker Forums to support occasional visitors keeping up to date. Reading digest emails is a valid way to “lurk” on a forum, and you never know when something is going to spark someone’s interest in contributing.

Similarly, by default, unprivileged users who haven’t interacted much (trust level 0 with no posts) are eventually deleted after 730 days of not logging in. Set “clean up inactive users after days” to 0 to disable deleting users, if you want them to be able to lurk reading digest emails indefinitely.

Consider adding the [yearly review](https://github.com/discourse/discourse-yearly-review.git) plugin which once per year, will generate a post like [2020: The Year in Review](https://meta.discourse.org/t/2020-the-year-in-review/174719) **and ultimately email it to your inactive users** which may encourage them to renew participation.

> [@Discourse Yearly Review](https://meta.discourse.org/t/discourse-yearly-review-plugin/105713):
>
> discourse2Summary Discourse Yearly Review creates a topic on January 1st that summarizes the previous year’s forum activity. (See our examples here on Meta - #year-in-review)hammer_and_wrenchRepository Link [https://github.com/discourse/discourse-yearly-review](https://github.com/discourse/discourse-yearly-review)open_bookInstall Guide [How to install plugins in Discourse](https://meta.discourse.org/t/install-plugins-in-discourse/19157)Initial Setup Head to your /admin/plugins page to click the discourse-yearly-review gear Settings button: [Yearly Review settings] Enter categor…

### Mail receiver container

Set up a third container as a mail receiver. It ensures bounce processing, makes your bounce processing independent of outgoing mail provider, and gives you the option of reply-by-email.

> [@Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver](https://meta.discourse.org/t/direct-delivery-incoming-email-for-self-hosted-sites/49487):
>
> Discourse is all about enabling civilized discussion. While plenty of people like a web interface, e-mail is still the “hub” of many people’s online lives. That’s why sending e-mail is so important, and when you’re sending e-mail, you really want to be able to receive it, too. There are several reasons why: If e-mails “bounce” (they can’t be delivered for some reason), you need to know about that. Repeatedly sending e-mails that bounce will get your e-mails flagged as spam. Receiving e-ma…

Make sure you have [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) set up for trusting your email sender; minimally, a policy like `v=spf1 +mx -all` if you send and receive through the same MX, but more specific may be better trusted as spam protection. Consider [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) as well.

If you use the same host name to receive email, you should really terminate SSL outside your container as for an “offline page” (see below) and will need to [map your certbot certificates into the container](https://meta.discourse.org/t/direct-delivery-incoming-email-for-self-hosted-sites/49487#integrating-with-external-nginx-6) and restart the container after running certbot.

## Terminate user SSL connections outside the container

There are two choices for terminating SSL outside the container, either of which brings substantial advantages over terminating inside the container. Set up one of them after you have successfully completed `discourse-setup` and bootstrapped your forum.

### External nginx

Use nginx running on the host system, rather than only in a container, both to host a maintenance page, and to support IPv6 address logging if your host has IPv6 support. (Otherwise, all IPv6 connections will be logged as coming from an internal RFC1918 address associated with your local docker virtual network interface.) This configuration will present a temporary maintenance page during most maintenance operations that will eventually redirect back to the page a user was looking at.

> [@Add an offline page to display when Discourse is rebuilding or starting up](https://meta.discourse.org/t/adding-an-offline-page-when-rebuilding/45238):
>
> warning This guide is intended for advanced users, who are already using nginx outside the docker container. By following this guide you make your setup more complicated and will lose some speed benefits like HTTP2 if you’re not running Ubuntu 16.04 or later. Proceed with caution! When Discourse is rebuilding or starting up, your users will usually either see an error message from their browser… …or a not-so-nice 502 error message from Nginx: If you’re a perfectionist …

Note that the instructions on that page (currently) suggest installing a package called `letsencrypt` but it is now normally called `certbot` instead. If you follow the instructions on that page to use `--certonly`, you will not need the nginx plugin for certbot, but installing the nginx plugin is another mechanism. On CentOS derivatives that’s:

```plaintext
dnf config-manager --set-enabled crb
dnf install epel-release
dnf install certbot python3-certbot-nginx
systemctl enable --now certbot-renew.timer

```

Make sure that certbot restarts nginx and the mail receiver container so that you do not end up with browsers or email blocking traffic with your site due to continuing to use an old, expired certificate.

```plaintext
# systemctl edit certbot-renew

```

For a system without a mail receiver, I added the two lines:

```plaintext
[Service]
ExecStartPost=/bin/systemctl reload nginx

```

On a system where I’m using a separate mail-receiver container that also shares the cert from the system:

```plaintext
[Service]
ExecStartPost=/bin/systemctl reload nginx
ExecStartPost=/bin/sh -c 'cd /var/discourse && ./launcher restart mail-receiver'

```

If you are using SELinux, the Ubuntu containers aren’t set up to label the `nginx.http.sock` file with `httpd_sys_content_t` for external nginx to be able to access it. You have two choices.

The first is to run nginx in permissive mode, removing SELinux protection for it: `semanage permissive -a httpd_t`

However, that removes SELinux protection from what is probably the most relevant service! To keep SELinux enabled, you will need to allow nginx to access the error pages and switch from proxying over a unix domain socket to a port (which is a few µs slower, but should not be noticeable to your users).

First, run these commands to allow nginx to access error pages:

```plaintext
semanage fcontext -a -t httpd_sys_content_t /var/www
restorecon -R -v /var/www

```

Then in your app.yaml, comment out or remove the `- "templates/web.socketed.template.yml"`, expose port 80 as a different port on the local machine and rebuild the container.

```plaintext
expose:
  - "8008:80" # http

```

Don’t use `https` here — you have terminated SSL in the external nginx, and the `X-Forwarded-Proto` header tells Discourse that the request came in via https. Make sure that port 8008 (or whatever other port you have chosen) is not exposed publicly by your firewall settings.

Then run this command to allow nginx to connect over the network to the containre:

```plaintext
setsebool -P httpd_can_network_connect 1

```

Then modify your external nginx configuration from proxying via `nginx.http.sock` to `http://127.0.0.1:8008` (or your chosen port) and clear the default `Connection: close` header, so that the external nginx doesn’t have to establish a new IP connection for every request.

```plaintext
...
  location / {
    proxy_pass http://127.0.0.1:8008;
    proxy_set_header Host $http_host;
    proxy_http_version 1.1;
    # Disable default "Connection: close"
    proxy_set_header "Connection" "";
...

```

Removing `web.socketed.template.yml` also removed the `real_ip` invocation, so add that back. Make sure that the IP address range you use makes sense; Docker’s default is to use the 172.16\* RFC1918 address space that are not routed on the public internet by policy. Add to your app.yml file something like this in the run section, selecting one or more of the RFC1918 address spaces or whatever else is appropriate for your deployment:

```yaml
run:
  - file:
     path: /etc/nginx/conf.d/outlets/server/real-ip-recursive.conf
     chmod: 644
     contents: |
       real_ip_recursive on;
  - file:
     path: /etc/nginx/conf.d/outlets/server/real-ip-header.conf
     chmod: 644
     contents: |
       real_ip_header X-Forwarded-For;
  - file:
     path: /etc/nginx/conf.d/outlets/server/set-real-ip-from.conf
     chmod: 644
     contents: |
       set_real_ip_from 192.168.0.0/16;
       set_real_ip_from 172.16.0.0/12;
       set_real_ip_from 10.0.0.0/8;

```

This is required for rate limiting to work correctly, as well as attributing registration and last-use IP addresses for users.

For more information:

> [@Handling the "chain of trust" of the end user's real IP](https://meta.discourse.org/t/handling-the-chain-of-trust-of-the-end-users-real-ip/406372):
>
> Background Discourse needs to be aware of the end user’s real IP address. However, an end user never directly connects to Discourse since there is always one or more upstream web servers (nginx running in the Discourse container) in place. Thus, we need a way to pass along that information to Discourse in a trusted manner. The x-forwarded-for header is the solution. In this topic I will describe the specific mechanisms for properly handling that information and how we’re expecting it to be pro…

### External service

I have not configured Fastly or Cloudflare in front of Discourse, but others have, and unlike external nginx running on the host, they can allow you to serve a maintenance page while the host system is entirely down, such as when rebooting during a system update on your host. If this is worthwhile to you, here’s how to do it:

> [@Full site CDN acceleration for Discourse](https://meta.discourse.org/t/full-site-cdn-acceleration-for-discourse/21467):
>
> [Fastly](http://www.fastly.com/) , [CloudFlare](https://www.cloudflare.com) and a few other CDNs offer a mode where they accelerate dynamic content. In a nutshell you point your domain IP address at the CDN and the CDN will intelligently decide how to deal with the request. Static content can be easily served from cache Dynamic content can be routed to the site. This provides some advantages over only shipping static assets which is covered in [the CDN howto](https://meta.discourse.org/t/enable-a-cdn-for-your-discourse/14857). You can elect for “[shielding](http://www.fastly.com/products/origin-shield/)” that protects your site from traffic spikes. Dynamic cont…

## Don’t rush to S3 uploads

Be very sure you always want to [use S3 (or equivalent) for uploaded images](https://meta.discourse.org/t/setting-up-file-and-image-uploads-to-s3/7229) before you enable `enable_s3_uploads` during setup, or migrate to it later. Be aware that using S3 (`s3_endpoint`) with its associated CDN (`s3_cdn_url`) for images will also result in serving javascript via that CDN. Migrating from S3 back to local storage is not supported and there are no concrete plans to implement it at this time. It’s a “one way door” that can’t even be undone by a full backup and restore. If you do use S3 or similar, don’t use Digital Ocean Spaces instead of S3. There are references here on meta to it not being reliable.

I moved my site to serving images through Digital Ocean Spaces and its associated CDN early on, and I had to write hundreds of lines of custom code to migrate back to local storage, doing minor damage to my Discourse instance in the process, due to the “one way door” not being well understood.

For more information:

> [@Migrate\_from\_s3 problems](https://meta.discourse.org/t/migrate-from-s3-problems/119064):
>
> When migrating from S3 to local storage, we see a number of issues. The main issue is that the migrate\_from\_s3 rake task is not taking the Uploads table as a starting point, but the posts. This causes it to skip a lot of uploads which are being left on S3. uploads used as logo’s because they are not referenced in a post uploads for avatars because they are not referenced in a post uploads that are (for some reason) referenced by their CDN URL in raw because they do not match [the regex](https://github.com/discourse/discourse/blob/master/lib/tasks/uploads.rake#L130) that is…

You do not need to enable S3 uploads to use a CDN for your Discourse. Consider using an independent CDN (e.g. Cloudflare, CloudFront, Fastly, GCS CDN) in front of a Discourse that manages its own images. It is my second-hand understanding that the warning about Cloudflare not being recommended is due to [“Rocket Loader” modifying JavaScript](https://meta.discourse.org/search?q=cloudflare%20broken); and that at this time, as long as you don’t use “Rocket Loader” it functions correctly.

> [@Enable a CDN for your Discourse](https://meta.discourse.org/t/enable-a-cdn-for-your-discourse/14857):
>
> So you’d like to use a CDN to speed up worldwide access to common assets on your Discourse forum? You might want to go all-in and [Set up file and image uploads to S3](https://meta.discourse.org/t/setting-up-file-and-image-uploads-to-s3/7229) (see also [Configure an S3 compatible object storage provider for uploads](https://meta.discourse.org/t/configure-an-s3-compatible-object-storage-provider-for-uploads/148916)). If you want a CDN without the added complexity of S3 and having two CDNs, this is the guide for you. The CDN will reduce some load on your server by letting the CDN serve those assets so that the server doesn’t have to. It also can speed things up by deliver…

## Discourse settings for moderation

On any site where moderation is active, strongly consider the `enable_whispers` configuration that allows moderators and administrators to talk about a topic in line. Also, category moderators have been given more abilities in recent versions of Discourse. It is worth being aware of `enable_category_group_moderation` if you have experts in different topics with their own categories, or if you have functionally separate categories such as for support.

Geolocation can be helpful when trying to understand whether an account is legitimate.

> [@Configure MAXMIND for reverse IP lookups](https://meta.discourse.org/t/configuring-maxmind-for-reverse-ip-lookups/173941):
>
> What is MAXMIND and why would I want it? Discourse uses [MAXMIND](https://www.maxmind.com/en/home) to provide geographical information for reverse IP lookups. Without Maxmind’s database you’ll see something like this: Or perhaps you’ve noticed this message fly by when you rebuild your container: MaxMind IP database download requires an account ID and a license key Please set DISCOURSE\_MAXMIND\_ACCOUNT\_ID and DISCOURSE\_MAXMIND\_LICENSE\_KEY. See https://meta.discourse.org/t/configure-maxmind-for-reverse…

The [Discourse Templates](https://github.com/discourse/discourse-templates) feature is really helpful for moderators. It lets you collaborate on common responses. We have a few dozen at Maker Forums. It has more features than the prior “Canned Responses” plugin that it replaces.

The [User Notes](https://github.com/discourse/discourse-user-notes) feature will help moderators share notes about users. You can put these to use for things like:

- “Keep an eye on this user, they may be malicious because …”
- “While this behavior seems suspect, I have validated that this is a legitimate user by …”
- “I’m already having a conversation with this user to address concerns, other moderators don’t need to pile on.”

## Information Accessibility

The [Discourse Solved](https://github.com/discourse/discourse-solved.git) plugin not only marks solved problems so that site visitors can identify them more easily, but I understand might also prioritize google search results.

Public information is more accessible than private information. On Maker Forums, our [FAQ strongly discourages personal messages and reminds everyone that personal messages are not truly private](https://forum.makerforums.info/faq#publicforum). However, by default, users may see the message:

> You’ve replied to _user_ 3 times, did you know you could send them a personal message instead?

If you really want to encourage users to go to personal messages, I suggest that you go to Admin → Customize → Text and change the `get_a_room` template to fix the comma splice.

If, like Maker Forums, you want to keep conversation in public to benefit everyone, Admin → Settings → Other → `get_a_room_threshold` can be set higher, like 1000000.

Similarly, if you have a forum providing help, the `max_replies_in_first_day` default 10 might push new users in a conversation asking for help into personal messages when they use up their budget of replies. Consider increasing this setting to avoid pushing conversations into personal messages.

## Connect users, build a community

A few plugins can help connect users to each other.

If your forum doesn’t have too many simultaneous users, consider the Who’s Online plugin to give people more of a sense of connection. You might want to limit display to logged-in users, possibly only those who have reached at least trust level 1. You can use it only to add presence flair (`whos_online_avatar_indicator`) to avatars by setting `whose_online_minimum_display` very high and `whos_online_hide_below_minimum_display` true. This can be useful for support forums to support and encourage quick question and answer while helping users resolve problems.

However, a sense of presence can cut both ways. A user who is online at a different time from the majority of forum users might feel lonely, or the forum might feel like a “ghost town” to them.

> [@Discourse Who's Online](https://meta.discourse.org/t/whos-online-plugin-discourse-whos-online/52345):
>
> discourse2Summary Discourse Who’s Online displays a list of users currently active on the sitehammer_and_wrenchRepository Link [https://github.com/discourse/discourse-whos-online](https://github.com/discourse/discourse-whos-online)open_bookInstall Guide [How to install plugins in Discourse](https://meta.discourse.org/t/install-plugins-in-discourse/19157)Featuresinformation_source Think carefully before installing this plugin. Discourse is designed for asynchronous discussion, and a list of online users can be harmful for some communities information_source This plugin is b…

If you have users in many countries and want them to have hints about when each other are more likely to be available, consider the National Flags plugin, and encourage users to set a national flag in their profile.

> [@National Flags (fork)](https://meta.discourse.org/t/national-flags-fork/384423):
>
> information_source This is a fork of [National Flags](https://meta.discourse.org/t/national-flags/55969) (which no longer seems to be maintained) and is mainly an updated version to make it work with the latest glimmer/ember/discourse demands. It’s therefor almost functionally identical, with only some small changes (see below). I don’t want to hijack the original topic, in case the author comes back slight_smileinformation_sourceSummary Shows a country flag next to the username on posts and on the profile. …

A tricky one is translation. It would be convenient to help people communicate when they don’t speak the same language, but currently (as of this writing) there are no translation services with free tiers of service. If you choose to pay for translation services, you can enable translation with the Discourse Translator plugin.

> [@Discourse Translator](https://meta.discourse.org/t/discourse-translator/32630):
>
> discourse2Summary Discourse Translator translates posts on Discourse using Microsoft, Google, or Yandex translation APIs.hammer_and_wrenchRepository Link [https://github.com/discourse/discourse-translator](https://github.com/discourse/discourse-translator)open_bookInstall Guide [How to install plugins in Discourse](https://meta.discourse.org/t/install-plugins-in-discourse/19157)Features Each post is only translated once per locale which saves you moneybagNote that language detection is also run once for every post. Be careful when enabli…

# Backup

For system files, consider backing up at least:

- `/var/discourse/containers` (for discourse configuration details)
- `/var/www` (for error pages)
- `/etc/ssl` (for letsencrypt config, to avoid having to bootstrap certbot as part of restoring a backup; otherwise you have to comment out the SSL portion of your nginx configuration while you are bootstrapping; this works only if you keep the backups recent because the certificates have short validity)
- `/etc/systemd/system/backup-uploads.service` (for doing images backups to S3)
- `/usr/local/bin/mc` (minio-client as image backup tool, if you choose to use it)
- `/root/.mc` (configuration for image backup with minio-client)
- `/root/.ssh` (incoming SSH session authentication)

Some of these files you may back up by checking them into Git and pushing them somewhere off site. If the files you check into Git include secrets (like database passwords), definitely don’t push them to a public repository. Alternatively, you could script copying them off the system and checking them into Git on a supervisory system that you control. Script this sufficiently frequently to keep your backups of `/etc/ssl` fresh.

The goal is to have backups both in case of disaster and to have record of changes in case of mistake.

A better alternative for most of these files is to keep the canonical copies elsewhere, and use a tool like Ansible to maintain the configuration on the system, which makes it just as easy to update after a backup. But if you are going to do that, you probably figured it out without me telling you!

## Discourse Configuration for backup

- Back up thumbnails with `include_thumbnails_in_backups`. A restore without thumbnails takes a long time to regenerate them. If your site doesn’t have many graphics, the thumbnails take insignificant space. If your site is graphics-rich, regenerating thumbnails could take days. While thumbnails are being regenerated, email notifications will be disabled. Either way, it makes no sense to omit thumbnails from backups.

- Do not include images in backups if you have lots of images. This will make backups slow and unwieldy. Back them up separately. If you back up images after your database backup, your backups will be consistent.

- Arrange for backups to go off site somehow.

This page shows how to set up database backups to S3 or something like S3:

> [@Configure automatic backups for Discourse](https://meta.discourse.org/t/configure-automatic-backups-for-discourse/14855):
>
> bookmark This guide explains how to configure automatic backups for Discourse, including storage options on local servers and S3-compatible storage. Learn how to set up automatic backups for your Discourse platform. This guide covers configuring automatic backups, storing them on local servers or S3-compatible storage, and managing storage retention options like Amazon Glacier. Configuring automatic backups Navigate to /admin settings. Select the Backup section. Set backup\_frequency to …

### Back up with restic

Configure discourse to back up to the file system, then back up from the file system to a remote backup target with [restic](https://restic.net/). Here’s a sample recipe.

```shell
# dnf install restic
# mkdir /var/restic
# mkdir /opt/backup
# cd /opt/backup
# cat > backup <<EOF
#!/usr/bin/bash

set -e
. /opt/backup/backup-config

restic --cache-dir=/var/restic \
	backup \
	/etc \
	/root \
	/var/discourse \
	/opt/backup \
	--exclude /var/discourse/shared/data/postgres_data

restic --cache-dir=/var/restic forget \
	--prune --keep-hourly 24 --keep-daily 7 --keep-monthly 3
EOF
# chmod +x backup

```

These details will differ depending on the restic target. Not all of them use AWS environment variables, so read the restic documentation.

```shell
# cat > backup-config <<EOF
### These details depend on which restic target you configure, so change them
export AWS_ACCESS_KEY_ID=yours-here
export AWS_SECRET_ACCESS_KEY=same
export RESTIC_PASSWORD_FILE=/root/restic-password
export RESTIC_REPOSITORY=see-the-restic-documentation
EOF
# {$EDITOR:-nano} /root/restic-password

```

You will need to store a copy of whatever you put in `/root/restic-password` or you will not be able to read the backups! Use your password vault.

Finally, create some services, initialize the restic repository, and set a timer so that backups start.

```shell
# cat > /etc/systemd/system/backup.service <<EOF
[Unit]
Description=Back up to remote target
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
StandardOutput=file:/var/log/backup.out
StandardError=file:/var/log/backup.err
WorkingDirectory=/var/discourse
ExecStart=/opt/backup/backup

[Install]
WantedBy=multi-user.target
EOF
# cat > /etc/systemd/system/backup.timer <<EOF
[Unit]
Description=Regular system backups

[Timer]
Persistent=true
OnCalendar=00/4:30:00
Unit=backup.service

[Install]
WantedBy=timers.target
EOF

# systemctl daemon-reload
# . /opt/backup/backup-config
# restic init
# /opt/backup/backup
# systemctl enable backup.timer

```

After this process, you should be able to see that you have created your initial backup.

```shell
# restic snapshots

```

I have successfully used restic backups made this way to move a Discourse server from one system to another using the `restic restore` command.

### Streaming minio image backups

As an alternative to Restic, while database backups can be stored to S3, there is no S3 image backup separate from serving images from S3. An alternative is to use [minio-client](https://docs.min.io/docs/minio-client-complete-guide) to copy images to any S3-like storage. This can be many S3-like targets, including S3 and minio, but [not DigitalOcean Spaces](https://github.com/minio/mc/issues/3067) because it is built on top of the Ceph filesystem which does not implement the `ListObjectsV2` API the same way that S3 does.

In S3, create a bucket that blocks public access ( **Permissions** → **Block public access** is the easy way to get this right in AWS).

Install minio-client (mc) somehow. Here’s one way.

```plaintext
curl https://dl.min.io/client/mc/release/linux-amd64/mc > /usr/local/bin/mc && chmod +x /usr/local/bin/mc

```

Configure minio-client with an alias called `backup` using a command something like this:

```plaintext
# mc alias set backup https://s3.amazonaws.com ACCESSKEY SECRETKEY --api S3v4
# mc mirror /var/discourse/shared/standalone/uploads backup/UPLOADS-BACKUP-BUCKET

```

Then create a service /etc/systemd/system/backup-uploads.service like this

```plaintext
[Unit]
Description=Neartime remote backup sync of discourse uploads
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=600
User=root
ExecStart=/usr/local/bin/mc mirror --overwrite -a --watch /var/discourse/shared/app/uploads backup/UPLOADS-BACKUP-BUCKET

[Install]
WantedBy=multi-user.target

```

Note that `UPLOADS-BACKUP-BUCKET` here should be a different bucket from the `s3_backup_bucket` into which you configure discourse to upload database backups. Also, note that the path will be `/var/discourse/shared/web_only/uploads` if you use the standard multi-container deployment.

```plaintext
# systemctl enable backup-uploads
# systemctl start backup-uploads
# journalctl -fu backup-uploads

```

Upload a test image and make sure you see lines for successfully backing up the original and optimized images. Control-C will exit follow mode in journalctl.

## Recovery

I have never had to test this plan as of this writing. This summary might miss something.

- Restore all backed up files generally
- Start nginx (now your maintenance page will show)
- Do a normal deployment of Discourse using the restored files in /var/discourse/containers
- Install minio-client in /usr/local/bin/mc if you didn’t restore it from backups
- If you did not back up `/root/mc`, set up the backup alias `# mc alias set backup https://s3.amazonaws.com ACCESSKEY SECRETKEY --api S3v4`
- `# mc cp backup/UPLOADS-BACKUP-BUCKET /var/discourse/shared/app/uploads`
- Restore the most recent database backup; I recommend that you [Restore a backup from the command line](https://meta.discourse.org/t/restore-a-backup-from-command-line/108034)
- Only **after** you have confirmed the site is operational, re-configure backing up uploads to S3 as documented above.

## Streaming postgresql backups

In the future, I may create, test, and provide a configuration to enable using [continuous WAL archiving](https://www.postgresql.org/docs/current/continuous-archiving.html) to stream near-instantaneous Postgres backups with minio-client using the `archive-command` in postgresql, similar to streaming uploads backups.

> [@Streaming backups with minio-client?](https://meta.discourse.org/t/streaming-backups-with-minio-client/192924):
>
> I don’t like the most frequent backup schedule of only once per day. That seems like potentially a lot of people’s careful writing to throw away in case of disaster. However, I don’t want to set up a cluster with HA; that’s too resource-heavy for my use case; I just want to have a high degree of confidence that I can recover nearly everything written on my Discourse, even if it takes a while. This Discourse is a free resource for a non-paying community, so I’m sensitive to value that the communi…

# Performance monitoring

There are at least two approaches to performance monitoring.

## Prometheus container

Set up prometheus, putting prometheus logs in /var/discourse/shared/prometheus if you are running it on the same system. Prometheus files can grow large, and you do not want them to fill up the root file system; you also probably want to bring them along if you move to a newer host system (either upgrading to a larger VM or a VM with a newer operating system installation).

> [@Install Prometheus plugin + Prometheus on your server](https://meta.discourse.org/t/install-prometheus-plugin-prometheus-on-your-server/72786):
>
> The new [Discourse Prometheus](https://meta.discourse.org/t/prometheus-exporter-plugin-for-discourse/72666) makes Discourse spit out data that Prometheus can consume and then make cool graphs. Check out the description linked above, if you haven’t already. That data doesn’t help much if you don’t have Prometheus installed. You can head over to [https://prometheus.io/](https://prometheus.io/) to download, install, and configure the Prometheus, or, for $100 I will: install the Prometheus plugin on your Discourse instance Install Prometheus in a Docker container on your server Configure Prometheus …

If you deploy prometheus on the discourse system (or anywhere else on the public internet), configure security in front of it. Installed that way, one option would be nginx configuration like this:

```plaintext
  location /prometheus/ {
    auth_basic "Prometheus";
    auth_basic_user_file /etc/nginx/prometheus-htpasswd;
    proxy_pass http://localhost:9090/;
  }

```

## Sysstat

If Prometheus is too much, consider using sysstat instead.

- `dnf install sysstat` (or `apt install sysstat` on debian and derivatives)
- `systemctl enable --now sysstat`
- `systemctl enable --now sysstat-collect.timer`
- `systemctl enable --now sysstat-summary.timer`
- `systemctl edit sysstat-collect.timer` and change `OnCalendar=*:00/10` to `OnCalendar=*:00/2`
- If /etc/default/sysstat exists, change `false` to `true`

After this, the `sar` command can tell you if you are running out of resources from time to time.

# Other Resources

Here’s a complementary (and more compact) discussion of using Discourse internally as a primary form of internal communications.

> [@Why you should use Discourse internally for your company/team instead of Slack (4 years use case)](https://meta.discourse.org/t/why-you-should-use-discourse-internally-for-your-company-team-instead-of-slack-4-years-use-case/296983):
>
> We have been using Discourse as the main tool for communication, record keeping, research documenting, lab journaling tool for over 4 years. I will argue that most companies would greatly benefit from having Discourse as their main communication tool rather than Slack & email chains & WhatsApp. It boils down to this: If you believe there is any value in the conversations between employees for future reflection then you need Discourse. The simple reason is that channel based instant messaging …

---

_[View the full topic](https://meta.discourse.org/t/mkjs-opinionated-discourse-deployment-configuration/193355)._
