Install and Run Discourse behind Nginx the Right Way, First Time!

Everyone says it is simple. It is NOT, even especially when you want to run Discourse behind a front-facing server like Nginx. After going through enough pain to get up and running a Discourse instance on time, see how to Install and Run Discourse behind Nginx the Right Way the First Time!

This tutorial makes these assumptions, so if you meet any of them, you’re good to know:

  • You’re on a VPS such as a Droplet from Digital Ocean with specs
    • 1 Gig or more RAM
    • 1 Core CPU or more
  • You’re on Ubuntu 16.04 LTS
  • You know how to work your way around your VPS
  • You have a email account at Zoho already, sending emails
  • You have Nginx installed
  • You are serving website(s) already with Nginx
  • You want Discourse installed on a subdomain, such as
  • We use as the example domain

If you’re looking to install Discourse on a fresh droplet, this might not be what you want. Check out the other tutorials on how to do so.

Our Itenary

This is how you might wanna approach it, which worked for me:

  • Send Discourse notification emails with Zoho Mail
    • This is important so that when someone signs up, they can receive an email from Discourse to verify their account.
  • Front-facing server is Nginx (Discourse uses Nginx too in its Docker instance)
  • Install Docker Engine
  • Install Discourse

So, without much ado, let’s get our discourse running, beginning with the simple steps.

Setting up Email

Probably a convention now, emails you send messages from but don’t intend to receive or reply to messages are usually named somewhere along the lines of ‘No Reply’.

So our email we’ll be sending user notifications from will be

Using an email address with a . (dot) in the name failed several times i.e I couldn’t find any docs saying that ain’t allowed, and no errors were or are thrown, except I could NOT authenticate. I found this the hardway.

Maybe I didn’t do something right somewhere, but with a dot never worked for me.

Adding an email account to your Zoho should be simple. I am of the assumption that you already have something like so you’re simply adding another user.

Zoho Mail Add User
Zoho Mail Add User

What password to use? All-numbers password didn’t work for me also. It is hard to tell at the moment, but it just didn’t work, always had the Authentication Error.

Email is out of the way.

A simple way to verify if your Zoho new email user is functioning, simply open a new Incognito (or something similar) tab in your browser, visit, log in with your new email user.

We’ll want to send emails using the email address, so make sure you’ve opened up the POP in the new email account.

Green means Go!


Install Docker


sudo apt install docker-engine
sudo service docker start
sudo docker run hello-world

The last line is to ensure and test if docker is properly installed.

Add the current user to the docker group like so:

sudo usermod -aG docker $USER

Done with Docker!

Discourse, Baby!

Installing Discourse is a straightforward process too, except information relating to how to get it happening ‘straightforwardly’ is not easily found.

Let’s go!

sudo mkdir /var/discourse
sudo git clone /var/discourse

Then, we need to manually prepare our app.yml file.

We want to install Discourse as a standalone. By manually creating our app.yml we get to tweak things a little bit.

cd /var/discourse
sudo cp samples/standalone.yml containers/app.yml
sudo nano containers/app.yml

The app.yml gives us a lot of options, however, as you go through, notice what we comment out, such as:

  • We don’t want Rate Limiting with our Discourse Standalone
  • SSL will be handled by the outer Nginx, so we also disable that
  • The Discourse instance is exposed on port 25654:80 on which our outer Nginx will be mapped against. The idea is so that we can share Discourse with another web server like Apache or Nginx which don’t joke with their port 80.
    • On my Droplet, I only have 3 ports open, including port 80, through which EVERY web request incoming goes through. That port is so crucial I can’t just kick my outer Nginx off that and replace with Discourse. That’s where what port to expose Discourse instance on is crucially useful!

In the app.yml that shows on screen, we go like this:

I don’t just get it. Why is the configuration file not a JSON object instead?

Obviously, YAML has issues and isn’t intuitive to the average dumb guy like myself. A clear sign Discourse has been rigged for only the Elite in the society.

## After making changes to this file, you MUST rebuild
## /var/discourse/launcher rebuild app
## visit to validate this file as needed

  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  #- "templates/web.socketed.template.yml"
  #- "templates/web.ratelimited.template.yml"
## which TCP/IP ports should this container expose?
## If you want Discourse to share a port with another webserver like Apache or nginx,
## see for details
  - "25654:80"
#  - "80:80"   # http
#  - "443:443" # https

  db_default_text_search_config: "pg_catalog.english"

  ## Set db_shared_buffers to a max of 25% of the total memory.
  ## will be set automatically by bootstrap based on detected RAM, or you can override
  db_shared_buffers: "128MB"
  ## can improve sorting performance, but adds memory usage per-connection
  #db_work_mem: "40MB"
  ## Which Git revision should this container use? (default: tests-passed)
  #version: tests-passed

  LANG: en_US.UTF-8

  ## How many concurrent web requests are supported? Depends on memory and CPU cores.
  ## will be set automatically by bootstrap based on detected CPUs, or you can override

  ## TODO: The domain name this Discourse instance will respond to
  ## Consider this as what transforms into the server_name in an Nginx configuration
  ## Uncomment if you want the container to be started with the same
  ## hostname (-h option) as specified above (default "$hostname-$config")

  ## TODO: List of comma delimited emails that will be made admin and developer
  ## on initial signup example ','
  ## This email is what you'll use to log into Discourse instance the first time.

  ## TODO: The SMTP mail server used to validate new accounts and send notifications
  DISCOURSE_SMTP_PASSWORD: addpasswordhere  # WARNING a char '#' in pw can cause problems!
  ## There wouldn't be any issue like above with password if this is a JSON object

  ## The CDN address for this Discourse instance (configured to pull)
  ## see for details

