작성일: 2026-07-23 목적: 앞선 라이센스 조사에서 내린 "Team Edition으로 간다"는 결정을 실제로 집행한 기록. 대상: 로컬(M1 Mac) 개발 인스턴스와 Ubuntu 운영 서버 양쪽. 버전은 양쪽 다 11.7.0. 성격: 작업 기록. 절차보다 왜 이게 안전한가에 무게를 뒀다.


1. 배경 — 결정은 이미 내려져 있었다

앞선 조사(2026-07-22-01)에서 도달한 결론은 "AI/RAG 중심이고 SSO가 필수가 아니면 Team Edition이 Entry보다 유리하다"였다. Entry의 10,000 메시지 캡은 RAG 수집에 치명적인 반면 Team은 히스토리가 무제한이기 때문이다. 이 글은 그 결정을 실제로 집행한 기록이다.

1.1 전환 전 상태 — "Entry인 줄 알았는데 아니었다"

작업 시작 시점의 인식은 "Entry Edition이 설치되어 있다"였다. 확인해보니 달랐다.

확인 항목 로컬 운영 서버
이미지 mattermost-enterprise-edition:11.7.0 동일
licenses 테이블 행 수 0 0
LicenseFileLocation "" ""
기동 시 라이센스 경고 0건 0건

Entry 라이센스가 등록된 상태가 아니라, 라이센스가 아예 없는 Enterprise 빌드였다. Entry는 Enterprise 바이너리로 구동되기 때문에 이미지 이름만 봐서는 구분이 안 된다. 구분하려면 licenses 테이블을 봐야 한다.

이 사실이 전환을 안전하게 만든 결정적 조건이 된다. 뒤에서 다시 다룬다.


2. 전환의 실체 — 바꾸는 것은 환경변수 한 줄

공식 mattermost/docker 저장소의 compose는 이미지를 변수로 참조한다.

mattermost:
  image: mattermost/${MATTERMOST_IMAGE}:${MATTERMOST_IMAGE_TAG}

그래서 compose 파일은 손댈 필요가 없다. .env의 한 줄이 전부다.

- MATTERMOST_IMAGE=mattermost-enterprise-edition
+ MATTERMOST_IMAGE=mattermost-team-edition
  MATTERMOST_IMAGE_TAG=11.7.0        # 버전은 그대로 유지

사전에 mattermost/mattermost-team-edition:11.7.0이 Docker Hub의 linux/amd64에 존재하는지 확인했다. 로컬 compose가 platform: linux/amd64로 핀돼 있어 태그가 없으면 그 자리에서 막힌다.


3. 왜 데이터가 보존되는가

전환에서 가장 불안한 지점은 "컨테이너를 재생성하는데 데이터가 괜찮은가"다. 답은 상태가 어디에 사는지를 보면 나온다.

3.1 상태가 이미지 안에 하나도 없다

┌─ 이미지 (읽기 전용, 교체 대상) ─┐   ← EE → TE 로 바뀐 건 여기뿐
├─ 컨테이너 (일회용, 재생성됨) ───┤   ← up -d 가 파괴 후 재생성
└─ 바인드 마운트 (호스트 디스크) ─┘   ← 손대지 않음

실제 마운트를 보면 Mattermost 상태 중 이미지 안에 있는 것이 하나도 없다.

호스트 경로 컨테이너 경로 담긴 것
volumes/app/mattermost/config /mattermost/config config.json
volumes/app/mattermost/data /mattermost/data 첨부파일
volumes/app/mattermost/plugins /mattermost/plugins 플러그인
volumes/db/var/lib/postgresql/data /var/lib/postgresql DB 전체

image: 한 줄은 "어떤 바이너리를 실행할지"만 결정한다. "데이터를 어디서 읽을지"는 volumes:가 결정하고, 그건 건드리지 않았다. 그래서 컨테이너 재생성은 데이터 손실 사건이 아니라 프로세스 교체 사건이다. 실제로 파괴적인 명령은 볼륨까지 지우는 down -v뿐이다.

PG18 경로 함정: 실제 PGDATA는 volumes/db/var/lib/postgresql/data/18/docker다. data/ 바로 아래에 PG_VERSION이 없어서 "데이터가 사라졌다"고 오해하기 쉽다.

3.2 EE와 TE가 같은 DB를 읽는 이유

EE와 TE는 다른 제품이 아니라 같은 소스 트리의 두 빌드다. EE는 코어에 엔터프라이즈 모듈을 추가로 컴파일해 넣은 것이고, DB 스키마는 코어가 정의한다. TE 기동 로그가 이걸 그대로 보여준다.

"build_hash_enterprise":"none"
"Team Edition Build","enterprise_build":false
"Current version is 11.7.0"

