単一の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部分に直接関連する追加の注意点が1つあります。

HAProxyとDiscourseでは、Webコンテナを再構築する際(例:./launcher rebuild app1)、HAProxyがバックエンドにトラフィックを送信し続けている間に再起動するため、一時的に503 Service Unavailableの応答が返されるという一般的な動作があります。これはDiscourse自体のエラーではなく、バックエンドが再構築中に一時的に利用できなくなるために発生します。

推奨される回避策は、HAProxy管理ソケットを使用して次の操作を行うことです。
\t1.\t再構築の前にHAProxyでサーバーを無効にする、および
\t2.\t再構築が完了した後に再度有効にする

これにより、一時的な503エラーを防ぐことができます。

この動作と回避策の説明を文書化した既存のMetaディスカッションがあります。

ローリング再構築にHAProxyを使用している方は、そのスレッドが、実行手順書に管理ソケットコマンドが含まれている理由について役立つ背景情報を提供しています。

私も同様のことを行っており、サイトごとにWeb専用スタイルのコンテナを一つ用意し、リバースプロキシとしてtraefik(ただし、nginx-proxyを使った設定も試したことがあります)を使用しています。一時的にHAproxy(私が知る限りCDCKが使用しているもの)を試しましたが、煩雑だと感じました。

Discourseサーバーごとに1つのRedisが必要だと確信しています。

ここで用語の不一致があるかもしれません。

「Discourse サーバーごとに 1 つの Redis」と言う場合、サーバーを 1 つの論理的な Discourse サイトと見なすのであれば、私も同意します。

私のケースでは:

  • HAProxy はフェイルオーバー/フロントのためだけに
    使用されています
  • マルチサイト構成はありません
  • Discourse サイトは 1 つだけです(単一のホスト名、単一の Postgres DB)
  • たまたま同じサイトを提供できる 2 つのアプリコンテナが存在するだけです

したがって、これはマルチウェブ/HA レイアウトに近く、2 つの独立した Discourse インストールではありません。

そのセットアップでは、Redis を共有することが期待されており、必要です。そうしないと、次のものが失われます。

  • 共有セッション
  • MessageBus 配信
  • レート制限
  • バックグラウンドジョブの調整

これは、複数の web_only コンテナを実行したり、ウェブワーカーを水平にスケーリングしたりする場合と同じパターンです。
複数のアプリコンテナ → 1 つの Postgres + 1 つの Redis。

Redis を共有してはならないのは、2 つの別々の Discourse サイト(異なるホスト名/データベース)がある場合です。その場合、キーの競合を避けるために、各サイトは独自の Redis DB(またはインスタンス)を必要とします。

したがって、概念的には一致していると思います。

  • :white_check_mark: Discourse サイトごとに 1 つの Redis
  • :cross_mark: 個々のアプリコンテナごとに 1 つの Redis

もし私が何か誤解しているようでしたら、さらに明確にさせていただきます。トポロジーをより明確に説明したかっただけです。

ああ。それは私が考えていたことと正反対ですね。タイトルは「1台のサーバーで2つのDiscourseコミュニティ」ですが、あなたは「1つのDiscourseコミュニティに2台のサーバー」について話しています。

おっしゃる通りです。2つの異なるトポロジーを混同していました。スレッドのタイトルがその手がかりになります。

このトピックは「1台のサーバーで2つのコミュニティ」(2つの独立したサイト)に関するものです。
私が以前述べた「外部Redis(単一インスタンス)」に関するコメントは、別のパターン、すなわち**「1つのコミュニティに対する2つのアプリコンテナ」**(単一サイトのHA/マルチウェブ)を説明していました。

したがって、明確に再述します。

A) 1台のサーバー上の2つの独立したDiscourseサイト(OPが尋ねていること)

  • 2つの別個のインストールとして扱います。
  • 別々のPostgres DBと別々のRedisインスタンスを持つ必要があります(または、引用された落とし穴であるMessageBus Pub/Subに対して十分な分離が必要です)。

B) 複数のウェブ/アプリコンテナを持つ1つのDiscourseサイト(私が説明していたこと)

  • そのサイトのために、同じPostgres DBと、同じRedisを共有しなければなりません(セッション、レート制限、MessageBusなど)。

したがって::white_check_mark: あなたのRedisに関する警告はA(2つのコミュニティ/2つのサイト)に適用されます。
私の「共有Redis」に関する注記は、B(複数のコンテナにまたがる1つのコミュニティのスケールアウト)にのみ適用されます。

訂正ありがとうございます。今後のランブックや投稿では、この2つのケースを明確に区別するようにします。

「いいね!」 1