단일 Discourse 사이트용 여러 앱 컨테이너

단일 서버에서 Discourse의 독립 설치(multisite 미사용)를 여러 개 호스팅할 수 있습니다 (별도 컨테이너 / 별도 포트 / 별도 app.yml).

multisite보다 수동적인 작업이 필요하지만, 인스턴스 간 격리를 유지하고 나중에 개별 사이트를 전용 서버로 이전하는 것이 더 쉬워집니다.

실용적인 패턴 중 하나는 다음과 같습니다:

• 외부 Postgres (단일 인스턴스)
• 외부 Redis (단일 인스턴스)
• 여러 Discourse 웹 컨테이너
• 단일 Sidekiq 노드
• 헬스 체크가 포함된 리버스 프록시

이 방식은 multisite를 완전히 피하면서 저트래픽 환경에서도 비용 절감을 허용합니다.



DISCOURSE 멀티 컨테이너 런북
외부 Postgres + 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 네트워크 (필수)

컨테이너가 이름으로 서로를 해결(resolve)할 수 있도록 사용자 정의 Docker 네트워크가 필요합니다.

단계 명령어
네트워크 생성 docker network create discourse-net
확인 docker network ls | grep discourse-net

이로 인해 다음이 올바르게 작동합니다:

• DISCOURSE_DB_HOST=pg
• DISCOURSE_REDIS_HOST=redis


  1. 시크릿(비밀번호)
목적 명령어
Postgres 슈퍼유저 export PG_SUPERPASS='REPLACE_ME_super_strong'
Discourse DB 비밀번호 export DISCOURSE_DBPASS='REPLACE_ME_discordb_strong'
Redis 비밀번호 export REDIS_PASS='REPLACE_ME_redis_strong'
시크릿 키 베이스 export SECRET_KEY_BASE="$(openssl rand -hex 64)"

  1. POSTGRES 컨테이너
