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

You can host multiple standalone Discourse installs on a single server (separate containers / separate ports / separate app.yml), without using Discourse “multisite”.

It’s more manual than multisite, but it keeps instances isolated and makes it easier to migrate an individual site to its own server later.

One practical pattern is:

• external Postgres (single instance)
• external Redis (single instance)
• multiple Discourse web containers
• one Sidekiq node
• reverse proxy with health checks

This avoids multisite entirely while still allowing cost savings on low-traffic setups.



DISCOURSE MULTI-CONTAINER RUNBOOK
External Postgres + Redis + HAProxy + app1 / app2


  1. HOST PACKAGES
Step Command
Update system apt-get update
Install base tools apt-get install -y ca-certificates curl gnupg lsb-release
Install HAProxy + certbot + socat apt-get install -y haproxy certbot socat

  1. DOCKER NETWORK (REQUIRED)

A user-defined Docker network is required so containers can resolve each other by name.

Step Command
Create network docker network create discourse-net
Verify docker network ls | grep discourse-net

This allows:

• DISCOURSE_DB_HOST=pg
• DISCOURSE_REDIS_HOST=redis

to work correctly.


  1. SECRETS
Purpose Command
Postgres superuser export PG_SUPERPASS='REPLACE_ME_super_strong'
Discourse DB password export DISCOURSE_DBPASS='REPLACE_ME_discordb_strong'
Redis password export REDIS_PASS='REPLACE_ME_redis_strong'
Secret key base export SECRET_KEY_BASE="$(openssl rand -hex 64)"

  1. POSTGRES CONTAINER
Step Command
Create directory mkdir -p /var/discourse/external/postgres
Run container 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
Verify docker ps | grep pg

  1. CREATE DATABASE
Step Command
Create role docker exec -it pg psql -U postgres -c "CREATE ROLE discourse LOGIN PASSWORD '$DISCOURSE_DBPASS';"
Create DB docker exec -it pg psql -U postgres -c "CREATE DATABASE discourse OWNER discourse ENCODING 'UTF8' TEMPLATE template0;"
Text search docker exec -it pg psql -U postgres -d discourse -c "ALTER DATABASE discourse SET default_text_search_config = 'pg_catalog.english';"
Test login docker exec -it pg psql -U discourse -d discourse -c "select 1;"

  1. PGVECTOR EXTENSION

Required for modern Discourse versions.

Step Command
Install docker exec -it pg bash -lc 'apt-get update && apt-get install -y postgresql-15-pgvector && rm -rf /var/lib/apt/lists/*'
Create extension docker exec -it pg psql -U postgres -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;"
Verify docker exec -it pg psql -U postgres -d discourse -c "SELECT extname FROM pg_extension WHERE extname='vector';"

  1. REDIS CONTAINER
Step Command
Create directory mkdir -p /var/discourse/external/redis

Redis config template:

requirepass REPLACE_ME_REDIS
appendonly yes
save 900 1
save 300 10
save 60 10000
Step Command
Write config tee /var/discourse/external/redis/redis.conf >/dev/null <<EOF
Insert password sed -i "s/REPLACE_ME_REDIS/$REDIS_PASS/" /var/discourse/external/redis/redis.conf
Run 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
Test auth docker exec -it redis redis-cli -a "$REDIS_PASS" ping

  1. DISCOURSE DIRECTORY LAYOUT
Step Command
Create base dir mkdir -p /var/discourse
Enter cd /var/discourse
Clone repo git clone https://github.com/discourse/discourse_docker.git
Containers dir mkdir -p /var/discourse/containers
Shared logs mkdir -p /var/discourse/shared/web-only/log/var-log
Link containers ln -sfn /var/discourse/containers /var/discourse/discourse_docker/containers
Link launcher ln -sfn /var/discourse/discourse_docker/launcher /var/discourse/launcher

  1. APPLICATION CONTAINERS

app1.yml
• web + sidekiq
• port 8001

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

app2.yml
• web only
• port 8002
• sidekiq disabled

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

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

  1. BOOTSTRAP
Step Command
Enter cd /var/discourse/discourse_docker
Bootstrap app1 ./launcher bootstrap app1
Start app1 ./launcher start app1
Bootstrap app2 ./launcher bootstrap app2
Start app2 ./launcher start app2

  1. HEALTH CHECKS
Step Command
app1 curl -sSf http://127.0.0.1:8001/srv/status
app2 curl -sSf http://127.0.0.1:8002/srv/status
sidekiq app1 docker exec -it app1 pgrep -fa sidekiq
sidekiq app2 `docker exec -it app2 pgrep -fa sidekiq

  1. TLS CERTIFICATE
Step Command
Stop proxy systemctl stop haproxy
Issue cert certbot certonly --standalone -d example.com --agree-tos -m you@example.com --non-interactive
Start proxy systemctl start haproxy

  1. HAPROXY LOGIC
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. ZERO-DOWNTIME REBUILDS
Step Command
Disable app1 echo "disable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Rebuild app1 ./launcher rebuild app1
Enable app1 echo "enable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Step Command
Disable app2 echo "disable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock
Rebuild app2 ./launcher rebuild app2
Enable app2 echo "enable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock

END

Docker networking required
External Postgres and Redis
pgvector installed
Sidekiq isolated to app1
HAProxy health checks enabled
Maintenance fallback active
Rolling rebuilds supported

Migrating one site to its own server later

One advantage of running fully standalone Discourse installs (instead of multisite) is that migration is straightforward and low-risk.

Each Discourse instance already has:

• its own container
• its own uploads
• its own database
• its own Redis usage
• its own app.yml

No multisite disentangling is required.


High-level migration steps

  1. Provision a new VPS

Install Docker and Discourse normally on the new server.
Do not configure multisite.


  1. Create a full backup

From the source site:

Admin → Backups → Create Backup

Download the backup file.

This includes:

• database
• uploads
• users
• settings
• themes


  1. Restore on the new server

On the new server:

• complete initial setup
• log in as admin
• upload the backup
• restore

Discourse handles schema compatibility automatically.


  1. DNS cutover

Update the domain’s A record to point to the new server IP.

Once DNS propagates, users are transparently moved.


  1. Decommission old container

On the original server:

• stop the old container
• remove it when confident

Other Discourse installs on the same host are unaffected.


Why this is simpler than multisite

In multisite setups, migration often requires:

• separating databases
• extracting site-specific data
• adjusting multisite.yml
• reworking Sidekiq
• reconfiguring uploads and email

With standalone installs, none of that is necessary.

Each site is already independent.


Summary

This approach trades a little operational complexity early on
for very simple separation later.

It works particularly well during experimentation
or early-stage community building.


When this approach is probably not a good fit

This setup is usually not a good idea if:

• the sites expect moderate or high traffic early on
• you rely heavily on official Discourse support
• you are uncomfortable debugging Docker, networking, or reverse proxies
• uptime requirements are strict or business-critical
• multiple sites are tightly coupled operationally
• you expect frequent plugin experimentation across all instances

In these cases, either:

• a supported multisite setup
or
• one Discourse install per server

will usually result in fewer operational surprises.


Important note

This approach increases infrastructure flexibility,
but also increases responsibility on the administrator.

It works best when the person running it is comfortable owning the full stack
and treating occasional breakage as part of the learning process.

If stability and supportability are the primary goals,
a supported configuration is almost always the better choice.

1 个赞

关于上述 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(一个社区跨多个容器扩展)。

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

1 个赞