마이그레이션 상태는 전환 전후 모두 version 171로 같았다. TE 바이너리가 기대 버전과 DB 상태가 일치함을 확인하고 DDL을 한 줄도 실행하지 않았다. 로그에 ALTER TABLE이나 CREATE TABLE이 없고, "Worker started", worker_name:"Migrations"는 잡 워커가 대기 상태로 뜬 것이지 스키마 변경이 아니다.

여기서 1.1의 발견이 다시 등장한다. 무라이센스였기 때문에 엔터프라이즈 기능이 잠겨 있었고, 따라서 TE가 읽지 못할 EE 전용 데이터가 애초에 쌓이지 않았다. 유료 라이센스로 LDAP이나 컴플라이언스를 실제로 쓰고 있었다면 이야기가 완전히 달라졌을 것이다.

위험한 것은 에디션 차이가 아니라 버전 차이다. TE 10.x로 내렸다면 DB는 171인데 바이너리는 더 낮은 버전을 기대해 스키마 불일치가 났을 것이다.

3.3 Postgres는 컨테이너조차 재생성되지 않았다

컨테이너 Created Started
postgres 9일 전 그대로 전환 시각
mattermost 전환 시각 (신규) 전환 시각

up -d는 이미지가 바뀐 mattermost만 재생성한다. 데이터 이관(migration)은 일어나지 않았다. "전환"이라는 말이 크게 들리지만 실제 변경 범위는 컨테이너 하나였다.

3.4 config.json은 재작성됐지만 바이트 단위로 동일

전환 후 config.json의 mtime이 갱신돼 있어 덮어쓴 것처럼 보였다. 백업본과 비교해보니 내용은 완전히 같았다.

EE 백업: 26059 bytes  sha 2d53ca01ab62
TE 현재: 26059 bytes  sha 2d53ca01ab62

Mattermost는 부팅할 때 config.json을 읽어 누락된 키에 기본값을 채우고 다시 저장한다. EE(무라이센스)와 TE의 설정 스키마가 같아서 채울 것이 없었고, 결과적으로 재작성이 no-op이 됐다. 설정이 살아남은 것은 운이 아니라 스키마가 같았기 때문이다.


4. Hermes 연동이 끊기지 않는 구조

Hermes Agent는 채널별 봇으로 Mattermost에 붙는다. 연결은 토큰 하나로 이어진 두 개의 저장소이고, 둘 다 Mattermost 이미지 바깥에 있다.

Hermes 쪽                              Mattermost 쪽
~/.hermes → /opt/data                  Postgres (바인드 마운트)
  profiles/<name>/.env                   useraccesstokens 테이블
    MATTERMOST_TOKEN=...          ←─→     같은 토큰 행 (isactive=t)
    MATTERMOST_URL=...

끊기지 않은 이유는 세 가지다.

  1. Hermes 컨테이너를 아예 건드리지 않았다. 양쪽 모두 재시작조차 하지 않아 uptime이 유지됐다.
  2. 토큰 검증 주체가 DB다. 새 TE 바이너리가 같은 useraccesstokens 행을 조회하므로 같은 Bearer 토큰이 그대로 인증된다. 토큰 재발급이 필요 없었던 이유다.
  3. 엔드포인트가 안 변했다. 포트 발행은 compose의 APP_PORT가 결정하지 이미지가 결정하지 않는다.

봇 토큰은 DB에 저장된 자격증명이지 서버 바이너리에 박힌 값이 아니다. 그래서 바이너리를 갈아끼워도 인증이 유지된다.

4.1 환경별 접근 경로 차이가 최대 함정

로컬(M1) 운영 서버(Ubuntu)
Hermes → MM 주소 http://host.docker.internal:8065/ http://mattermost:8065/
Docker 네트워크 의존 없음 (호스트 경유) 있음 (mattermost 네트워크 합류)
docker compose down 안전 금지

운영 서버의 mattermost 네트워크에는 Mattermost, Postgres, nginx 외에 Hermes를 포함한 외부 컨테이너 3개가 합류해 있었다. 볼륨과 달리 네트워크는 down이 삭제한다. 한 번 실수하면 셋이 동시에 떨어져 나간다.


5. 로컬(M1) 전환

.envCOMPOSE_FILE이 설정돼 있어 docker compose 맨손 호출로 동작하는 환경이다.

# 1) 논리 DB 백업 (무중단)
docker compose exec -T postgres pg_dump -U <POSTGRES_USER> <POSTGRES_DB> > db-backup-ee-11.7.0.sql

# 2) 정지 후 볼륨 백업 (down이 아니라 stop)
docker compose stop
cp -a volumes volumes.backup-ee-11.7.0

# 3) .env 변경 후 교체
docker compose pull
docker compose up -d

