单个 Discourse 站点使用多个应用容器

您可以在单个服务器上托管多个独立的 Discourse 安装(使用单独的容器/端口/ app.yml),而无需使用 Discourse 的“多站点”功能。

这比多站点模式更手动,但它能保持实例隔离,并使将来将单个站点迁移到独立服务器变得更加容易。

一种实用的模式是:

• 外部 PostgreSQL(单个实例)
• 外部 Redis(单个实例)
• 多个 Discourse Web 容器
• 一个 Sidekiq 节点
• 带健康检查的反向代理

这完全避免了多站点模式,同时仍能在低流量设置中实现成本节约。



DISCOURSE 多容器运行手册
外部 PostgreSQL + Redis + HAProxy + app1 / app2


  1. 主机软件包
步骤 命令
更新系统 apt-get update
安装基础工具 apt-get install -y ca-certificates curl gnupg lsb-release
安装 HAProxy + certbot + socat apt-get install -y haproxy certbot socat

  1. Docker 网络(必需)

需要用户定义的 Docker 网络,以便容器可以通过名称解析彼此。

步骤 命令
创建网络 docker network create discourse-net
验证 docker network ls | grep discourse-net

这将使以下配置正常工作:

• DISCOURSE_DB_HOST=pg
• DISCOURSE_REDIS_HOST=redis


  1. 密钥
用途 命令
PostgreSQL 超级用户 export PG_SUPERPASS='REPLACE_ME_super_strong'
Discourse 数据库密码 export DISCOURSE_DBPASS='REPLACE_ME_discordb_strong'
Redis 密码 export REDIS_PASS='REPLACE_ME_redis_strong'
密钥基 export SECRET_KEY_BASE="$(openssl rand -hex 64)"

  1. PostgreSQL 容器
步骤 命令
创建目录 mkdir -p /var/discourse/external/postgres
运行容器 docker run -d --name pg --restart=always --network=discourse-net -e POSTGRES_PASSWORD="$PG_SUPERPASS" -v /var/discourse/external/postgres:/var/lib/postgresql/data postgres:15
验证 docker ps | grep pg

  1. 创建数据库
步骤 命令
创建角色 docker exec -it pg psql -U postgres -c "CREATE ROLE discourse LOGIN PASSWORD '$DISCOURSE_DBPASS';"
创建数据库 docker exec -it pg psql -U postgres -c "CREATE DATABASE discourse OWNER discourse ENCODING 'UTF8' TEMPLATE template0;"
文本搜索 docker exec -it pg psql -U postgres -d discourse -c "ALTER DATABASE discourse SET default_text_search_config = 'pg_catalog.english';"
测试登录 docker exec -it pg psql -U discourse -d discourse -c "select 1;"

  1. PGVECTOR 扩展

现代 Discourse 版本需要此扩展。

步骤 命令
安装 docker exec -it pg bash -lc 'apt-get update && apt-get install -y postgresql-15-pgvector && rm -rf /var/lib/apt/lists/*'
创建扩展 docker exec -it pg psql -U postgres -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;"
验证 docker exec -it pg psql -U postgres -d discourse -c "SELECT extname FROM pg_extension WHERE extname='vector';"

  1. Redis 容器
步骤 命令
创建目录 mkdir -p /var/discourse/external/redis

Redis 配置模板:

requirepass REPLACE_ME_REDIS
appendonly yes
save 900 1
save 300 10
save 60 10000
步骤 命令
写入配置 tee /var/discourse/external/redis/redis.conf >/dev/null <<EOF
插入密码 sed -i "s/REPLACE_ME_REDIS/$REDIS_PASS/" /var/discourse/external/redis/redis.conf
运行 Redis docker run -d --name redis --restart=always --network=discourse-net -v /var/discourse/external/redis:/data -v /var/discourse/external/redis/redis.conf:/usr/local/etc/redis/redis.conf redis:7-alpine redis-server /usr/local/etc/redis/redis.conf
测试认证 docker exec -it redis redis-cli -a "$REDIS_PASS" ping

  1. Discourse 目录布局
步骤 命令
创建基础目录 mkdir -p /var/discourse
进入目录 cd /var/discourse
克隆仓库 git clone https://github.com/discourse/discourse_docker.git
容器目录 mkdir -p /var/discourse/containers
共享日志 mkdir -p /var/discourse/shared/web-only/log/var-log
链接容器 ln -sfn /var/discourse/containers /var/discourse/discourse_docker/containers
链接启动器 ln -sfn /var/discourse/discourse_docker/launcher /var/discourse/launcher

  1. 应用容器

