공식
mattermost/docker저장소를 Ubuntu 리눅스 서버에 설치하고, nginx 리버스 프록시 + Let's Encrypt(HTTPS)로https://<도메인>서비스까지 올린 실제 과정을 정리한 문서입니다. 공식 저장소: https://github.com/mattermost/docker이 글의 실제 예시 환경
- 서버: Ubuntu (예:
your-server), amd64- 저장소 경로:
~/docker/- 도메인:
mattermost.example.com- Compose 구조:
docker-compose.yml(base) + override 파일 택1- 서비스: postgres + mattermost + nginx (3개 컨테이너)
0. 개요 — Compose 파일 구조 (base + override)
이 저장소는 base + override 패턴을 씁니다.
| 파일 | 역할 |
|---|---|
docker-compose.yml |
base. postgres + mattermost 정의. 포트 발행(ports:) 없음 |
docker-compose.without-nginx.yml |
override. APP_PORT:8065를 호스트에 직접 노출 (localhost/프록시 뒤 접속용) |
docker-compose.nginx.yml |
override. nginx 컨테이너를 붙여 80/443으로 HTTPS 서비스 |
핵심: base 파일만
docker compose up하면 mattermost는 컨테이너 내부에서만 8065를 열고 호스트로는 노출되지 않습니다. override를 반드시 함께 지정해야 합니다.
두 override는 "동시 사용"이 아니라 "택일"입니다.
- nginx 버전 — 서버 앞단에 아무 프록시도 없을 때. nginx가 443/80을 열고 TLS 종료 처리.
- without-nginx 버전 — 앞단에 이미 nginx/Traefik/클라우드 LB가 있어서 그쪽이 HTTPS를 담당할 때.
이 문서는 단독 서버 + nginx 버전(가장 흔한 케이스)을 기준으로 합니다.
최소 경로: 1 → 2 → 3 → 4 → 5 → 6
1. 사전 준비 (필수)
1-1. Docker 설치 확인
docker --version
docker compose version
없으면 Docker 공식 설치:
curl -fsSL https://get.docker.com | sh
M1 Mac과의 차이: Ubuntu(amd64)에서는
platform: linux/amd64에뮬레이션이 필요 없습니다. 이미지가 네이티브로 돕니다. compose 파일에platform: linux/amd64가 남아 있어도 amd64 서버에서는 무해하니 그대로 둬도 됩니다.
1-2. 도메인 · DNS · 방화벽 (HTTPS의 핵심 전제)
Let's Encrypt 발급이 되려면 아래가 반드시 충족돼야 합니다.
- 공인 도메인이 있을 것 (
localhost불가) - 도메인의 DNS A레코드가 이 서버의 공인 IP를 가리킬 것
- 서버의 80·443 포트가 외부 개방돼 있을 것
확인:
# 도메인이 가리키는 IP
dig +short mattermost.example.com
# 이 서버의 공인 IP — 위 결과와 같아야 함
curl -s ifconfig.me
방화벽(ufw 사용 시):
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8443/udp # Calls(음성/화상) 기능 사용 시
sudo ufw allow 8443/tcp
클라우드(AWS/GCP/Oracle 등)라면 보안 그룹/방화벽 규칙에서도 80·443을 열어야 합니다.
2. .env 설정
docker/ 폴더의 .env에서 도메인 관련 값을 실제 값으로 맞춥니다.
DOMAIN=mattermost.example.com
MM_SERVICESETTINGS_SITEURL=https://mattermost.example.com
HTTPS_PORT=443
HTTP_PORT=80
CALLS_PORT=8443
DOMAIN이localhost로 남아 있으면 HTTPS·SiteURL이 모두 깨집니다. 반드시 실제 도메인으로 바꾸세요. 인증서(CERT_PATH/KEY_PATH) 경로는 3번에서 발급 후 맞춥니다.
3. Let's Encrypt 인증서 발급
nginx 컨테이너는 .env의 CERT_PATH/KEY_PATH에 있는 인증서를 마운트합니다. 이 파일이
없거나 형식이 깨지면 nginx가 아래 에러로 기동 실패(재시작 루프) 합니다.
nginx: [emerg] cannot load certificate "/cert.pem":
PEM_read_bio_X509_AUX() failed
(SSL: error:0480006C:PEM routines::no start line:Expecting: TRUSTED CERTIFICATE)
read-only file system경고는 정상입니다(컨테이너가 read-only). 진짜 원인은 인증서입니다.
3-1. 발급 스크립트 사용법 확인
cd ~/docker
head -50 scripts/issue-certificate.sh
usage 요약:
Usage: issue-certificate.sh [-h] <-d DOMAIN> <-o PATH>
-d 인증서를 발급할 도메인 (예: mattermost.example.com)
-o 인증서를 저장할 출력 경로 (반드시 절대경로, 예: ${PWD}/certs)
⚠️ 자주 하는 실수:
-o는 출력 경로(PATH) 입니다. 이메일이 아닙니다.-o에 이메일을 넣으면 아래 에러가 납니다.includes invalid characters for a local volume name ... use absolute path이메일은 발급 도중 certbot이 대화형으로 물어봅니다.
3-2. 발급 실행
cd ~/docker
# nginx 컨테이너가 80을 점유 중이면 먼저 내림 (certbot이 80으로 도메인 검증)
docker compose -f docker-compose.yml -f docker-compose.nginx.yml stop nginx
# -o 는 절대경로. ${PWD}=/root/docker 이므로 .env의 ./certs 와 일치
bash scripts/issue-certificate.sh -d mattermost.example.com -o ${PWD}/certs
실행 중 certbot이 대화형으로 물어봅니다:
Enter email address→ 관리자 이메일 입력Agree to Terms of Service? (Y)→YShare email with EFF? (Y/N)→ 아무거나 (N무방)
성공 시:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/mattermost.example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/mattermost.example.com/privkey.pem
This certificate expires on 2026-10-11.
위 경로는 컨테이너 내부 기준입니다. 호스트에서는
~/docker/certs/etc/letsencrypt/live/mattermost.example.com/에 저장됩니다.
호스트에서 확인:
ls -l ~/docker/certs/etc/letsencrypt/live/mattermost.example.com/
4. .env의 인증서 경로를 발급 결과로 교체
.env에서 CERT_PATH/KEY_PATH를 방금 발급된 경로로 바꿉니다. (기존 self-signed용
./volumes/web/cert/... 줄은 주석 처리 또는 교체)
CERT_PATH=./certs/etc/letsencrypt/live/mattermost.example.com/fullchain.pem
KEY_PATH=./certs/etc/letsencrypt/live/mattermost.example.com/privkey.pem
cert.pem이 아니라fullchain.pem을 쓰는 게 중요합니다.fullchain은 서버 인증서 + 중간 인증서(체인)를 합친 파일이라, 이게 없으면 일부 브라우저/앱에서 "인증서 체인 불완전" 에러가 납니다.
5. nginx 버전으로 기동
cd ~/docker
docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
docker compose -f docker-compose.yml -f docker-compose.nginx.yml logs -f nginx
.env경로를 바꿨으므로restart가 아니라up -d(컨테이너 재생성) 로 볼륨 마운트를 다시 잡는 게 안전합니다.
[emerg] cannot load certificate 없이 nginx가 조용히 떠 있으면 성공입니다.
접속 테스트:
curl -I https://mattermost.example.com
브라우저에서 https://mattermost.example.com 접속 → 자물쇠(🔒)와 함께 Mattermost 로그인 화면이 뜨면 HTTPS까지 완료입니다.
6. 계정 / 관리자 (mmctl)
6-1. "회원가입 링크가 안 보인다"는 정상
- 계정 0개인 새 서버 → 루트 접속 시 "첫 계정 만들기" 화면
- 계정이 1개라도 있음 → 로그인 화면만 나오고, 기본값
EnableOpenServer: false때문에 "Create account" 링크가 숨겨짐
주의 — 데이터는 재설치해도 남는다:
docker compose ... down은 컨테이너만 지우고 데이터 볼륨(./volumes+ postgres)은 보존합니다. 그래서 without-nginx로 설치 → 삭제 → nginx로 재설치해도 기존 유저가 그대로 살아있습니다. 루트 접속이 로그인으로 리다이렉트되면 = 이미 계정이 있다는 뜻입니다.
6-2. 현재 유저 확인
로그인/비밀번호 없이 컨테이너 내부 관리 채널(mmctl --local)로 확인합니다.
cd ~/docker
docker compose -f docker-compose.yml -f docker-compose.nginx.yml exec mattermost \
mmctl --local user list
예시 출력 해석:
| 유저 | 성격 |
|---|---|
calls@localhost, playbooks@localhost, system-bot@localhost |
내장 봇 (플러그인용, 건드리지 말 것) |
member1@example.com, member2@example.com 등 |
실제 사람 계정 |
특정 유저가 관리자인지 확인 (roles에 system_admin 있으면 관리자):
docker compose -f docker-compose.yml -f docker-compose.nginx.yml exec mattermost \
mmctl --local user search <이메일_또는_유저명>
6-3. 관리자 계정 추가 (기존 계정은 그대로 두고)
docker compose -f docker-compose.yml -f docker-compose.nginx.yml exec mattermost \
mmctl --local user create \
--email admin@mattermost.example.com \
--username admin \
--password '<강한-비밀번호>' \
--system-admin
--system-admin이 붙어야 시스템 관리자로 생성됩니다.- 비밀번호는 기본 정책상 대문자+소문자+숫자 정도는 포함해야 통과됩니다.
생성 확인 후 https://mattermost.example.com/login 에서 로그인:
docker compose -f docker-compose.yml -f docker-compose.nginx.yml exec mattermost \
mmctl --local user search admin@mattermost.example.com
6-4. 기타 계정 관리
# 기존 유저를 관리자로 승격
docker compose -f docker-compose.yml -f docker-compose.nginx.yml exec mattermost \
mmctl --local user promote <이메일>
# 비밀번호 리셋 (비번 분실 시)
docker compose -f docker-compose.yml -f docker-compose.nginx.yml exec mattermost \
mmctl --local user change-password <이메일> --password '새비번123!'
mmctl --local은 컨테이너 안에서 인증 없이 도는 관리 채널입니다. 서버 접근 권한 = 최고 관리 권한이므로, 비번을 몰라도 계정을 부트스트랩/복구할 수 있습니다.
7. 두 번째 사용자 추가 방법
첫 관리자 이후 기본값(EnableOpenServer: false)에서는 자유 가입이 막혀 있습니다.
- 방법 1 (권장, 내부용): 관리자 로그인 → 팀 이름 옆 ⋮ → Invite People → 초대 링크
- 방법 2: System Console → User Management → Users → Add User
- 방법 3 (공개 서비스): System Console → Site Configuration → Users and Teams → Enable Open Server → true (로그인 화면에 "Create account" 링크가 다시 생김. 스팸 가입 주의)
8. 운영 명령 (참고)
# 상태 확인
docker compose -f docker-compose.yml -f docker-compose.nginx.yml ps
# 로그
docker compose -f docker-compose.yml -f docker-compose.nginx.yml logs -f mattermost
docker compose -f docker-compose.yml -f docker-compose.nginx.yml logs -f nginx
# 재시작 / 종료 (down은 데이터 볼륨 보존)
docker compose -f docker-compose.yml -f docker-compose.nginx.yml restart nginx
docker compose -f docker-compose.yml -f docker-compose.nginx.yml down
명령이 길어 번거로우면 alias 등록:
echo "alias mmc='docker compose -f docker-compose.yml -f docker-compose.nginx.yml'" >> ~/.bashrc
source ~/.bashrc
# 이후: mmc up -d / mmc ps / mmc logs -f nginx / mmc exec mattermost mmctl --local user list
9. 인증서 자동 갱신 (webroot 무중단 방식)
Let's Encrypt 인증서는 90일 만료입니다. docker-compose.nginx.yml은 무중단 갱신을 위한
shared-webroot 볼륨과 고정 네트워크 이름(mattermost)을 이미 정의해 두었습니다. 아래는
실제로 검증한 webroot 방식 구성입니다 (nginx를 켜둔 채 갱신 → 다운타임 없음).
갱신 방식 비교
| 방식 | 동작 | 다운타임 |
|---|---|---|
| webroot (이 문서 채택) | certbot이 검증 파일을 shared-webroot에 쓰고 nginx가 서빙 |
없음 |
| standalone | nginx를 잠깐 내리고 certbot이 80포트 직접 사용 | 있음(수 초) |
9-1. 갱신 설정이 webroot인지 확인
scripts/issue-certificate.sh로 발급하면 갱신 설정이 webroot로 저장됩니다. 확인:
grep authenticator /root/docker/certs/etc/letsencrypt/renewal/mattermost.example.com.conf
# authenticator = webroot 가 나와야 함
standalone으로 돼 있으면 webroot로 전환:
sed -i 's/^authenticator = standalone/authenticator = webroot/' \
/root/docker/certs/etc/letsencrypt/renewal/mattermost.example.com.conf
cat >> /root/docker/certs/etc/letsencrypt/renewal/mattermost.example.com.conf <<'EOF'
webroot_path = /usr/share/nginx/html,
[[webroot_map]]
mattermost.example.com = /usr/share/nginx/html
EOF
9-2. ⚠️ nginx가 ACME 검증 경로를 서빙하도록 (가장 중요)
Mattermost 기본 nginx conf는 80포트 요청을 전부 https로 301 리다이렉트합니다. 그러면
Let's Encrypt가 평문 http로 검증 파일(/.well-known/acme-challenge/)을 못 가져와 갱신이
멈춘 것처럼 됩니다. acme-challenge 경로만 리다이렉트에서 빼줘야 합니다.
nginx 함정:
return 301이 server 블록 바로 아래(location 밖)에 있으면 location이 아예 평가되지 않아, acme location을 추가해도 소용없습니다. 리다이렉트를location /안으로 옮겨야 합니다.
/root/docker/nginx/conf.d/default.conf 의 listen 80 server 블록을 이 형태로 만듭니다:
server {
listen 80;
server_name mattermost.example.com;
# ACME 검증만 예외 — 평문 http로 서빙 (리다이렉트보다 우선)
location ^~ /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
default_type "text/plain";
}
# 나머지는 https로 리다이렉트 (server 레벨 아님, location 안)
location / {
return 301 https://$host$request_uri;
}
}
반영 및 검증:
docker exec nginx_mattermost nginx -t # 문법 확인 (ssl_stapling warn은 무해)
docker exec nginx_mattermost nginx -s reload
# 검증 경로가 http로 실제 서빙되는지 테스트
docker run --rm -v shared-webroot:/w alpine sh -c \
'mkdir -p /w/.well-known/acme-challenge && echo OK-HELLO > /w/.well-known/acme-challenge/test'
curl -s http://mattermost.example.com/.well-known/acme-challenge/test
# → OK-HELLO 가 나와야 성공 (301 HTML이 나오면 conf 수정이 안 된 것)
# 테스트 파일 정리
docker run --rm -v shared-webroot:/w alpine rm -f /w/.well-known/acme-challenge/test
9-3. 갱신 시뮬레이션 (dry-run)으로 최종 확인
docker run --rm \
-v /root/docker/certs/etc/letsencrypt:/etc/letsencrypt \
-v /root/docker/certs/lib/letsencrypt:/var/lib/letsencrypt \
-v shared-webroot:/usr/share/nginx/html \
certbot/certbot renew --dry-run --no-random-sleep-on-renew
Congratulations, all simulated renewals succeeded 가 나오면 완료입니다.
"멈춘 것처럼 보이는" 정상 동작:
--no-random-sleep-on-renew없이 실행하면 certbot이 최대 8분(랜덤)까지 일부러 대기한 뒤 진행합니다(Non-interactive renewal: random delay of ... seconds). 이는 전 세계 서버가 동시에 Let's Encrypt를 때리지 않도록 부하를 분산하는 의도된 동작이며, cron 환경에서는 오히려 바람직합니다. 테스트할 때만--no-random-sleep-on-renew로 이 대기를 건너뛰어 즉시 결과를 봅니다.
9-4. 갱신 스크립트 생성
cat > /root/docker/scripts/renew-cert.sh <<'EOF'
#!/bin/bash
set -e
CERTS=/root/docker/certs
# 만료 30일 이내일 때만 실제 갱신 (아니면 조용히 넘어감)
docker run --rm \
-v "$CERTS/etc/letsencrypt:/etc/letsencrypt" \
-v "$CERTS/lib/letsencrypt:/var/lib/letsencrypt" \
-v shared-webroot:/usr/share/nginx/html \
certbot/certbot renew --quiet
# 갱신됐을 수 있으니 nginx가 새 인증서를 다시 읽게 reload (무중단)
docker exec nginx_mattermost nginx -s reload
EOF
chmod +x /root/docker/scripts/renew-cert.sh
cron에서는 랜덤 지연을 그대로 둡니다(부하 분산). 그래서 스크립트의
renew --quiet에는--no-random-sleep-on-renew를 넣지 않습니다.
9-5. cron 등록 (하루 2회 — certbot 공식 권장)
crontab -e
아래 한 줄 추가:
0 3,15 * * * /root/docker/scripts/renew-cert.sh >> /var/log/cert-renew.log 2>&1
확인:
crontab -l | grep renew-cert
동작 원리:
certbot renew는 만료 30일 이내일 때만 실제 갱신합니다. 매일 두 번 돌아도 그전에는 "not due"로 조용히 넘어가므로 rate limit 걱정이 없습니다. 갱신되면fullchain.pem내용만 바뀌고 경로는 그대로여서, 스크립트의nginx -s reload한 줄이 새 인증서를 무중단으로 반영합니다.
문제 해결 — dry-run이 계속 멈추면
curl ...acme-challenge/test가 301이면 → 9-2 nginx conf 수정 안 됨 (리다이렉트 예외 누락)--no-random-sleep-on-renew없이 멈춘 것처럼 보이면 → 랜덤 지연(정상). 2~3분 대기하거나 플래그 추가Requesting a certificate부근에서 진짜 멈추면 → 컨테이너 outbound(드물게 IPv6) 문제.docker run에--network host추가로 우회
요약 체크리스트
- Docker / Docker Compose 설치 확인
- 도메인 DNS A레코드 → 서버 공인 IP (
dig=curl ifconfig.me) - 방화벽/보안그룹에서 80·443 (+ Calls 쓰면 8443/udp,tcp) 개방
-
.env의DOMAIN,MM_SERVICESETTINGS_SITEURL을 실제 도메인으로 설정 -
bash scripts/issue-certificate.sh -d <도메인> -o ${PWD}/certs로 인증서 발급 (⚠️-o는 이메일 아님, 절대경로) -
.env의CERT_PATH/KEY_PATH를 발급된fullchain.pem/privkey.pem경로로 교체 -
-f docker-compose.yml -f docker-compose.nginx.yml up -d로 기동,logs -f nginx확인 - https://<도메인> 접속 → 자물쇠 확인
-
mmctl --local user list로 기존 계정 확인 -
mmctl --local user create ... --system-admin으로 관리자 계정 추가 - nginx 80블록에 acme-challenge 예외 추가 (webroot 갱신용, 리다이렉트는
location /안) -
certbot renew --dry-run --no-random-sleep-on-renew로 갱신 검증 (succeeded 확인) -
renew-cert.sh생성 + cron 하루 2회 등록 (자동 갱신)