How to Set Up Discourse Multisite with Letsencrypt SSL


#1

Through trials & errors, we finally managed to set up Discourse multisite with Letsencrypt SSL. Below is the step-by-step guide on how to implement it.

Note: Official Discourse does not support multiple SMTP for multisite, hence it is not possible to use different SMTP services for multisite installs.

  1. During bootstrap, instead of following the first time set up prompt, press CTRL + C to exit and manually edit the app.yml using command: nano containers/app.yml

  2. Example content of manually edited app.yml:

    this is the all-in-one, standalone Discourse Docker container template

    After making changes to this file, you MUST rebuild

    /var/discourse/launcher rebuild app

    BE VERY CAREFUL WHEN EDITING!

    YAML FILES ARE SUPER SUPER SENSITIVE TO MISTAKES IN WHITESPACE OR ALIGNMENT!

    visit http://www.yamllint.com/ to validate this file as needed

    templates:

    • “templates/postgres.template.yml”
    • “templates/redis.template.yml”
    • “templates/web.template.yml”
    • “templates/web.ratelimited.template.yml”

    Uncomment these two lines if you wish to add Lets Encrypt (https)

    #- “templates/web.ssl.template.yml”
    #- “templates/web.letsencrypt.ssl.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 https://meta.discourse.org/t/17247 for details

    expose:

    • “80” # http

    params:
    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: “3584MB”

    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

    env:
    LANG: en_US.UTF-8

    DISCOURSE_DEFAULT_LOCALE: en

    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

    UNICORN_WORKERS: 4

    TODO: The domain name this Discourse instance will respond to

    DISCOURSE_HOSTNAME: 'test1.domain.com
    VIRTUAL_HOST: 'test1.domain.com,test2.domain.com,test3.domain.com,test4.domain.com,test5.domain.com
    LETSENCRYPT_HOST: 'test1.domain.com,test2.domain.com,test3.domain.com,test4.domain.com,test5.domain.com
    LETSENCRYPT_EMAIL: ‘name@email.com’

    Uncomment if you want the container to be started with the same

    hostname (-h option) as specified above (default “$hostname-$config”)

    #DOCKER_USE_HOSTNAME: true

    TODO: List of comma delimited emails that will be made admin and developer

    on initial signup example ‘user1@example.com,user2@example.com’

    DISCOURSE_DEVELOPER_EMAILS: ‘name@email.com’

    TODO: The SMTP mail server used to validate new accounts and send notifications

    DISCOURSE_SMTP_ADDRESS: smtp.sparkpostmail.com # required
    DISCOURSE_SMTP_PORT: 587 # (optional, default 587)
    DISCOURSE_SMTP_USER_NAME: SMTP_Injection # required
    DISCOURSE_SMTP_PASSWORD: 999api999password999here999 # required, WARNING the char ‘#’ in pw can cause problems!
    #DISCOURSE_SMTP_ENABLE_START_TLS: true # (optional, default true)

    If you added the Lets Encrypt template, uncomment below to get a free SSL certificate

    #LETSENCRYPT_ACCOUNT_EMAIL: name@email.com

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

    see https://meta.discourse.org/t/14857 for details

    #DISCOURSE_CDN_URL: //discourse-cdn.example.com

    The Docker container is stateless; all data is stored in /shared

    volumes:

    • volume:
      host: /var/discourse/shared/standalone
      guest: /shared
    • volume:
      host: /var/discourse/shared/standalone/log/var-log
      guest: /var/log

    Plugins go here

    see https://meta.discourse.org/t/19157 for details

    hooks:
    after_postgres:
    - exec: sudo -u postgres createdb b_discourse || exit 0
    - exec:
    stdin: |
    grant all privileges on database b_discourse to discourse;
    cmd: sudo -u postgres psql b_discourse
    raise_on_fail: false

     - exec: /bin/bash -c 'sudo -u postgres psql b_discourse <<< "alter schema public owner to discourse;"'
     - exec: /bin/bash -c 'sudo -u postgres psql b_discourse <<< "create extension if not exists hstore;"'
     - exec: /bin/bash -c 'sudo -u postgres psql b_discourse <<< "create extension if not exists pg_trgm;"'
     - exec: sudo -u postgres createdb c_discourse || exit 0
     - exec:
          stdin: |
            grant all privileges on database c_discourse to discourse;
          cmd: sudo -u postgres psql c_discourse
          raise_on_fail: false
    
     - exec: /bin/bash -c 'sudo -u postgres psql c_discourse <<< "alter schema public owner to discourse;"'
     - exec: /bin/bash -c 'sudo -u postgres psql c_discourse <<< "create extension if not exists hstore;"'
     - exec: /bin/bash -c 'sudo -u postgres psql c_discourse <<< "create extension if not exists pg_trgm;"'
     - exec: sudo -u postgres createdb d_discourse || exit 0
     - exec:
          stdin: |
            grant all privileges on database d_discourse to discourse;
          cmd: sudo -u postgres psql d_discourse
          raise_on_fail: false
    
     - exec: /bin/bash -c 'sudo -u postgres psql d_discourse <<< "alter schema public owner to discourse;"'
     - exec: /bin/bash -c 'sudo -u postgres psql d_discourse <<< "create extension if not exists hstore;"'
     - exec: /bin/bash -c 'sudo -u postgres psql d_discourse <<< "create extension if not exists pg_trgm;"'
     - exec: sudo -u postgres createdb e_discourse || exit 0
     - exec:
          stdin: |
            grant all privileges on database e_discourse to discourse;
          cmd: sudo -u postgres psql e_discourse
          raise_on_fail: false
    
     - exec: /bin/bash -c 'sudo -u postgres psql e_discourse <<< "alter schema public owner to discourse;"'
     - exec: /bin/bash -c 'sudo -u postgres psql e_discourse <<< "create extension if not exists hstore;"'
     - exec: /bin/bash -c 'sudo -u postgres psql e_discourse <<< "create extension if not exists pg_trgm;"'
    

    after_code:
    - exec:
    cd: $home/plugins
    cmd:
    - git clone https://github.com/discourse/docker_manager.git
    before_bundle_exec:
    - file:
    path: $home/config/multisite.yml
    contents: |
    secondsite:
    adapter: postgresql
    database: b_discourse
    pool: 25
    timeout: 5000
    db_id: 2
    host_names:
    - test2.domain.com
    thirdsite:
    adapter: postgresql
    database: c_discourse
    pool: 25
    timeout: 5000
    db_id: 3
    host_names:
    - test3.domain.com
    fourthsite:
    adapter: postgresql
    database: d_discourse
    pool: 25
    timeout: 5000
    db_id: 4
    host_names:
    - test4.domain.com
    fifthsite:
    adapter: postgresql
    database: e_discourse
    pool: 25
    timeout: 5000
    db_id: 5
    host_names:
    - test5.domain.com

    after_bundle_exec:
    - exec: cd /var/www/discourse && sudo -E -u discourse bundle exec rake multisite:migrate

    Any custom commands to run after building

    run:

    • 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=‘noreply@domain.com’”
    • exec: echo “End of custom commands”