app1.yml
• Web + Sidekiq
• 端口 8001

docker_args: "--network=discourse-net"
expose:
  - "8001:80"

app2.yml
• 仅 Web
• 端口 8002
• 禁用 Sidekiq

docker_args: "--network=discourse-net"
expose:
  - "8002:80"

run:
  - exec: bash -lc 'mkdir -p /etc/service/sidekiq && touch /etc/service/sidekiq/down'

  1. 引导启动
步骤 命令
进入目录 cd /var/discourse/discourse_docker
引导 app1 ./launcher bootstrap app1
启动 app1 ./launcher start app1
引导 app2 ./launcher bootstrap app2
启动 app2 ./launcher start app2

  1. 健康检查
步骤 命令
app1 curl -sSf http://127.0.0.1:8001/srv/status
app2 curl -sSf http://127.0.0.1:8002/srv/status
app1 的 Sidekiq docker exec -it app1 pgrep -fa sidekiq
app2 的 Sidekiq `docker exec -it app2 pgrep -fa sidekiq

  1. TLS 证书
步骤 命令
停止代理 systemctl stop haproxy
签发证书 certbot certonly --standalone -d example.com --agree-tos -m you@example.com --non-interactive
启动代理 systemctl start haproxy

  1. HAProxy 逻辑
frontend fe_discourse
    bind :80
    bind :443 ssl crt /etc/letsencrypt/live/example.com/haproxy.pem

    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    http-request set-header X-Forwarded-Proto http if !{ ssl_fc }

    redirect scheme https code 301 if !{ ssl_fc }

    use_backend be_discourse if { nbsrv(be_discourse) gt 0 }
    default_backend be_maint
backend be_discourse
    balance roundrobin
    option httpchk GET /srv/status
    server app1 127.0.0.1:8001 check
    server app2 127.0.0.1:8002 check
backend be_maint
    http-request return status 503 content-type text/html string "<h1>Maintenance</h1>"

  1. 零停机重建
步骤 命令
禁用 app1 echo "disable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
重建 app1 ./launcher rebuild app1
启用 app1 echo "enable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
步骤 命令
禁用 app2 echo "disable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock
重建 app2 ./launcher rebuild app2
启用 app2 echo "enable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock

结束

需要 Docker 网络
外部 PostgreSQL 和 Redis
已安装 pgvector
Sidekiq 隔离到 app1
已启用 HAProxy 健康检查
维护回退已激活
支持滚动重建

将来将单个站点迁移到独立服务器

运行完全独立的 Discourse 安装(而不是多站点)的一个优势是迁移简单且风险低。

每个 Discourse 实例已具备:

• 自己的容器
• 自己的上传文件
• 自己的数据库
• 自己的 Redis 使用
• 自己的 app.yml

无需进行多站点的解耦。


高层迁移步骤

  1. 配置新的 VPS

在新服务器上正常安装 Docker 和 Discourse。
不要配置多站点。


  1. 创建完整备份

从源站点:

管理员 → 备份 → 创建备份

下载备份文件。

这包括:

• 数据库
• 上传文件
• 用户
• 设置
• 主题


  1. 在新服务器上恢复

在新服务器上:

• 完成初始设置
• 以管理员身份登录
• 上传备份
• 执行恢复

Discourse 会自动处理模式兼容性。


  1. DNS 切换

更新域名的 A 记录,使其指向新服务器的 IP。

一旦 DNS 生效,用户将无缝迁移。


  1. 停用旧容器

在原始服务器上:

• 停止旧容器
• 确认无误后将其删除

同一主机上的其他 Discourse 安装不受影响。


为何这比多站点更简单

在多站点设置中,迁移通常需要进行:

• 分离数据库
• 提取站点特定数据
• 调整 multisite.yml
• 重新设计 Sidekiq
• 重新配置上传和邮件

对于独立安装,这些都不需要。

每个站点已经是独立的。


总结

此方法以早期稍高的运维复杂性为代价,换取了后期非常简单的分离。

它特别适合实验阶段或早期社区建设。


何时此方法可能不太合适

在以下情况下,此设置通常不是一个好主意:

• 站点预计早期就有中等或高流量
• 您严重依赖 Discourse 官方支持
• 您不习惯调试 Docker、网络或反向代理
• 正常运行时间要求严格或关乎业务关键
• 多个站点在运维上紧密耦合
• 您期望在所有实例上进行频繁的插件实验

在这些情况下,以下任一方案通常会导致更少的运维意外:

• 受支持的多站点设置

• 每个 Discourse 安装使用独立的服务器


重要提示

此方法增加了基础设施的灵活性,
但也增加了管理员的责任。

当操作人员能够掌控整个技术栈,
并将偶尔的故障视为学习过程的一部分时,此方法效果最佳。

如果稳定性和可支持性是主要目标,
那么受支持的配置几乎总是更好的选择。

关于上述 HAProxy 设置的另一个补充说明。

HAProxy + Discourse 存在一个常见行为,即重建 Web 容器(例如使用 ./launcher rebuild app1)时,会短暂返回 503 Service Unavailable 响应,因为 HAProxy 在后端重启期间仍在向其发送流量。这本身并不是 Discourse 中的错误——它发生是因为后端在重建过程中暂时不可用。

推荐的解决方法是使用 HAProxy 管理套接字来:

  1. 在重建之前禁用 HAProxy 中的服务器,以及
  2. 在重建完成后重新启用它

这可以防止瞬态 503 错误。

有一个现有的 Meta 讨论记录了这种行为以及对解决方法​​的解释:

如果这里有人对滚动重建使用 HAProxy,那么该帖子为 runbook 中包含管理套接字命令提供了有用的背景信息。

我做的事情类似,每个站点使用一个仅限 Web 样式的容器,并使用 traefik(尽管我也使用过 nginx-proxy)作为反向代理。我曾尝试使用 HAproxy 一段时间(据我所知,CDCK 也是这么做的),但觉得它很麻烦。

我非常确定您需要为每个 Discourse 服务器配置一个 Redis 实例。

我认为这里可能存在一些术语上的分歧。

当您说“每个 Discourse 服务器使用一个 Redis”时,如果服务器指的是一个逻辑上的 Discourse 站点,我表示同意。

在我的情况下:

  • HAProxy 仅用于故障转移/前端代理
  • 没有多站点配置
  • 只有一个 Discourse 站点(单个主机名,单个 Postgres 数据库)
  • 恰好有两个应用程序容器可以服务于同一个站点

因此,这更接近于多 Web/高可用性(HA)布局,而不是两个独立的 Discourse 安装。

在这种设置中,共享 Redis 是预期且必需的——否则您会丢失:

  • 共享会话
  • MessageBus 传递
  • 速率限制
  • 后台作业协调

当有两个独立的 Discourse 站点(不同的主机名/数据库)时,Redis 才不能共享。在这种情况下,每个站点都需要自己的 Redis 数据库(或实例)以避免键冲突。

所以我想我们在概念上是一致的——只是:

  • :white_check_mark: 每个 Discourse 站点一个 Redis
  • :cross_mark: 每个单独的应用程序容器一个 Redis

如果您有任何误解,我很高兴进一步澄清——只是想更清楚地解释一下拓扑结构。

哦。这与我以为我们在谈论的恰恰相反。标题是“一个服务器服务两个 Discourse 社区”,而您在谈论“两个服务器服务一个 Discourse 社区”。

你说得对——我混淆了两种不同的拓扑结构,而主题标题就是线索。

本主题是关于“一台服务器上的两个社区”(两个独立的站点)。
我早先提到的“外部 Redis(单实例)”评论描述的是另一种模式:“一个社区的两个应用容器”(单个站点的 HA/多 Web)。

所以,明确重申如下:

A) 一台服务器上的两个独立的 Discourse 站点(OP 询问的内容)

  • 将它们视为两个独立的安装
  • 它们应该有独立的 Postgres 数据库和独立的 Redis 实例(或者至少有足够的隔离度来处理 MessageBus 的发布/订阅,这就是你引用的难点)

B) 一个在多个 Web/应用容器上运行的 Discourse 站点(我之前描述的内容)

  • 它们必须共享同一个 Postgres 数据库和同一个 Redis 实例(用于会话、速率限制、MessageBus 等)

所以::white_check_mark: 你的 Redis 警告适用于 A(两个社区/两个站点)。
我的“共享 Redis”说明仅适用于 B(一个社区跨多个容器扩展)。

感谢你的指正——我会在后续的运行手册/帖子中明确区分这两种情况。