在一个安装中为三个域名运行三个容器并启用 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 socket 来连接外部的 Nginx 和后端的 Discourse 容器,而不是监听 80 或 443 端口,同时也移除了后端容器的 SSL 配置。
请注意,这里每个论坛只有一个容器,而不是为每个论坛分别设置数据容器和 Web 容器。

## 这是一个全功能的独立 Discourse Docker 容器模板
##
## 修改此文件后,你必须重新构建
## /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 等其他 Web 服务器共享端口,
## 详见 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%。
  ## 将由 bootstrap 根据检测到的 RAM 自动设置,你也可以覆盖
  db_shared_buffers: "256MB"

  ## 可以提高排序性能,但会增加每个连接的内存使用量
  #db_work_mem: "40MB"

  ## 此容器应使用哪个 Git 版本?(默认:tests-passed)
  #version: tests-passed

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

  ## 支持多少并发 Web 请求?取决于内存和 CPU 核心数。
  ## 将由 bootstrap 根据检测到的 CPU 自动设置,你也可以覆盖
  UNICORN_WORKERS: 4

  ## TODO: 此 Discourse 实例将响应的域名
  ## 必需项。Discourse 无法仅通过 IP 地址运行。
  DISCOURSE_HOSTNAME: bbs.antivte.com

  ## 如果你想让容器以与上述指定的相同的主机名(-h 选项)启动,请取消注释
  #DOCKER_USE_HOSTNAME: true

  ## TODO: 初始注册时将设为管理员和开发者的逗号分隔的电子邮件列表
  ## 示例 '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

  ## 此 Discourse 实例的 HTTP 或 HTTPS CDN 地址(配置为拉取)
  ## 详见 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 "开始自定义命令"
  ## 如果你想设置首次注册的“发件人”电子邮件地址,请取消注释并修改:
  ## 收到第一封注册邮件后,请重新注释该行。只需运行一次。
  #- exec: rails r "SiteSetting.notification_email='info@unconfigured.discourse.org'"
  - exec: echo "自定义命令结束"

2.2 设置

创建一个如下所示的 setup 脚本:

#!/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 socket 是否正常:

root@docker-s-1vcpu-2gb-sgp1-01:~# docker ps
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS              PORTS               NAMES
9702f94ea9b4        local_discourse/bbs   "/sbin/boot"        9 hours ago         Up 9 hours                              bbs
dc13c303c38e        local_discourse/cp    "/sbin/boot"        9 hours ago         Up 9 hours                              cp
dafa592ee16f        local_discourse/ytb   "/sbin/boot"        9 hours ago         Up 9 hours                              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;

未来我会告诉你如何在外部 Nginx 中使用 Let’s Encrypt 自动生成的证书和密钥。

events {
  worker_connections 1024;
}

http {
  # "auto_ssl" 共享字典应定义足够的存储空间来
  # 保存你的证书数据。1MB 的存储空间可容纳约
  # 100 个独立域的证书。
  lua_shared_dict auto_ssl 1m;
  # "auto_ssl_settings" 共享字典用于临时存储各种设置
  # 例如端口 8999 上的钩子服务器使用的密钥。请勿更改或
  # 省略它。
  lua_shared_dict auto_ssl_settings 64k;

  # 必须定义 DNS 解析器才能使 OCSP 装订功能正常工作。
  #
  # 此示例使用 Google 的 DNS 服务器。你可能想使用系统的
  # 默认 DNS 服务器,可在 /etc/resolv.conf 中找到。如果你的网络
  # 不支持 IPv6,你可能希望使用 "ipv6=off" 标志禁用 IPv6 结果
  # (例如 "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. 你将得到你想要的结果

Woola!

这似乎是现有 Discourse 多站点功能的一个更复杂的版本。我很好奇,运行三个独立的容器争夺相同的资源,相比运行一个(或者在 Web 和数据分离的情况下运行两个)容器,究竟有什么优势?

这完全是预算问题。你可以将其视为原型测试阶段,用于验证客户和营销策略。

即使在预算阶段,我认为多站点实施也会更加简单和实用。这样,我在配置方面需要担心的事情更少,从而有更多时间专注于客户/用户需求。

这一切都是因为我想要在单机服务器上同时运行测试环境和生产环境。如果宿主机的 80 和 443 端口已被占用,我就没有其他办法来实现这一切了。

非常感谢分享,但我觉得这还需要大量细节才能成为#教程,正在重新分类。