단계 명령어
디렉터리 생성 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';"
DB 생성 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 only
• 포트 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
sidekiq app1 docker exec -it app1 pgrep -fa sidekiq
sidekiq app2 `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

END

Docker 네트워크 필수
외부 Postgres 및 Redis
pgvector 설치됨
Sidekiq가 app1에 격리됨
HAProxy 헬스 체크 활성화됨
유지보수 폴백 활성화됨
롤링 리빌드 지원됨

나중에 한 사이트를 전용 서버로 이전하는 방법

완전히 독립적인 Discourse 설치(multisite가 아닌)를 실행하는 것의 이점 중 하나는 이전이 단순하고 위험이 낮다는 것입니다.

각 Discourse 인스턴스는 이미 다음을 가지고 있습니다:

• 자체 컨테이너
• 자체 업로드
• 자체 데이터베이스
• 자체 Redis 사용
• 자체 app.yml

multisite를 분리하는 작업은 필요하지 않습니다.


고수준 이전 단계

  1. 새 VPS 프로비저닝

새 서버에 Docker와 Discourse를 정상적으로 설치합니다.
multisite를 구성하지 마세요.


  1. 전체 백업 생성

소스 사이트에서:

관리자 → 백업 → 백업 생성

백업 파일을 다운로드합니다.

이것은 다음을 포함합니다:

• 데이터베이스
• 업로드
• 사용자
• 설정
• 테마


  1. 새 서버에서 복원

새 서버에서:

• 초기 설정 완료
• 관리자 로그인
• 백업 업로드
• 복원

Discourse는 스키마 호환성을 자동으로 처리합니다.


  1. DNS 전환

도메인의 A 레코드를 새 서버 IP를 가리키도록 업데이트합니다.

DNS가 전파되면 사용자는 투명하게 이동됩니다.


  1. 구 컨테이너 폐기

원래 서버에서:

• 구 컨테이너 중지
• 확신이 들면 제거

같은 호스트의 다른 Discourse 설치에는 영향을 주지 않습니다.


이것이 multisite보다 단순한 이유

multisite 환경에서는 이전 시 종종 다음이 필요합니다:

• 데이터베이스 분리
• 사이트별 데이터 추출
• multisite.yml 조정
• Sidekiq 재구성
• 업로드 및 이메일 재설정

독립 설치의 경우 이러한 작업은 전혀 필요하지 않습니다.

각 사이트는 이미 독립적입니다.


요약

이 접근 방식은 초기 운영 복잡성을 약간 감수하는 대가로
나중에 매우 단순한 분리를 제공합니다.

실험 또는 초기 단계 커뮤니티 구축 시 특히 잘 작동합니다.


이 접근 방식이 적합하지 않을 가능성이 있는 경우

다음과 같은 경우 이 설정은 일반적으로 좋은 아이디어가 아닙니다:

• 사이트가 초기부터 중등도 이상의 트래픽을 예상하는 경우
• 공식 Discourse 지원에 크게 의존하는 경우
• Docker, 네트워킹 또는 리버스 프록시 디버깅에 익숙하지 않은 경우
• 가동 시간 요구 사항이 엄격하거나 비즈니스에 중요한 경우
• 여러 사이트가 운영 면에서 긴밀하게 결합되어 있는 경우
• 모든 인스턴스에서 빈번한 플러그인 실험을 예상하는 경우

이러한 경우, 다음 중 하나가 운영상의 예상치 못한 문제를 줄이는 데 도움이 될 것입니다:

• 지원되는 multisite 설정
또는
• 서버당 하나의 Discourse 설치


중요한 참고 사항

이 접근 방식은 인프라 유연성을 증가시키지만,
관리자의 책임도 증가시킵니다.

전체 스택을 소유하고 간헐적인 장애를 학습 과정의 일부로 취급하는 데 익숙한 사람이 실행할 때 가장 잘 작동합니다.

안정성과 지원 가능성이 주요 목표라면,
지원되는 구성이 거의 항상 더 나은 선택입니다.

위 설정의 HAProxy 부분과 직접적으로 연결되는 추가 참고 사항이 하나 있습니다.

HAProxy와 Discourse를 함께 사용할 때 웹 컨테이너를 재빌드(예: ./launcher rebuild app1)하는 과정에서 HAProxy가 백엔드가 재시작되는 동안에도 해당 백엔드로 트래픽을 계속 전송하기 때문에 503 Service Unavailable 응답이 잠시 반환되는 일반적인 현상이 있습니다. 이는 Discourse 자체의 오류가 아니라, 재빌드 과정에서 백엔드가 일시적으로 사용할 수 없기 때문에 발생하는 것입니다.

권장되는 우회 방법은 HAProxy 관리 소켓을 사용하여 다음을 수행하는 것입니다:
1. 재빌드 전에 HAProxy에서 서버를 비활성화하고,
2. 재빌드가 완료된 후 다시 활성화하는 것입니다.

이렇게 하면 일시적인 503 오류를 방지할 수 있습니다.

이러한 동작과 우회 방법의 설명을 문서화한 기존 Meta 토론이 있습니다:

HAProxy를 롤링 재빌드에 사용하고 계신 분이라면, 해당 스레드에서 런북에 관리 소켓 명령이 포함된 이유에 대한 유용한 맥락을 파악할 수 있습니다.

저도 비슷한 방식으로 운영합니다. 각 사이트당 웹 전용 스타일의 단일 컨테이너를 사용하고, traefik(nginx-proxy를 사용한 설정도 가지고 있습니다)을 리버스 프록시로 사용했습니다. HAproxy를 잠시 사용해보았지만(제가 아는 바로는 CDCK가 사용하는 것이었습니다), 번거롭게 느껴졌습니다.

각 Discourse 서버마다 하나의 redis가 필요하다고 확신합니다.

여기 용어에 약간의 불일치가 있는 것 같습니다.

“Discourse 서버당 Redis 하나”라고 하셨을 때, 여기서 서버가 하나의 논리적 Discourse 사이트를 의미한다면 동의합니다.

제 환경의 경우:

  • HAProxy는 오직 페일오버/프론팅(froning) 용도로만 사용되고 있습니다.
  • 멀티사이트 구성이 없습니다.
  • Discourse 사이트가 하나뿐입니다 (호스트네임 하나, Postgres DB 하나).
  • 해당 사이트를 제공할 수 있는 앱 컨테이너가 두 개 있을 뿐입니다.

따라서 이는 두 개의 독립적인 Discourse 설치 환경이 아니라, 멀티 웹/HA(고가용성) 구성에 더 가깝습니다.

이러한 구성에서는 Redis 공유가 예상되며 필수적입니다. 그렇지 않으면 다음을 잃게 됩니다:

  • 공유 세션
  • MessageBus 전달
  • 레이트 리미팅
  • 백그라운드 작업 조정

이는 여러 web_only 컨테이너를 실행하거나 웹 워커를 수평으로 확장하는 것과 동일한 패턴입니다:
여러 앱 컨테이너 → Postgres 하나 + Redis 하나.

Redis를 공유해서는 안 되는 경우는 두 개의 별개의 Discourse 사이트(서로 다른 호스트네임/데이터베이스)가 있을 때입니다. 이 경우 키 충돌을 피하기 위해 각 사이트는 자체 Redis DB(또는 인스턴스)가 필요합니다.

따라서 개념적으로는 우리가 일치한다고 생각합니다. 다만:

  • :white_check_mark: Discourse 사이트당 Redis 하나
  • :cross_mark: 개별 앱 컨테이너당 Redis 하나는 아닙니다.

무엇을 오해했다면 추가로 설명해 드릴 수 있습니다. 토폴로지를 더 명확히 설명하고 싶었을 뿐입니다.

아. 우리가 이야기하고 있다고 생각했던 것과 정반대네요. 제목은 "2개 디스코urs 커뮤니티를 위한 서버 1대"인데, 당신은 "1개 디스코urs 커뮤니티를 위한 서버 2대"에 대해 이야기하고 계시네요

맞습니다. 두 가지 다른 토폴로지를 혼동했었고, 스레드 제목이 그 단서였습니다.

이 주제는 “커뮤니티 2개를 위한 서버 1대”(두 개의 독립적인 사이트)에 대한 것입니다.
제가 앞서 언급했던 “외부 Redis(단일 인스턴스)” 코멘트는 다른 패턴, 즉 **“커뮤니티 1개를 위한 앱 컨테이너 2대”(단일 사이트의 HA/멀티 웹)”를 설명하는 것이었습니다.

따라서 명확히 다시 정리하면 다음과 같습니다:

A) 서버 1대에 독립적인 Discourse 사이트 2개 (OP가 질문한 내용)

  • 이를 두 개의 별개의 설치로 취급해야 합니다.
  • 별도의 Postgres DB와 별개의 Redis 인스턴스를 사용해야 합니다(또는 인용하신 MessageBus Pub/Sub의 함정 문제를 피할 수 있을 정도로 충분한 격리가 필요합니다).

B) 웹/앱 컨테이너가 여러 개인 Discourse 사이트 1개 (제가 설명하던 내용)

  • 해당 사이트의 Postgres DB와 Redis를 반드시 공유해야 합니다
    (세션, 레이트 리미팅, MessageBus 등).

즉: :white_check_mark: Redis에 대한 경고는 A(커뮤니티 2개/사이트 2개)에 적용됩니다.
제 “Redis 공유” 언급은 B(여러 컨테이너로 확장된 커뮤니티 1개)에만 적용됩니다.

수정해 주셔서 감사합니다. 이후 런북/포스트 작성 시 두 경우를 명시적으로 분리해 두겠습니다.