VPS에 Docker로 Chatwoot 직접 설치 및 운영하기
Docker Compose와 Traefik을 사용하여 Chatwoot을 직접 호스팅하는 방법을 안내합니다. Postgres pgvector 설정, SMTP 메일 발송, 데이터 백업 및 안전한 버전 업그레이드 전략을 포함하여 1년 이상 안정적으로 운영하는 실무 가이드를 제공합니다.
구축할 내용
VPS에서 Chatwoot을 직접 호스팅하려면 Rails 웹 프로세스, Sidekiq 백그라운드 워커, pgvector 확장이 포함된 PostgreSQL, Redis 등 4개의 컨테이너를 실행해야 합니다. Chatwoot은 오픈 소스 고객 지원 데스크이므로, 사용자가 제어하는 서버에서 팀 공유 받은 편지함과 웹사이트 채팅 위젯을 사용할 수 있습니다. 설치에는 약 20분이 소요됩니다. 설치 이후의 메일 발송, 백업, 업그레이드, 규모 산정 작업이 1년 뒤에도 서비스가 정상적으로 운영될지를 결정합니다.
각 컨테이너는 하나의 역할을 수행합니다. Rails는 상담원 대시보드와 위젯 API(애플리케이션 프로그래밍 인터페이스)를 제공합니다. Sidekiq은 이메일 발송, 연결된 채널 폴링, 자동화 규칙 실행, 보고서 생성과 같은 느린 작업을 처리합니다. Postgres는 대화 내용, 연락처, 상담원 계정 및 대시보드에서 변경하는 모든 설정을 저장합니다. Redis는 Sidekiq 큐와 페이지 새로고침 없이 대시보드에 새 메시지를 전달하는 ActionCable pub/sub 채널을 유지합니다. 여기서 Redis는 단순한 일회성 캐시가 아니며, 데이터 유실 시 큐에 쌓인 작업도 함께 사라집니다.
업스트림 compose 파일의 Postgres 이미지는 기본 postgres 이미지가 아닌 pgvector/pgvector:pg16입니다. 이는 Chatwoot의 스키마가 AI 기능을 위해 vector 확장을 사용하기 때문입니다. 기본 Postgres로 교체하면 해당 확장의 제어 파일이 이미지에 포함되어 있지 않아 첫 번째 데이터베이스 실행 시 ERROR: extension "vector" is not available 오류와 함께 중단됩니다. 업스트림에서 제공하는 이미지를 사용하십시오.
이 가이드는 서버에 Docker와 리버스 프록시가 이미 설정되어 있다고 가정합니다. 설정되어 있지 않다면 VPS에서 Docker Compose 사용하기를 먼저 진행한 후 돌아오십시오.
자체 호스팅 Chatwoot에는 어느 정도 사양의 VPS가 필요한가?
2026년 8월 기준으로, 공식 요구 사항 페이지에서는 최소 사양으로 4 GB RAM과 4 CPU 코어를 권장하며, 이 사양으로 하루 최대 10,000건의 대화를 처리할 수 있다고 명시합니다. 8 GB RAM과 8 CPU 코어 구성은 하루 최대 20,000건까지 처리 가능합니다. 또한 최소 1 GB의 swap 공간을 요구하는데, 그 이유는 업그레이드 도중 메모리가 부족해지는 상황을 방지하기 위해서입니다. 파일 업로드를 고려하기 전에 Postgres를 위해 최소 5 GB에서 10 GB의 디스크 공간을 확보하십시오.
이제 현실적인 부분을 말씀드리겠습니다. 2 GB VPS에서도 Chatwoot을 부팅할 수는 있으며, 상담원 2명이 조용한 받은 편지함을 관리하는 환경에서는 문제없어 보입니다. 하지만 두 가지 지점에서 서비스가 중단됩니다. 첫 번째는 Sidekiq입니다. 공식 문서에 따르면 바쁜 서버에서 Sidekiq은 1 GB 이상의 메모리를 사용하므로, 이메일이 몰리거나 보고서 작업이 실행되면 Rails, Postgres, Redis가 메모리를 점유하기도 전에 시스템 메모리가 한계에 도달합니다. 두 번째는 업그레이드 과정입니다. db:chatwoot_prepare은 마이그레이션을 적용하기 위해 새로운 Rails 프로세스를 실행하는데, 이 과정에서 실제 작업을 수행하기도 전에 수백 MB의 메모리를 소모합니다.
이때 정중한 경고는 없습니다. 커널의 OOM(Out of Memory) 킬러가 가장 큰 프로세스에 SIGKILL을 보내면, Docker는 컨테이너가 종료된 것으로 간주하고 restart: always가 이를 다시 시작합니다. 이후 docker compose ps을 확인하면 컨테이너가 계속 Exited (137) 상태로 돌아가는 것을 볼 수 있는데, 여기서 137은 시그널 9에 의해 강제 종료되었음을 의미합니다. sudo dmesg -T | grep -i "killed process"를 실행하여 커널이 선택한 프로세스를 확인하면 이를 확정할 수 있습니다.
4 GB 사양이 예산을 초과한다면, 2 GB VPS에 2 GB의 swap을 설정하여 운영하십시오. 서비스가 완전히 죽는 대신 부하가 걸릴 때 응답 속도가 느려지는 것을 감수해야 합니다. 어느 쪽이든 서비스별로 메모리 상한을 설정하는 것이 좋습니다. 그래야 워커 프로세스가 데이터베이스까지 함께 다운시키는 상황을 막을 수 있습니다. Docker Compose의 메모리 제한을 참조하십시오.
파일 업로드는 사용자가 제한을 설정할 수 없는 증가 요인입니다. 고객이 첨부하는 모든 스크린샷은 스토리지 볼륨에 저장되어 계속 쌓이므로, 데이터베이스가 디스크를 가득 채웠다고 단정하기보다 docker system df -v을 주기적으로 모니터링하십시오.
Compose 파일을 가져오고 버전 태그 고정하기
mkdir -p ~/chatwoot && cd ~/chatwoot
wget -O .env https://raw.githubusercontent.com/chatwoot/chatwoot/develop/.env.example
wget -O docker-compose.yaml https://raw.githubusercontent.com/chatwoot/chatwoot/develop/docker-compose.production.yaml
chmod 600 .env방금 다운로드한 파일에는 image: chatwoot/chatwoot:latest이 명시되어 있습니다. 다른 작업을 수행하기 전에 이 부분을 먼저 변경하십시오.
services:
base: &base
image: chatwoot/chatwoot:v4.16.2
env_file: .env
volumes:
- storage_data:/app/storagelatest을 사용하면 다음 docker compose pull 실행 시 당일 배포된 최신 버전이 적용됩니다. 이 과정에서 읽어보지 못한 마이그레이션이 포함된 메이저 버전으로 업데이트될 수 있습니다. Chatwoot 마이그레이션은 실질적으로 되돌릴 수 없으므로, 실수로 버전이 올라가면 실행 취소가 아닌 백업 복원을 수행해야 합니다. 태그를 고정하고 의도적으로 변경하십시오. 2026년 8월 기준 최신 릴리스는 v4.16.2입니다. 오늘 기준으로 고정해야 할 태그는 릴리스 페이지에서 확인하십시오.
base 서비스는 rails과 sidekiq가 모두 병합하는 YAML 앵커입니다. 따라서 한 곳에서 태그를 변경하면 두 서비스 모두에 적용됩니다. 파일을 수정하는 김에 상단의 version: '3' 줄을 삭제하십시오. 최신 Compose는 이 줄을 무시하며, 명령어를 실행할 때마다 the attribute 'version' is obsolete, it will be ignored 경고를 출력합니다.
.env 파일 작성하기
먼저 비밀 키를 생성합니다. 업스트림에서는 영문자와 숫자로만 구성된 값을 요구합니다. 특수 문자가 포함되면 셸이나 YAML 파서를 거칠 때 값이 손상될 수 있기 때문입니다.
head /dev/urandom | tr -dc A-Za-z0-9 | head -c 63 ; echo ''그런 다음 .env에 다음 키들을 설정합니다.
SECRET_KEY_BASE=<the 63 characters you just generated>
FRONTEND_URL=https://support.example.com
FORCE_SSL=true
DEFAULT_LOCALE=en
ENABLE_ACCOUNT_SIGNUP=true
POSTGRES_HOST=postgres
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=<long random string>
POSTGRES_DATABASE=chatwoot
REDIS_URL=redis://redis:6379
REDIS_PASSWORD=<a different long random string>
RAILS_ENV=production
INSTALLATION_ENV=docker
ACTIVE_STORAGE_SERVICE=localPOSTGRES_HOST=postgres과 redis://redis:6379은 프로젝트의 기본 네트워크에서 해석되는 Compose 서비스 이름입니다. FRONTEND_URL은 단순한 장식이 아닙니다. Chatwoot은 위젯 스크립트 URL과 발송되는 모든 이메일 내 링크를 이 값을 기준으로 생성합니다. 따라서 값이 잘못되면 비밀번호 재설정 링크가 응답하지 않는 호스트를 가리키게 됩니다.
이제 업스트림 파일의 함정을 확인해야 합니다. postgres 서비스는 .env을 읽지 않습니다. 이 서비스는 자체적인 environment 블록을 가지고 있으며 POSTGRES_PASSWORD=가 비어 있습니다. 따라서 .env에만 비밀번호를 설정하면 데이터베이스에는 비밀번호가 설정되지 않고 애플리케이션에만 설정되는 문제가 발생합니다. 서비스를 동일한 변수로 지정하십시오.
postgres:
image: pgvector/pgvector:pg16
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=chatwoot
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}Compose는 ${...} 치환을 위해 프로젝트 디렉터리에서 .env를 읽으므로, 이제 양쪽 모두 동일한 문자열을 사용하게 됩니다. 이 설정을 잘못하면 Rails가 PG::ConnectionBad: FATAL: password authentication failed for user "postgres" 오류와 함께 중단됩니다.
많은 사용자가 당황하는 동작이 하나 있습니다. Postgres 이미지는 데이터 디렉터리가 비어 있을 때 초기화하는 과정에서만 POSTGRES_PASSWORD을 적용합니다. initdb는 두 번 실행되지 않으므로 나중에 값을 변경해도 아무런 효과가 없습니다. 스택을 이미 한 번 실행했다면 데이터베이스 내부에서 직접 변경해야 합니다.
docker compose exec postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD 'the-new-password';"ENABLE_ACCOUNT_SIGNUP=true은 임시 설정입니다. 이 설정은 공개 가입 양식을 열어 첫 번째 계정을 생성할 수 있게 합니다. 계정 생성이 완료되면 즉시 false로 변경하고 docker compose up -d을 다시 실행하십시오. 그렇지 않으면 URL을 알아낸 누구나 지원 데스크에 가입할 수 있습니다. 그 이후부터는 초대장을 통해서만 상담원을 추가할 수 있으며, 비밀번호는 이 애플리케이션 내에서만 관리됩니다. 서비스가 6개 이상으로 늘어나고 서비스마다 별도의 계정 목록을 관리하는 것이 번거로워지면, Authentik과 같은 자체 호스팅 ID 공급자를 도입하여 이를 대체할 수 있습니다.
이제 .env에는 이 스택의 모든 비밀 정보가 평문으로 저장되어 있습니다. 따라서 파일 모드를 600으로 유지하고 git 저장소에 포함되지 않도록 하십시오. Compose가 env 파일을 읽는 방식 및 비밀 정보 유출 경로에서 env_file와 environment의 차이를 포함한 주의 사항을 확인할 수 있습니다.
기존 Traefik 뒤에 Chatwoot 배치하기
하나의 애플리케이션을 위해 두 번째 리버스 프록시를 구축하지 마십시오. 만약 Traefik이 이미 이 서버의 다른 컨테이너에 대해 TLS(전송 계층 보안)를 종료하고 있다면, label 블록을 사용하여 Chatwoot을 연결할 수 있습니다. 아직 설정하지 않았다면 여러 Docker Compose 앱 앞단의 Traefik을 먼저 설정한 뒤 다시 돌아오십시오.
업스트림의 docker-compose.yaml는 기본 상태와 가깝게 유지하여 나중에 새 버전과 diff를 수행할 수 있도록 하고, 변경 사항은 override 파일에 작성하십시오. Compose는 docker-compose.override.yaml를 자동으로 병합하며, 여러 파일로 Compose 분할하기에서 병합 규칙을 설명합니다.
services:
rails:
networks:
- default
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.chatwoot.rule=Host(`support.example.com`)"
- "traefik.http.routers.chatwoot.entrypoints=websecure"
- "traefik.http.routers.chatwoot.tls.certresolver=letsencrypt"
- "traefik.http.services.chatwoot.loadbalancer.server.port=3000"
networks:
proxy:
external: true사용자 환경에 맞는 entrypoint와 certresolver 이름을 사용하십시오. 컨테이너는 Traefik과 동일한 Docker 네트워크에 위치해야 하며, 이것이 proxy 항목이 수행하는 역할입니다. 또한 컨테이너는 default에도 연결되어 있어야 하며, 그렇지 않으면 Postgres 및 Redis 연결이 끊어집니다. 이 두 번째 줄은 사용자가 자주 놓치는 부분입니다.
ports: 블록은 그대로 두십시오. 업스트림은 이를 127.0.0.1:3000에 바인딩하는데, 이는 루프백 전용이므로 인터넷에서 직접 접근할 수 없으며 curl -I http://127.0.0.1:3000을 사용하여 서버 내부에서 테스트할 때 유용합니다.
에이전트 대시보드는 실시간 메시지 전달을 위해 /cable로 웹소켓 연결을 유지합니다. Traefik은 별도의 추가 설정 없이 HTTP 업그레이드 요청을 전달하므로 추가할 설정은 없습니다. 나중에 Traefik 앞단에 CDN이나 다른 프록시를 배치한다면 해당 장비에서 웹소켓을 허용하십시오. 그렇지 않으면 대시보드는 정상적으로 로드되지만, 수동으로 새로고침을 해야만 새 메시지가 나타나는 현상이 발생합니다.
데이터베이스 초기화 및 스택 시작
먼저 데이터 서비스를 실행하고 Postgres가 첫 실행을 완료할 때까지 기다립니다.
docker compose up -d postgres redis
docker compose logs postgres | tail -n 5database system is ready to accept connections가 준비될 때까지 기다립니다. 그런 다음 스키마를 생성합니다.
docker compose run --rm rails bundle exec rails db:chatwoot_prepare이 명령은 데이터베이스가 없으면 생성한 뒤 스키마와 기본 시드 데이터를 로드합니다. 마이그레이션 로그를 출력하고 정상적으로 종료됩니다. 만약 postgres:5432 - no response을 계속 출력하며 멈춰 있다면, 엔트리포인트가 아직 연결을 수락하지 않는 데이터베이스를 기다리는 중입니다. 첫 실행 시에는 보통 initdb가 작업 중인 상태입니다. 기다린 후 Postgres 로그를 확인하고 다시 실행하십시오. 만약 vector 확장 기능에서 멈춘다면, pgvector 이미지를 일반 Postgres 이미지로 교체한 것입니다.
docker compose up -d
docker compose ps
docker compose logs --tail 30 rails네 개의 컨테이너 모두 Up 상태여야 하며, rails 로그는 http://0.0.0.0:3000에서 수신 대기 중이라는 Puma 메시지로 끝나야 합니다. 그런 다음 공개 경로를 확인합니다.
curl -sI https://support.example.com | head -n 1HTTP/2 200은 전체 체인이 정상 작동함을 의미합니다. Traefik에서 404 오류가 발생한다면 라우터 규칙이 일치하지 않는 것이며, 보통 호스트 이름 오타가 원인입니다. 502 오류는 Traefik이 라우터는 찾았으나 컨테이너에 도달하지 못한 경우로, 거의 항상 proxy 네트워크가 누락되었거나 loadbalancer.server.port가 3000이 아닐 때 발생합니다.
URL을 열고 /app/auth/signup에서 계정을 생성한 뒤, ENABLE_ACCOUNT_SIGNUP=false을 설정하고 docker compose up -d를 실행하여 양식을 닫습니다.
SMTP 설정이 없을 때 비밀번호 재설정과 이메일 대화가 실패하는 이유
SMTP(Simple Mail Transfer Protocol) 설정이 없는 Chatwoot은 메일을 발송할 수 없는 지원 데스크가 되며, 이는 알림 이상의 문제를 야기합니다. 비밀번호 재설정 기능이 작동하지 않아 잠긴 관리자는 계속해서 접속할 수 없게 됩니다. 상담원 초대 역시 이메일로 이루어지므로 발송되지 않습니다. 이메일 대화에서 고객에게 회신하는 기능도 작동하지 않아 대화가 단방향으로만 진행됩니다. 이는 많은 사용자가 간과했다가 가장 곤란한 상황에서 발견하게 되는 문제입니다.
메커니즘은 명확합니다. SMTP 설정이 없으면 ActionMailer는 기본값인 포트 25의 localhost으로 메일을 전달하려고 시도합니다. Rails 컨테이너 내부에는 메일 서버가 없으므로 전달 작업에서 Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25 예외가 발생합니다. 메일은 백그라운드 작업으로 발송되므로 해당 오류는 Rails 로그가 아닌 Sidekiq 로그에 기록됩니다. 한편 "비밀번호를 잊으셨나요"를 클릭한 사용자는 정상적인 확인 메시지를 보게 되지만, 실제 메일은 수신하지 못합니다.
MAILER_SENDER_EMAIL=Support <support@example.com>
SMTP_DOMAIN=example.com
SMTP_ADDRESS=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=support@example.com
SMTP_PASSWORD=<the relay password>
SMTP_AUTHENTICATION=plain
SMTP_ENABLE_STARTTLS_AUTO=trueSTARTTLS를 사용하는 포트 587을 사용하십시오. 이 방식은 일반 텍스트로 연결을 시작한 뒤 인증 전에 암호화된 연결로 업그레이드합니다. 대부분의 VPS 제공업체는 스팸 방지를 위해 아웃바운드 포트 25를 차단하므로, 일반적으로 포트 587을 통한 릴레이만이 유일한 연결 방법입니다. SMTP_DOMAIN는 SMTP 통신 과정에서 서버가 알리는 도메인이며, 일부 릴레이는 이 정보가 일치하지 않으면 연결을 거부합니다.
설정을 적용한 뒤 워커를 모니터링하십시오:
docker compose up -d rails sidekiq
docker compose logs -f sidekiq로그인 페이지에서 비밀번호 재설정을 트리거하십시오. 정상적으로 전달되면 Sidekiq 로그에 메일러 작업이 완료된 것으로 나타납니다. 실패하면 예외 클래스가 표시되고 Sidekiq이 점진적으로 대기 시간을 늘리며 재시도를 수행합니다. 이 때문에 잘못된 릴레이 설정은 몇 분 간격으로 몇 시간 동안 동일한 오류를 발생시킵니다.
두 가지 거부 사례가 흔히 발생하며, 둘 다 Chatwoot의 버그가 아닙니다. 535 Authentication failed은 해당 릴레이의 사용자 이름이나 비밀번호가 잘못되었음을 의미하며, 많은 제공업체는 계정 비밀번호 대신 애플리케이션 전용 비밀번호를 요구합니다. 550 Sender address rejected은 MAILER_SENDER_EMAIL이 릴레이에서 발신자로 허용하지 않는 주소임을 의미하므로, 해당 업체에서 인증받은 메일함이나 도메인을 사용해야 합니다.
대화로 이메일을 수신하는 작업은 별개의 과정입니다. 이는 MAILER_INBOUND_EMAIL_DOMAIN와 RAILS_INBOUND_EMAIL_SERVICE, 그리고 수신된 메시지를 Chatwoot으로 전달해 줄 메일 서버가 필요합니다. 릴레이 서비스를 이용하는 것이 가장 빠른 방법입니다. 전체 메일 경로를 직접 관리하고 싶다면 Mailcow로 직접 메일 서버 운영하기를 통해 해당 작업에 필요한 실제 요구 사항을 확인하십시오.
백업 대상과 복구 검증 방법
Chatwoot 백업은 네 가지 요소로 구성되며, 하나라도 누락되면 복구가 아닌 재구축이 됩니다.
- Postgres 데이터베이스: 대화, 연락처, 상담원 계정 및 모든 설정이 저장됩니다.
storage_data볼륨:ACTIVE_STORAGE_SERVICE=local는 업로드된 파일을 디스크에 저장하고 Postgres에는 참조 행만 남기기 때문에 반드시 필요합니다..env파일:SECRET_KEY_BASE와ACTIVE_RECORD_ENCRYPTION_*키를 포함하고 있습니다.- compose 파일: 데이터베이스 스키마와 일치하는 정확한 이미지 태그를 기록하고 있습니다.
데이터베이스만 복구하면 모든 대화의 첨부 파일이 깨집니다. 데이터베이스의 행은 디스크에 더 이상 존재하지 않는 파일을 가리키기 때문입니다.
cd ~/chatwoot
docker compose exec -T postgres pg_dump -U postgres -Fc chatwoot > db-$(date +%F).dump-T 옵션은 중요합니다. 이 옵션이 없으면 Compose가 의사 터미널(pseudo terminal)을 할당하여 스트림 내의 줄바꿈 바이트를 재작성하므로, pg_restore에서 거부하는 덤프 파일이 생성됩니다. -Fc은 압축을 지원하고 pg_restore가 선택적으로 작동할 수 있게 해주는 사용자 지정 형식입니다.
docker run --rm -v chatwoot_storage_data:/data:ro -v "$PWD":/backup alpine \
tar czf /backup/storage-$(date +%F).tgz -C /data .볼륨 이름은 프로젝트 디렉터리 이름에 _storage_data을 붙인 형태입니다. Docker는 존재하지 않는 볼륨 이름을 지정하면 오류를 내는 대신 빈 볼륨을 생성하므로, 명령을 실행하기 전에 docker volume ls | grep storage_data로 이름을 확인하십시오. 확인하지 않으면 오류 없이 유효하지만 비어 있는 아카이브만 생성됩니다. 작업 후에는 ls -lh storage-*.tgz로 크기를 확인하십시오.
두 파일이 보호 대상과 같은 디스크에 있으면 아무런 보호 효과가 없습니다. 데이터베이스 덤프에는 모든 고객 메시지가 평문으로 포함되어 있으므로, 파일을 서버 외부로 전송하고 암호화하십시오. restic을 이용한 암호화된 외부 백업에서 백업 일정과 보관 정책을 다룹니다.
복구 훈련: 실제 상황 전에 반드시 수행하십시오
운영 중인 서버가 아닌 두 번째 VPS에 복구하십시오. .env, compose 파일, 두 개의 아카이브를 복사한 뒤 다음을 실행합니다.
docker compose up -d postgres
docker compose exec -T postgres pg_restore -U postgres -d chatwoot --clean --if-exists < db-2026-08-10.dump
docker run --rm -v chatwoot_storage_data:/data -v "$PWD":/backup alpine \
sh -c 'rm -rf /data/* && tar xzf /backup/storage-2026-08-10.tgz -C /data'
docker compose up -d--clean --if-exists는 로드하기 전에 기존 객체를 삭제하므로, 삭제해도 무방한 데이터베이스를 대상으로만 실행하십시오. 복구 후 로그인하여 첨부 파일이 있는 대화를 엽니다. 메시지 목록이 로드되고 파일이 다운로드된다면 백업이 정상적으로 수행된 것입니다.
다른 SECRET_KEY_BASE로 복구하면 모든 세션 쿠키가 무효화되어 모든 사용자가 로그아웃됩니다. 다른 ACTIVE_RECORD_ENCRYPTION_* 키로 복구하는 것은 더 심각합니다. Chatwoot이 채널 자격 증명이 저장된 열을 복호화할 수 없어 ActiveRecord::Encryption::Errors::Decryption 오류가 발생합니다. 이것이 .env이 백업 목록에 포함된 이유입니다.
Chatwoot을 새 태그로 업그레이드하는 방법
명령어보다 순서가 더 중요합니다.
- 현재 태그와 대상 태그 사이의 릴리스 노트를 읽고, 필요한 수동 작업이 있는지 확인합니다.
- 데이터베이스 덤프와 스토리지 아카이브를 새로 생성하고, 두 파일의 크기가 정상인지 확인합니다.
docker-compose.yaml파일 내base서비스의 이미지 태그를 수정합니다.- 새 이미지를 내려받고, 스택을 중지한 뒤, 마이그레이션을 실행하고, 다시 시작합니다.
docker compose pull
docker compose down
docker compose run --rm rails bundle exec rails db:chatwoot_prepare
docker compose up -d
docker compose images마이그레이션은 새 이미지에서 실행되어야 하므로 마이그레이션 전에 이미지를 내려받아야 합니다. 이전 이미지에는 새 마이그레이션 파일이 포함되어 있지 않기 때문입니다. 마이그레이션 전에 스택을 중지하십시오. 이전 코드와 새 스키마가 일치하지 않으면 실행 중인 이전 Rails 프로세스가 오류를 발생시키거나 새 스키마가 허용하지 않는 행을 기록할 수 있습니다. 또한 중지하면 마이그레이션에 필요한 메모리가 확보되는데, 이것이 업스트림에서 스왑(swap)을 요구하는 주된 이유입니다.
docker compose images은 각 컨테이너가 실제로 실행 중인 태그를 출력합니다. 이를 통해 태그를 수정하고 내려받기를 잊은 경우를 파악할 수 있습니다.
한 번에 여러 버전을 건너뛰지 마십시오. 아주 오래된 설치 환경의 경우 중간 태그를 거쳐 업그레이드하는 것이 업스트림의 권장 사항입니다. 마이그레이션이 기본 스키마에 통합되면 이전 마이그레이션 파일은 삭제되므로, 너무 오래된 데이터베이스는 더 이상 진행할 경로가 없는 상태가 될 수 있습니다. 한 번에 하나의 마이너 버전씩 이동하고 각 단계마다 준비(prepare) 단계를 실행하십시오.
마이그레이션이 실행되기 전에 Rails가 시작되면 서비스 제공을 거부하고 ActiveRecord::PendingMigrationError: Migrations are pending를 기록합니다. restart: always이 설정된 경우 컨테이너가 반복적으로 재시작되므로, docker compose ps를 확인하면 가동 시간이 몇 초마다 초기화되는 것을 볼 수 있습니다. 준비 단계를 실행하면 이 문제가 해결됩니다.
롤백은 이전 태그로 되돌리고 덤프를 복원하는 것을 의미합니다. 신뢰할 수 있는 역방향 마이그레이션 경로는 존재하지 않으며, 이것이 2단계가 필요한 이유입니다.
실패 유형 및 확인되는 메시지
Traefik에서 502 Bad Gateway 발생. 라우터는 일치했으나 백엔드가 응답하지 않는 상태입니다. docker compose ps를 확인하여 rails가 Up 상태인지 확인한 다음, docker network inspect proxy을 실행하여 rails 컨테이너가 컨테이너 목록에 나타나는지 확인하십시오. 컨테이너가 연결되어 있지 않으면 Traefik은 이를 인식할 수 없으므로, 요청이 라우터와 일치하더라도 전달할 곳이 없게 됩니다.
대시보드는 로드되지만 새 메시지를 보려면 새로고침이 필요함. /cable로 향하는 웹소켓 연결이 차단되었거나, FRONTEND_URL가 브라우저 주소창의 주소와 일치하지 않는 경우입니다. 주소가 일치하지 않으면 페이지가 다른 오리진으로 웹소켓을 열려고 시도하며, 브라우저는 이를 차단합니다.
FATAL: password authentication failed for user "postgres". .env에 설정된 비밀번호와 Postgres 데이터 볼륨에 저장된 비밀번호가 서로 다릅니다. .env을 다시 수정해도 이미 초기화된 데이터베이스는 변경되지 않으므로, 실행 중인 컨테이너 내부에서 ALTER USER를 사용하여 수정하십시오.
NOAUTH Authentication required.. Redis가 --requirepass로 실행 중이지만 애플리케이션이 비밀번호 없이 연결을 시도하고 있습니다. 이는 .env에서 REDIS_PASSWORD이 누락되었거나 설정이 반영되지 않았기 때문입니다. docker compose exec redis redis-cli -a "$REDIS_PASSWORD" ping을 사용하여 직접 테스트하십시오. 정상이라면 PONG라고 응답해야 합니다.
컨테이너가 코드 137로 종료됨. 이는 SIGKILL 신호이며, 사양이 낮은 서버에서는 커널의 OOM(Out of Memory) 킬러가 작동한 것입니다. 스왑(swap)을 추가하거나, 서비스별 메모리 제한을 설정하거나, 더 높은 사양의 플랜으로 업그레이드하십시오.
FAQ
자체 호스팅 Chatwoot VPS에는 RAM이 얼마나 필요한가?
2026년 8월 기준, 업스트림에서는 하루 최대 10,000건의 대화를 처리하기 위해 최소 4 GB RAM과 4개 CPU 코어를 요구하며, 최대 20,000건을 처리하려면 8 GB RAM과 8개 코어를 권장합니다. 최소 1 GB의 스왑(swap)을 추가하십시오. 업그레이드 시 마이그레이션을 적용하기 위해 두 번째 Rails 프로세스가 실행되는데, 이때 메모리가 부족한 소형 서버는 문제가 발생하기 때문입니다. 2 GB VPS에서도 부팅은 가능하며 소수의 상담원이 사용할 수는 있으나, 부하가 걸리면 Sidekiq만으로도 1 GB를 초과할 수 있습니다. 따라서 사용량이 많거나 업그레이드 중에는 컨테이너가 종료 코드 137로 강제 종료될 수 있음을 예상해야 합니다.
Chatwoot 비밀번호 재설정 이메일이 도착하지 않는 이유는 무엇인가?
SMTP 설정이 구성되지 않았기 때문에 ActionMailer가 localhost의 25번 포트로 전송을 시도하지만, 컨테이너 내부에는 메일 서버가 없기 때문입니다. 브라우저에는 성공 메시지가 표시되더라도 Sidekiq 작업은 Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25 오류와 함께 실패합니다. .env 파일에 SMTP_ADDRESS, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, MAILER_SENDER_EMAIL을 설정하고 rails 및 sidekiq 서비스를 재시작한 뒤, 재설정을 트리거하면서 docker compose logs -f sidekiq을 모니터링하십시오.
Chatwoot을 복구하려면 무엇을 백업해야 하는가?
Postgres 데이터베이스, storage_data Docker 볼륨, .env 파일 및 compose 파일을 백업해야 합니다. 데이터베이스만으로는 충분하지 않습니다. 업로드된 파일은 볼륨에 저장되고 Postgres는 해당 파일에 대한 참조만 가지고 있으므로, 데이터베이스만 복구하면 첨부 파일이 깨진 대화 내용만 남게 됩니다. .env은 중요합니다. SECRET_KEY_BASE가 다르면 모든 사용자가 로그아웃되며, ACTIVE_RECORD_ENCRYPTION_* 키가 다르면 암호화된 컬럼을 읽을 수 없게 됩니다.
데이터베이스를 손상시키지 않고 Chatwoot을 업그레이드하려면 어떻게 해야 하는가?
백업을 수행하고 compose 파일의 이미지 태그를 변경한 뒤, docker compose pull, docker compose down, docker compose run --rm rails bundle exec rails db:chatwoot_prepare, docker compose up -d을 실행하십시오. 마이그레이션은 새 이미지에서 실행되어야 하므로 먼저 이미지를 pull하고, 이전 코드가 새 스키마와 충돌하여 오류를 일으킬 수 있으므로 먼저 스택을 중지하십시오. 구버전 설치 환경에서는 마이그레이션이 기본 스키마에 통합되면 삭제되므로, 한 번에 하나의 마이너 버전씩 순차적으로 이동하십시오.
pgvector 대신 표준 postgres 이미지를 사용할 수 있는가?
아니요. Chatwoot 스키마는 vector 확장을 활성화하므로, 기본 postgres 이미지를 사용하면 db:chatwoot_prepare 과정에서 ERROR: extension "vector" is not available 오류가 발생합니다. 해당 이미지에는 확장의 제어 파일이 포함되어 있지 않기 때문입니다. 업스트림 compose 파일에 정의된 pgvector/pgvector:pg16를 유지하거나, 사용하는 Postgres 메이저 버전에 맞는 pgvector가 포함된 다른 이미지를 사용하십시오.