# Multiple app containers for a single Discourse site

**URL:** https://meta.discourse.org/t/multiple-app-containers-for-a-single-discourse-site/393913
**Category:** Self-hosting
**Tags:** hosting
**Created:** [January 19, 2026, 10:03am UTC](https://meta.discourse.org/t/multiple-app-containers-for-a-single-discourse-site/393913 "2026-01-19T10:03:43Z")
**Posts on this page:** 6
**Page:** 1

<div class="post-metadata">

### Author: ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)
#### Post date: [January 19, 2026, 10:03am UTC](https://meta.discourse.org/t/multiple-app-containers-for-a-single-discourse-site/393913/1 "2026-01-19T10:03:43Z")

</div>

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.

* * *

> [@](#):
>
> > **Context — why I’m posting this approach**
> >
> > The discussion above explored two different paths:
> > 
> > • Discourse multisite (shared Rails app)  
> > • Single server hosting multiple standalone Discourse installs
> > 
> > This post documents the second option.
> > 
> > The goal is not to recommend it as a default setup, but to show a practical pattern that:
> > 
> > • keeps instances isolated  
> > • avoids multisite coupling  
> > • allows later migration per site  
> > • works well for experimentation and early-stage communities
> > 
> > This is intentionally not multisite and does not use multisite.yml.
> 
> * * *
> 
> > **Support status and expectations**
> >
> > This approach sits outside the officially supported Discourse installation model.
> > 
> > It may involve:
> > 
> > • multiple standalone containers  
> > • shared external Postgres  
> > • shared external Redis  
> > • custom reverse proxy
> > 
> > Because of this, helpers may ask that issues be reproduced on a standard single-site install before deeper debugging.
> > 
> > This does not mean the configuration is invalid — only that support may be best-effort.
> 
> * * *
> 
> > **What would need to change for full support**
> >
> > To stay fully within supported configurations, one of the following would apply:
> > 
> > • multisite using multisite.yml  
> > • separate servers per Discourse install
> > 
> > Anything in between is technically valid, but not guaranteed supportable.
> 
> * * *
> 
> > **Why document this anyway**
> >
> > This pattern is still useful for:
> > 
> > • learning Discourse internals  
> > • staging environments  
> > • early experimentation  
> > • cost-controlled testing  
> > • administrators comfortable self-debugging
> > 
> > This documentation is shared for practical reference, not as a production recommendation.
> > 
> > * * *
> > 
> > ### Transport model (no Unix sockets)
> > 
> > This approach does not use Unix domain sockets.
> > 
> > All communication happens over TCP:
> > 
> > • Discourse → Postgres via pg:5432  
> > • Discourse → Redis via redis:6379  
> > • HAProxy → Discourse via host ports (8001, 8002)
> > 
> > This is intentional.
> > 
> > Using TCP avoids shared filesystem dependencies between containers and keeps each instance fully portable.
> > 
> > It also simplifies later migration to a separate server, since no socket paths or permissions need to be reproduced.
> > 
> > Unix sockets are commonly used in single-host, non-containerised setups, but offer little benefit in a Docker-based architecture like this one.

* * *

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:

```plaintext
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

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

```

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

```plaintext
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

```plaintext
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

```

```plaintext
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

```

```plaintext
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.

---

<div class="post-metadata">

### Author: ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)
#### Post date: [January 19, 2026, 10:09am UTC](https://meta.discourse.org/t/multiple-app-containers-for-a-single-discourse-site/393913/2 "2026-01-19T10:09:39Z")

</div>

one additional note that ties directly to the HAProxy part of the setup above.

There’s a common behavior with HAProxy + Discourse where rebuilding a web container (e.g. with ./launcher rebuild app1) will briefly return `503 Service Unavailable` responses because HAProxy is still sending traffic to that backend while it’s restarting. This isn’t an error in Discourse itself - it happens because the backend is momentarily unavailable during the rebuild.

The recommended workaround is to use the HAProxy admin socket to:  
1. disable the server in HAProxy before the rebuild, and  
2. re-enable it after the rebuild finishes

This prevents those transient 503s.

There’s an existing Meta discussion documenting this behavior and the explanation of the workaround:

> [@Transient 503s after ./launcher rebuild appN when they’re fronted by HAProxy](https://meta.discourse.org/t/transient-503s-after-launcher-rebuild-appn-when-they-re-fronted-by-haproxy/383179):
>
> I had created a test forum with two web\_only containers, the mail-receiver container, Postgres and Redis on a Docker network. The two web containers nginx’s were fronted by HAProxy. They worked well, but i found on rebuilding one of the two web containers there was always a small amount of downtime where 503 was returned. Fortunately, i found a mitigating workaround, but nothing perfect; before rebuilding app1, run echo "disable server be\_discourse/app1" | socat stdio /run/haproxy/admin.sock …

If anyone here is using HAProxy for rolling rebuilds, that thread provides useful context for why the admin socket commands are included in the runbook.

---

<div class="post-metadata">

### Author: ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)
#### Post date: [January 19, 2026, 4:05pm UTC](https://meta.discourse.org/t/multiple-app-containers-for-a-single-discourse-site/393913/3 "2026-01-19T16:05:52Z")

</div>

> [@Ethsim2](#):
>
> You can host multiple standalone Discourse installs on a single server (separate containers / separate ports / separate app.yml), without using Discourse “multisite”.

I do something similar, with a single web-only-style container per site and traefik (though I"ve also got a setup using nginx-proxy) as reverse proxy. I tried HAproxy for a while (it’s what CDCK uses, last I knew), but found it cumbersome.

> [@Ethsim2](#):
>
> • external Redis (single instance)

I am pretty sure that you need one redis per Discourse server.

> [@Multiple discourse on a single redis host](https://meta.discourse.org/t/multiple-discourse-on-a-single-redis-host/219318/2):
>
> Each Discourse needs its own totally separate redis server because message-bus uses [Redis Pub/Sub](https://redis.io/topics/pubsub), which is shared between all databases on a Redis server.

---

<div class="post-metadata">

### Author: ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)
#### Post date: [January 19, 2026, 4:20pm UTC](https://meta.discourse.org/t/multiple-app-containers-for-a-single-discourse-site/393913/4 "2026-01-19T16:20:44Z")

</div>

I think there may be a small terminology mismatch here.

When you say “one Redis per Discourse server”, I agree if by server we mean one logical Discourse site.

In my case:

- HAProxy is only being used for failover / fronting
- There is no multisite configuration
- There is only one Discourse site (single hostname, single Postgres DB)
- There just happen to be two app containers capable of serving that same site

So this is closer to a multi-web / HA layout, not two independent Discourse installs.

In that setup, sharing Redis is expected and required - otherwise you lose:

- shared sessions
- MessageBus delivery
- rate limiting
- background job coordination

This is the same pattern as running multiple `web_only` containers or horizontally scaling web workers:  
multiple app containers → one Postgres + one Redis.

Where Redis must not be shared is when there are two separate Discourse sites (different hostnames / databases). In that case, each site needs its own Redis DB (or instance) to avoid key collisions.

So I think we’re aligned conceptually - it’s just:

- ✅ one Redis per Discourse site
- ❌ not one Redis per individual app container

Happy to clarify further if I’ve misunderstood anything - just wanted to explain the topology more clearly

---

<div class="post-metadata">

### Author: ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)
#### Post date: [January 19, 2026, 4:46pm UTC](https://meta.discourse.org/t/multiple-app-containers-for-a-single-discourse-site/393913/5 "2026-01-19T16:46:12Z")

</div>

> [@Ethsim2](#):
>
> There just happen to be two app containers capable of serving that same site

Oh. That’s the opposite of what I thought we were talking about. The title is “One server for 2 discourse communties” You’re talking about “two servers for one Discourse commmunity”

---

<div class="post-metadata">

### Author: ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)
#### Post date: [January 19, 2026, 5:02pm UTC](https://meta.discourse.org/t/multiple-app-containers-for-a-single-discourse-site/393913/6 "2026-01-19T17:02:42Z")

</div>

You’re right - I conflated two different topologies, and the thread title is the giveaway.

**This topic is about “one server for 2 communities” (two independent sites).**  
My earlier “external Redis (single instance)” comment was describing a different pattern: **“two app containers for one community”** (HA / multi-web for a single site).

So to restate clearly:

### A) Two independent Discourse sites on one server (what the OP is asking)

- Treat them as **two separate installs**
- They should have **separate Postgres DBs and separate Redis instances** (or at least isolation sufficient for MessageBus Pub/Sub, which is the gotcha you quoted)

### B) One Discourse site with multiple web/app containers (what I was describing)

- They **must** share the same Postgres DB and the same Redis for that site  
(sessions, rate limiting, MessageBus, etc.)

So: ✅ your Redis warning applies to **A** (two communities / two sites).  
My “shared Redis” note only applies to **B** (one community scaled across multiple containers).

Thanks for the correction - I’ll keep the two cases explicitly separated in any follow-up runbooks/posts.