검증 결과는 전후 완전 일치였다.

항목
사용자/채널/게시물/팀 10 / 12 / 147 / 1 동일
봇 계정 / 액세스 토큰 8 / 5 8 / 5
백업 무결성 파일 2033개, 체크섬 일치

Hermes 게이트웨이 5개(default, sales, finance, dev, qna)가 전원 자동 재연결했다. 어댑터의 재연결 주기가 60초라 붙기까지 약 1분 걸렸다.


6. 운영 서버 전환 — 로컬과 다른 네 가지

로컬 절차를 그대로 가져가면 깨지는 지점이 있다.

# 차이 대응
1 COMPOSE_FILE.env에 없음 모든 명령에 -f 두 개를 붙임
2 Hermes가 Docker 네트워크에 합류 down 금지, up -d
3 nginx + Let's Encrypt 운영 중 --no-deps mattermost로 격리
4 공인 도메인 = 실사용자 존재 이미지 사전 pull로 다운타임 단축

1번이 가장 위험했다. 네트워크 정의가 nginx override 파일 안에 들어 있어서, -f를 빠뜨리고 맨손으로 docker compose up -d를 치면 다른 이름의 네트워크가 생성되고 Hermes가 mattermost:8065를 못 찾게 된다.

# docker-compose.nginx.yml 안에 있다
networks:
  default:
    name: mattermost

6.1 실행

MMC="docker compose -f docker-compose.yml -f docker-compose.nginx.yml"

# 무중단 사전 준비
$MMC exec -T postgres pg_dump -U <POSTGRES_USER> <POSTGRES_DB> > db-backup-ee-11.7.0.sql
cp -a volumes volumes.backup-ee-11.7.0                    # 실행 중 복사
docker pull mattermost/mattermost-team-edition:11.7.0     # 다운타임 단축의 핵심

# .env 변경 (.env.bak 자동 생성)
sed -i.bak 's|^MATTERMOST_IMAGE=mattermost-enterprise-edition$|MATTERMOST_IMAGE=mattermost-team-edition|' .env

# 적용 전 필수 검증 — 둘 다 확인될 때만 진행
$MMC config | grep -E "image:"          # → mattermost-team-edition:11.7.0
$MMC config | grep -A3 "^networks:"     # → name: mattermost

# mattermost 컨테이너만 교체
$MMC up -d --no-deps mattermost

볼륨 백업을 서비스 실행 중에 뜬 이유가 있다. Postgres 파일 복사본은 찢어질 수 있지만, 권위 있는 DB 백업은 pg_dump 결과물이고 이번 작업은 Postgres를 건드리지 않는다. 이 판단 덕분에 백업 다운타임이 0이 됐다.

6.2 타임라인

06:01:29  $MMC up -d --no-deps mattermost
06:01:36  ← Hermes 게이트웨이 6개 전부 재연결 (7초)
06:02:01  healthcheck healthy 확정

총 다운타임 32초. 로컬(약 3분)보다 훨씬 짧은데, 이미지를 미리 받아둔 것과 amd64 네이티브라 Rosetta 에뮬레이션이 없는 것이 겹친 결과다.

흥미로운 건 Hermes가 healthy 판정보다 먼저 붙었다는 점이다. Mattermost는 healthcheck 통과 전에 이미 WebSocket을 수락한다. 체감 단절은 32초보다 짧았다.

6.3 검증 결과

항목
사용자/채널/게시물/팀 17 / 22 / 698 / 2 동일
봇 계정 / 활성 토큰 10 / 6 10 / 6
nginx Up 9 days Up 9 days (재시작 없음)
HTTPS 200 200
네트워크 합류 컨테이너 6개 6개 전원 유지

Hermes 게이트웨이 6개가 전부 WebSocket connected and authenticated를 찍었다. 이후 정리 작업 내내 PID가 불변이었던 것으로 재시작이 없었음도 확인했다.

플러그인 활성 결과는 이랬다.

Plugin activated      → com.mattermost.calls
Plugin activated      → jira
Plugin activated      → mattermost-ai
Unable to activate    → playbooks     ← 유일한 실패

jira가 통과한 것이 이번 작업의 마지막 미검증 변수였다. 로컬에는 없던 플러그인이라 TE에서 동작할지 실증하지 못한 상태로 넘어갔는데, 문제없이 활성화됐다.


7. 유일한 손실 — Playbooks

"Unable to activate plugin", plugin_id:"playbooks",
error: "this plugin requires a professional license or higher"

이게 전환 때문인지 원래 그랬는지를 확정해야 했다. 볼륨을 통째로 백업해뒀기 때문에 EE 시절 로그가 백업 안에 남아 있었고, 대조가 가능했다.