## The Docker container is stateless; all data is stored in /shared
  - volume:
      host: /var/discourse/shared/standalone
      guest: /shared
  - volume:
      host: /var/discourse/shared/standalone/log/var-log
      guest: /var/log

## Plugins go here
## see for details
    - exec:
        cd: $home/plugins
          - git clone

## Any custom commands to run after building
  - exec: echo "Beginning of custom commands"
  ## If you want to set the 'From' email address for your first registration, uncomment and change:
  ## After getting the first signup email, re-comment the line. It only needs to run once.
  - exec: rails r "SiteSetting.notification_email=''"
  - exec: echo "End of custom commands"

The above is the complete app.yml I use, just replace the dummy parts with your own. Remember from above what you need to put in the password and username fields. The email should be without dots and the password not entirely numeric.

On a 1 Gig RAM Droplet, you might consider doing these tunings as per recommendations from Digital Ocean.

In the env section of the configuration file, set db_shared_buffers to 128MB and UNICORN_WORKERS to 2 so you have more memory room, like so

db_shared_buffers: "128MB" and UNICORN_WORKERS: 2

Tuning these memory settings will optimize Discourse performance on a 1 GB Droplet.

In the run block we’ve enabled an exec function to run. This is important to get emails sending for the first time. We can disable and rebuild later on, but for the first time, keep that line uncommented.

If you have that configuration tweaked, checked and rechecked, move onto the next part which deals with bringing up our app container.

We disabled the "templates/web.ratelimited.template.yml" because, our Inner Nginx tends to think only one IP address is trying to make multiple connections, which isn’t true.

See, when the Outer Nginx takes the incoming request, it funnels all the request to the Inner Nginx as a single user from a single IP. This gives the inner Nginx the impression a user is trying to bombard it with recurring multiple requests.

This causes Nginx to go in a stalemate. Disabling this here, but enabling it in the outer Nginx will actually be what we’re looking for.

If our Discourse instance is accepting requests from the public, there won’t be any need to remove the rate limiting, as incoming connections will be from random multiple users which won’t trigger the flood error you normally get.





Our app needs to be bootstrapped first. We’re still in the /var/discourse directory. That can be achieved with this:

sudo ./launcher bootstrap app

That will take some time. When done, we go ahead to bring it up with:

sudo ./launcher start app

Configure Nginx

This configuration is for the outer Nginx. We saw how to manage the inner Nginx a little bit in the previous section.

With that removal, the change applied to inner Nginx configuration when the app was built.

I use the express ‘Inner Nginx’ to refer to the Nginx bundled with Discourse’s Docker image. That Nginx is different from the ‘Outer Nginx’, the one currently facing the world.

This part, we’ll focus on the Outer Nginx configuration.

In the /etc/nginx/sites-enabled/ you should see a discourse.conf file. If not available there, check the /etc/nginx/sites-available/ directory for the file.

If you still don’t see any, create a file, namely discourse.conf in the /etc/nginx/sites-available/ folder and update it with these content.

server {
    listen 80; listen [::]:80;

    return 301 https://$host$request_uri;

server {
    listen 443 ssl http2;  

    include /etc/nginx/ssl/globalssl.conf;

    location / {
        proxy_set_header Host $http_host;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

The above configuration uses SSL generated by LetsEncrypt. We’re in 2016, and if you don’t happen to have your sites running on HTTPS, then ask yourself why, because SSL is free and extremely easy to generate these days.

Generating an SSL for your site is out of the scope of this article, but there are plenty of articles out there to this effect.

You need the proxy_set_header X-Forwarded-Proto $scheme; to ensure your Sidekiq works properly. Without this, you’ll come across the Forbidden when you try to issue actions in your Sidekiq dashboard

Now that our outer Nginx too is properly setup, a restart will be needed after we check our configurations for errors, so:

sudo nginx -t

If any errors, fix them. If all goes well,

sudo service nginx restart

Try accessing in your browser and you should be greeted with Discourse with the opportunity to log in with your account.

If you can’t log into your account, you might wanna reset it. Follow the steps below:

Run this command from console:

rake admin:create

You will be asked for Email, enter the email of an existing account.

Now you will be asked: User with this email already exists! Do you want to reset the password for this email? (Y/n). Press enter to continue.

Provide the new password and confirm your password.

If it worked, you’ll see Account updated successfully!

Now switch back to the browser and log in again. There you go!

Discourse in Action


Related Articles

Back to top button