Запуск 3 контейнеров для 3 доменов в одной установке с SSL

После нескольких дней настройки с Let’s Encrypt, вот инструкция для всех, кто заинтересован в запуске нескольких форумов.

1 Цель

Предположим, у вас есть домен следующего вида, и вы хотите запустить 3 форума:

bbs.antivte.com
cp.antivte.com
ytb.antivte.com

2 Контейнер Discourse

2.1 Подготовка

У вас должно быть 3 файла app.yml для разных форумов, названных, например, bbs.yml, cp.yml, ytb.yml — как вам удобно.
Содержимое должно быть следующим:
Обратите внимание: мы используем unix-сокет для подключения внешнего nginx и бэкенд-контейнера Discourse вместо прослушивания портов 80 и 443, а также удаляем SSL-конфигурацию для бэкенд-контейнера.
Обратите внимание также: здесь у нас только один контейнер для каждого форума, а не отдельные контейнеры для данных и веб-части для каждого форума.

## Это шаблон автономного контейнера Discourse "все-в-одном"
##
## После внесения изменений в этот файл вы ОБЯЗАНЫ выполнить пересборку:
## /var/discourse/launcher rebuild app
##
## БУДЬТЕ ОЧЕНЬ ОСТОРОЖНЫ ПРИ РЕДАКТИРОВАНИИ!
## YAML-ФАЙЛЫ ЧРЕЗВЫЧАЙНО ЧУВСТВИТЕЛЬНЫ К ОШИБКАМ В ПРОБЕЛАХ И ВЫРАВНИВАНИИ!
## Посетите http://www.yamllint.com/, чтобы при необходимости проверить этот файл

templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  - "templates/web.ratelimited.template.yml"
## Раскомментируйте эти две строки, если хотите добавить Let's Encrypt (https)
#  - "templates/web.ssl.template.yml"
#  - "templates/web.letsencrypt.ssl.template.yml"
  - "templates/web.socketed.template.yml"  # <-- Добавлено
## Какие TCP/IP-порты должен открывать этот контейнер?
## Если вы хотите, чтобы Discourse использовал порт совместно с другим веб-сервером, например Apache или nginx,
## см. https://meta.discourse.org/t/17247 для деталей
**#expose:**
**#  - "8080:80"   # http**
**#  - "8443:443" # https**

params:
  db_default_text_search_config: "pg_catalog.english"

  ## Установите db_shared_buffers максимум в 25% от общего объема памяти.
  ## Будет установлено автоматически при загрузке на основе обнаруженной памяти, либо вы можете переопределить
  db_shared_buffers: "256MB"

  ## Может улучшить производительность сортировки, но увеличивает использование памяти на подключение
  #db_work_mem: "40MB"

  ## Какую ревизию Git должен использовать этот контейнер? (по умолчанию: tests-passed)
  #version: tests-passed

env:
  LANG: en_US.UTF-8
  # DISCOURSE_DEFAULT_LOCALE: en

  ## Сколько одновременных веб-запросов поддерживается? Зависит от памяти и ядер CPU.
  ## Будет установлено автоматически при загрузке на основе обнаруженных CPU, либо вы можете переопределить
  UNICORN_WORKERS: 4

  ## TODO: Доменное имя, на которое будет отвечать этот экземпляр Discourse
  ## Обязательно. Discourse не будет работать с чистым IP-адресом.
  DISCOURSE_HOSTNAME: bbs.antivte.com

  ## Раскомментируйте, если хотите, чтобы контейнер запускался с тем же
  ## именем хоста (-h опция), что указано выше (по умолчанию "$hostname-$config")
  #DOCKER_USE_HOSTNAME: true

  ## TODO: Список email-адресов через запятую, которые станут администраторами и разработчиками
  ## при первой регистрации, например 'user1@example.com,user2@example.com'
  DISCOURSE_DEVELOPER_EMAILS: 'techempower@163.com'

  ## TODO: SMTP-сервер, используемый для проверки новых аккаунтов и отправки уведомлений
  ## SMTP-адрес, имя пользователя и пароль обязательны
  # ВНИМАНИЕ: символ '#' в пароле SMTP может вызвать проблемы!
  DISCOURSE_SMTP_ADDRESS: smtp.mailgun.org
  DISCOURSE_SMTP_PORT: 587
  DISCOURSE_SMTP_USER_NAME: postmaster@mail.antivte.com
  DISCOURSE_SMTP_PASSWORD: "67c9458eb7a6ff11b4db70f097b1b5c3-f7910792-0e7dbcc9"
  #DISCOURSE_SMTP_ENABLE_START_TLS: true           # (опционально, по умолчанию true)

  ## Если вы добавили шаблон Let's Encrypt, раскомментируйте ниже, чтобы получить бесплатный SSL-сертификат
  LETSENCRYPT_ACCOUNT_EMAIL: techempower@163.com

  ## HTTP или HTTPS CDN-адрес для этого экземпляра Discourse (настроен на загрузку)
  ## см. https://meta.discourse.org/t/14857 для деталей
  #DISCOURSE_CDN_URL: https://discourse-cdn.example.com