EE 시절 (백업 로그) TE 현재
playbooks 정상 구동 (플러그인 프로세스·폴러 동작) 활성화 실패
라이센스 경고 0건 2건

전환이 원인이 맞다. config.json의 playbooks: {Enable: true} 설정 자체는 보존됐고, 바이너리가 라이센스 검사에서 막는 구조다. 설정은 남고 기능만 사라진 경우다.

앞서 "무라이센스였으니 EE 전용 기능은 어차피 잠겨 있었다"고 전제했는데, Playbooks는 무라이센스 EE에서 동작하던 기능이라 그 전제가 이 항목에는 맞지 않았다.

다만 실사용량이 0이라 실피해는 없었다.

SELECT (SELECT count(*) FROM ir_playbook) playbooks, (SELECT count(*) FROM ir_incident) runs;
--  0 | 0

테이블이 존재한다고 쓰고 있는 것은 아니다. 플러그인이 설치되면 안 쓰더라도 스키마는 만들어진다. 행 수까지 확인해야 판단할 수 있다.

운영 서버는 재시작 없이 비활성화했다.

$MMC exec -T mattermost mmctl --local plugin disable playbooks
# → Disabled plugin: playbooks

mmctl --local을 쓴 이유는 config.json을 직접 편집하면 반영에 컨테이너 재시작이 필요해 또 다운타임이 생기기 때문이다. 활성화에 실패한 플러그인이라도 비활성화 명령은 정상 수락된다.


8. 정리 작업

EE 이미지(1.06GB)를 지웠더니 디스크가 1.8GB 줄었다. EE 전용 레이어가 다른 이미지와 공유되지 않아 함께 회수됐기 때문이다. TE 이미지는 611MB로, 445MB 차이가 곧 빌드에서 빠진 엔터프라이즈 코드의 실체다.

.gitignore에는 백업 산출물 패턴을 추가했다.

volumes.backup-*
*-backup-*.sql
db-backup-*.sql
.env.bak          # .env와 같은 시크릿이 들어있다

기존 .gitignorevolumes 패턴은 정확히 volumes라는 이름만 매칭하므로 volumes.backup-ee-11.7.0은 걸리지 않는다. 별도 패턴이 필요하다. sed -i.bak이 만든 .env.bak도 잊기 쉬운데, .env와 동일한 시크릿이 들어있어 반드시 제외해야 한다.

백업 자체는 지우지 않고 며칠 지켜보기로 했다. 지금 지우면 되돌릴 수단이 SQL 덤프 하나만 남는다.


9. 함정 정리

다음에 또 할 때를 위한 목록이다.

  1. docker compose down을 쓰지 말 것. 네트워크가 삭제되면 거기 합류한 외부 컨테이너가 전부 분리된다. stop 또는 up -d를 쓴다.
  2. COMPOSE_FILE.env에 있는지 먼저 확인. 없으면 -f를 모든 명령에 붙여야 하고, 빠뜨리면 네트워크 이름이 바뀐다.
  3. 적용 전 compose config로 두 가지를 동시에 확인. 이미지가 TE인지, 네트워크 이름이 유지되는지.
  4. 이미지를 미리 pull하면 다운타임이 수 분에서 수십 초로 줄어든다.
  5. --no-deps <service>로 폭발 반경을 좁힌다. nginx와 postgres를 건드리지 않는다.
  6. 버전은 반드시 동일하게 유지. 위험한 건 에디션 차이가 아니라 버전 차이다.
  7. 플러그인은 "존재"가 아니라 "사용량"으로 판단한다. 테이블이 있다고 쓰는 게 아니다.
  8. 볼륨 백업이 로그 백업을 겸한다. 전환 전후 동작을 대조할 수 있는 유일한 근거가 됐다.

10. 결론

로컬(M1) 운영 서버
에디션 TE 11.7.0 TE 11.7.0
다운타임 약 3분 32초
데이터 전량 보존 전량 보존
Hermes 5개 프로필 재연결 6개 프로필 재연결
실질 손실 Playbooks(미사용) Playbooks(미사용, 비활성화 완료)

전환의 본질은 바이너리 교체이지 데이터 이관이 아니다. 상태가 전부 이미지 바깥에 있고 EE와 TE가 같은 버전에서 같은 스키마를 공유하기 때문에, 실제 변경 범위는 mattermost 컨테이너 하나로 좁혀진다. 무라이센스 상태였다는 점이 EE 전용 데이터 유실 위험을 원천적으로 없앴다.

이제 Team Edition 베이스가 확보됐으므로, 앞선 조사에서 남겨둔 과제로 넘어갈 수 있다. 히스토리 무제한이라 롤오프를 걱정하지 않아도 되는 조건에서 WebSocket 수집과 RAG 인덱싱 파이프라인을 설계하는 일이다.

관련 문서