The edited parts are:

  1. only expose one port 80 and do not map it.

    expose:

    • “80” # http
  2. Edit the DISCOURSE_HOSTNAME and add VIRTUAL_HOST, LETSENCRYPT_HOST & LETSENCRYPT_EMAIL

    DISCOURSE_HOSTNAME: 'test1.domain.com
    VIRTUAL_HOST: 'test1.domain.com,test2.domain.com,test3.domain.com,test4.domain.com,test5.domain.com
    LETSENCRYPT_HOST: 'test1.domain.com,test2.domain.com,test3.domain.com,test4.domain.com,test5.domain.com
    LETSENCRYPT_EMAIL: ‘name@email.com’

  3. Edit DISCOURSE_DEVELOPER_EMAILS, DISCOURSE_SMTP_ADDRESS, DISCOURSE_SMTP_PORT (applicable for SparkPost), DISCOURSE_SMTP_USER_NAME & DISCOURSE_SMTP_PASSWORD

    DISCOURSE_DEVELOPER_EMAILS: ‘name@email.com’

    TODO: The SMTP mail server used to validate new accounts and send notifications

    DISCOURSE_SMTP_ADDRESS: smtp.sparkpostmail.com # required
    DISCOURSE_SMTP_PORT: 587 # (optional, default 587)
    DISCOURSE_SMTP_USER_NAME: SMTP_Injection # required
    DISCOURSE_SMTP_PASSWORD: 999api999password999here999 # required, WARNING the char ‘#’ in pw can cause problems!

  4. Add this whole chunk of code after hooks: depending on how many sites you want. Example listed below are for 5 sites: b_discourse, c_discourse, d_discourse & e_discourse. If just two sites then b_discourse is sufficient and do not include c_discourse, d_discourse & e_discourse codes.

    hooks:
    after_postgres:
    - exec: sudo -u postgres createdb b_discourse || exit 0
    - exec:
    stdin: |
    grant all privileges on database b_discourse to discourse;
    cmd: sudo -u postgres psql b_discourse
    raise_on_fail: false

     - exec: /bin/bash -c 'sudo -u postgres psql b_discourse <<< "alter schema public owner to discourse;"'
     - exec: /bin/bash -c 'sudo -u postgres psql b_discourse <<< "create extension if not exists hstore;"'
     - exec: /bin/bash -c 'sudo -u postgres psql b_discourse <<< "create extension if not exists pg_trgm;"'
     - exec: sudo -u postgres createdb c_discourse || exit 0
     - exec:
          stdin: |
            grant all privileges on database c_discourse to discourse;
          cmd: sudo -u postgres psql c_discourse
          raise_on_fail: false
    
     - exec: /bin/bash -c 'sudo -u postgres psql c_discourse <<< "alter schema public owner to discourse;"'
     - exec: /bin/bash -c 'sudo -u postgres psql c_discourse <<< "create extension if not exists hstore;"'
     - exec: /bin/bash -c 'sudo -u postgres psql c_discourse <<< "create extension if not exists pg_trgm;"'
     - exec: sudo -u postgres createdb d_discourse || exit 0
     - exec:
          stdin: |
            grant all privileges on database d_discourse to discourse;
          cmd: sudo -u postgres psql d_discourse
          raise_on_fail: false
    
     - exec: /bin/bash -c 'sudo -u postgres psql d_discourse <<< "alter schema public owner to discourse;"'
     - exec: /bin/bash -c 'sudo -u postgres psql d_discourse <<< "create extension if not exists hstore;"'
     - exec: /bin/bash -c 'sudo -u postgres psql d_discourse <<< "create extension if not exists pg_trgm;"'
     - exec: sudo -u postgres createdb e_discourse || exit 0
     - exec:
          stdin: |
            grant all privileges on database e_discourse to discourse;
          cmd: sudo -u postgres psql e_discourse
          raise_on_fail: false
    
     - exec: /bin/bash -c 'sudo -u postgres psql e_discourse <<< "alter schema public owner to discourse;"'
     - exec: /bin/bash -c 'sudo -u postgres psql e_discourse <<< "create extension if not exists hstore;"'
     - exec: /bin/bash -c 'sudo -u postgres psql e_discourse <<< "create extension if not exists pg_trgm;"'
    
  5. Add this chunk of code after after_code: section.

    before_bundle_exec:
    - file:
    path: $home/config/multisite.yml
    contents: |
    secondsite:
    adapter: postgresql
    database: b_discourse
    pool: 25
    timeout: 5000
    db_id: 2
    host_names:
    - test2.domain.com
    thirdsite:
    adapter: postgresql
    database: c_discourse
    pool: 25
    timeout: 5000
    db_id: 3
    host_names:
    - test3.domain.com
    fourthsite:
    adapter: postgresql
    database: d_discourse
    pool: 25
    timeout: 5000
    db_id: 4
    host_names:
    - test4.domain.com
    fifthsite:
    adapter: postgresql
    database: e_discourse
    pool: 25
    timeout: 5000
    db_id: 5
    host_names:
    - test5.domain.com

    after_bundle_exec:
    - exec: cd /var/www/discourse && sudo -E -u discourse bundle exec rake multisite:migrate

  6. Edit SiteSetting.notification_email to noreply@domain.com (applicable for using SparkPost)

    • exec: rails r “SiteSetting.notification_email=‘noreply@domain.com’”
  7. Press CTRL + O to save edits followed by CTRL + X to exit. Enter the following command to bootstrap: ./launcher bootstrap app

  8. Applicable for using SparkPost: after bootstrap successfully, you should be able to create admin account using your predefined admin email in app.yml for test1.domain.com. However, you will not be able to create admin account for other sites as they will try to send out emails from noreply@test2.domain.com, noreply@test3.domain.com, noreply@test4.domain.com & noreply@test5.domain.com but will be rejected by SparkPost due to policy_rejection.

    error_code 550
    raw_reason 550 5.7.1 Unconfigured Sending Domain <test2.domain.com>

To resolve this problem, add test2.domain.com, test3.domain.com, test4.domain.com & test5.domain.com to SparkPost Sending Domains and verify using DKIM records set in DNS settings of the (sub)domains. After you have verified the sending domains successfully SparkPost will still take a couple minutes to change the sending domains’ status to Ready to send. Then you will be able to sign up an admin account using the predefined developer email address and receive the ‘Confirm your new account’ verification email from SparkPost.