## Контейнер Docker не хранит состояние; все данные хранятся в /shared
volumes:
  - volume:
      **host: /var/discourse/shared/bbs**
      guest: /shared
  - volume:
      **host: /var/discourse/shared/bbs/log/var-log**
      guest: /var/log

## Плагины размещаются здесь
## см. https://meta.discourse.org/t/19157 для деталей
hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/discourse/docker_manager.git
          - git clone https://github.com/procourse/procourse-installer

## Любые пользовательские команды для запуска после сборки
run:
  - exec: echo "Начало пользовательских команд"
  ## Если вы хотите установить адрес email "От" для вашей первой регистрации, раскомментируйте и измените:
  ## После получения первого письма регистрации закомментируйте строку обратно. Выполняется только один раз.
  #- exec: rails r "SiteSetting.notification_email='info@unconfigured.discourse.org'"
  - exec: echo "Конец пользовательских команд"

2.2 Настройка

Создайте скрипт настройки следующего вида:

#!/usr/bin/env bash
./launcher bootstrap bbs
./launcher bootstrap test
./launcher bootstrap cp
./launcher bootstrap ytb
./launcher start bbs
./launcher  start test
./launcher  start  cp
./launcher  start  ytb

Если вы уже использовали этот скрипт и запускали каждый контейнер хотя бы раз, то после любого изменения содержимого app.yml вам нужно выполнить пересборку контейнера, чтобы изменения вступили в силу:

./launcher rebuild bbs

2.3 Проверка

Вы увидите, что 3 контейнера успешно запущены, и проверьте работу unix-сокета:

root@docker-s-1vcpu-2gb-sgp1-01:~# docker ps
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS              PORTS               NAMES
9702f94ea9b4        local_discourse/bbs   "/sbin/boot"        9 часов назад         Up 9 часов                              bbs
dc13c303c38e        local_discourse/cp    "/sbin/boot"        9 часов назад         Up 9 часов                              cp
dafa592ee16f        local_discourse/ytb   "/sbin/boot"        9 часов назад         Up 9 часов                              ytb
root@docker-s-1vcpu-2gb-sgp1-01:~#  curl --unix-socket /var/discourse/shared/bbs/nginx.http.sock http:/images/json
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>Дом тромбоза</title>

3 Внешний nginx

Установите nginx/OpenResty на ваш сервер и создайте файл nginx.conf в любой удобной директории. Директория влияет только на команду запуска nginx. Содержимое nginx.conf следующее:
Здесь у меня есть сертификат и ключ Cloudflare, размещенные в директории хоста следующим образом:

3.1 nginx.conf

    ssl_certificate      /var/discourse/shared/ssl/antivte.com.cert.pem;
    ssl_certificate_key  /var/discourse/shared/ssl/antivte.com.key.pem;

В будущем я расскажу, как использовать автоматические сертификаты и ключи Let’s Encrypt во внешнем nginx.

events {
  worker_connections 1024;
}

http {
  # Общий словарь "auto_ssl" должен быть определен с достаточным объемом памяти для
  # хранения данных сертификатов. 1 МБ памяти хватает примерно на 100 отдельных доменов.
  lua_shared_dict auto_ssl 1m;
  # Общий словарь "auto_ssl_settings" используется для временного хранения различных настроек,
  # таких как секрет, используемый сервером хуков на порту 8999. Не изменяйте и не удаляйте его.
  lua_shared_dict auto_ssl_settings 64k;

  # Для работы OCSP-штамповки должен быть определен DNS-резолвер.
  #
  # В этом примере используется DNS-сервер Google. Возможно, стоит использовать DNS-серверы вашей системы по умолчанию,
  # которые можно найти в /etc/resolv.conf. Если ваша сеть не совместима с IPv6, вы можете отключить результаты IPv6,
  # используя флаг "ipv6=off" (например, "resolver 8.8.8.8 ipv6=off").
  resolver 8.8.8.8;
server {
    listen 80; listen [::]:80;
    server_name bbs.antivte.com;  # <-- измените это

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;  listen [::]:443 ssl http2;
    server_name bbs.antivte.com;  # <-- измените это

    ssl_certificate      /var/discourse/shared/ssl/antivte.com.cert.pem;
    ssl_certificate_key  /var/discourse/shared/ssl/antivte.com.key.pem;
#    ssl_dhparam          /var/discourse/shared/standalone/ssl/dhparams.pem;
    ssl_session_tickets off;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA;

    http2_idle_timeout 5m; # увеличено с 3m по умолчанию

    location / {
        proxy_pass http://unix:/var/discourse/shared/bbs/nginx.http.sock;
        proxy_set_header Host $http_host;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

server {
    listen 80; listen [::]:80;
    server_name cp.antivte.com;  # <-- измените это

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;  listen [::]:443 ssl http2;
    server_name cp.antivte.com;  # <-- измените это

    ssl_certificate      /var/discourse/shared/ssl/antivte.com.cert.pem;
    ssl_certificate_key  /var/discourse/shared/ssl/antivte.com.key.pem;
#    ssl_dhparam          /var/discourse/shared/standalone/ssl/dhparams.pem;
    ssl_session_tickets off;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA;

    http2_idle_timeout 5m; # увеличено с 3m по умолчанию

    location / {
        proxy_pass http://unix:/var/discourse/shared/cp/nginx.http.sock;
        proxy_set_header Host $http_host;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Real-IP $remote_addr;
    }
}



server {
    listen 80; listen [::]:80;
    server_name ytb.antivte.com;  # <-- измените это

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;  listen [::]:443 ssl http2;
    server_name ytb.antivte.com;  # <-- измените это

    ssl_certificate      /var/discourse/shared/ssl/antivte.com.cert.pem;
    ssl_certificate_key  /var/discourse/shared/ssl/antivte.com.key.pem;
#    ssl_dhparam          /var/discourse/shared/standalone/ssl/dhparams.pem;
    ssl_session_tickets off;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA;

    http2_idle_timeout 5m; # увеличено с 3m по умолчанию

    location / {
        proxy_pass http://unix:/var/discourse/shared/ytb/nginx.http.sock;
        proxy_set_header Host $http_host;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Real-IP $remote_addr;
    }
}



}

3.2 Запуск nginx

Перейдите в директорию, где размещен ваш nginx.conf, и выполните команду:

 nginx -p `pwd`/ -c nginx.conf

4. Теперь вы получите то, что хотели

Ура!

Похоже, это более сложная версия уже существующего решения Discourse для нескольких сайтов. Интересно, какое преимущество даёт запуск трёх отдельных контейнеров, конкурирующих за одни и те же ресурсы, вместо одного (или двух, если веб и данные разделены)??

Это всё вопрос бюджета. И вы можете рассматривать это как предварительный этап, когда вы запускаете прототип для тестирования с вашими клиентами и маркетингом.

Даже на этапе бюджетирования я считаю, что мультисайтовое решение будет гораздо более простым и практичным в реализации. Это позволит мне меньше беспокоиться о конфигурации и больше времени уделять требованиям клиентов.

Всё это связано с тем, что я хочу тестировать и запускать production на моём собственном сервере. Если порты 80 и 443 на хосте заняты, у меня нет других способов реализовать всё это.

Огромное спасибо за публикацию, но, на мой взгляд, для того чтобы это стало #инструкцией, нужно гораздо больше деталей. Переношу в другую категорию.