---
title: 문제 해결
description: Multica를 자체 호스팅할 때 흔히 겪는 문제 — 증상, 원인, 진단 방법, 해결 방법.
---

import { Callout } from "fumadocs-ui/components/callout";

증상으로 문제를 찾아보세요. 각 항목은 **증상 / 가능한 원인 / 진단 방법 / 해결 방법**을 제공합니다. 여러분의 상황이 목록에 없다면 [GitHub](https://github.com/multica-ai/multica/issues)에 이슈를 등록하세요.

## 데몬이 서버에 연결되지 않음

**증상**: [`multica daemon`](/cli)의 `status` 명령이 `offline` 또는 `connection refused`를 표시합니다. 서버 로그에 `/api/daemon/register`나 `/api/daemon/heartbeat` 요청이 보이지 않습니다. 데몬 메커니즘이 어떻게 동작하는지는 [데몬과 런타임](/daemon-runtimes)을 참고하세요.

**가능한 원인**:

1. **`MULTICA_SERVER_URL`이 잘못된 주소를 가리킴** — 기본값은 `ws://localhost:8080/ws`이며, 자체 호스팅 시 여러분의 서버 주소로 변경해야 합니다
2. **네트워크 / 방화벽 차단** — 데몬과 서버가 같은 네트워크에 있지 않거나, 아웃바운드 트래픽이 차단됨
3. **토큰이 만료되었거나 유효하지 않음** — `multica login`을 한 번도 실행하지 않았거나, PAT이 취소됨
4. **서버가 등록을 거부함** — 로그인한 계정이 대상 워크스페이스에 속해 있지 않음(register가 403을 반환)
5. **DNS 해석 실패** — 데몬 기기에서 호스트명이 해석되지 않음

**진단 방법**:

```bash
multica daemon logs --lines 100    # look for daemon-side errors
echo $MULTICA_SERVER_URL          # confirm the address is set
curl -i http://<server-host>:8080/health   # hit the server directly
curl -i http://<server-host>:8080/readyz  # include DB + migration readiness
cat ~/.multica/config.json        # verify api_token exists
multica workspace list            # confirm you're a member of the target workspace
```

**해결 방법**: 위의 각 원인을 하나씩 처리하세요. 가장 흔한 두 가지 해결책은 **`MULTICA_SERVER_URL`을 변경하고 데몬을 재시작**하는 것(`multica daemon restart`)과 **다시 로그인**하는 것(`multica logout && multica login`)입니다.

## 작업이 `queued`에서 멈춤

**증상**: 에이전트에게 이슈를 할당한 뒤 이슈 상태가 곧바로 `in_progress`로 바뀌지만, 오랜 시간이 지나도 페이지에 에이전트 실행의 흔적이 보이지 않습니다. `multica daemon status`는 데몬을 `online`으로 표시합니다.

**가능한 원인**(빈도순):

1. **에이전트 동시 실행 상한 도달** — 이 에이전트의 `max_concurrent_tasks`(기본값 6)가 이미 다른 실행 중인 작업들로 가득 참
2. **같은 이슈에서 같은 에이전트의 다른 작업이 아직 실행 중** — 같은 에이전트 × 같은 이슈는 순차 실행이 강제됩니다(중복 실행 방지)
3. **에이전트가 보관됨** — 보관 후에도 새 작업은 여전히 대기열에 들어가지만 클레임될 수 없으며, 5분 뒤 타임아웃됩니다(code-issue G-01)
4. **데몬이 현재 워크스페이스에 이 런타임을 등록하지 않음** — 데몬을 재시작하거나 UI에서 런타임을 다시 선택하세요
5. **데몬 연결 끊김** — 최근 45초 동안 하트비트가 없었습니다. `daemon status`가 `online`으로 보이는 것은 방금 끊긴 상태를 반영한 것일 수 있습니다

**진단 방법**:

```bash
multica daemon status --output json       # runtime list + last_seen_at
multica agent list                         # check agent archived state
multica issue show <issue-id>             # inspect task history
```

서버 측(자체 호스팅)에서는 `"no_tasks"` / `"no_capacity"`를 grep하여 클레임 결과를 확인할 수 있습니다.

**해결 방법**:

- 동시 실행 가득 참 → 실행 중인 작업이 끝나기를 기다리거나, `multica agent update <id> --max-concurrent-tasks 10`으로 상한을 올리세요
- 같은 이슈 순차 실행 → 이전 작업이 끝나기를 기다리거나, 다른 에이전트에게 다시 할당하세요
- 에이전트 보관됨 → `multica agent restore <id>`
- 런타임 미등록 → `multica daemon restart`하면 데몬이 다시 등록합니다

## WebSocket이 연결되지 않음

**증상**: 브라우저 콘솔에 `WebSocket is closed`가 기록됩니다. 페이지에 실시간 업데이트(작업 진행 상황, 댓글, 인박스)가 표시되지 않아 새로고침해야 보입니다. 백엔드 작업은 여전히 실행됩니다.

**가능한 원인**:

1. **Origin 검사 실패** — 여러분의 프런트엔드 도메인이 서버의 CORS 허용 목록에 없습니다. 기본 허용 목록에는 `localhost:3000/5173/5174`만 포함되며, 공개 인터넷에서 자체 호스팅하려면 `FRONTEND_ORIGIN`이 필요합니다
2. **프로토콜 불일치** — `https://` 프런트엔드에는 `wss://`가 필요하고, HTTP는 `ws://`를 사용합니다
3. **리버스 프록시가 WebSocket 업그레이드를 활성화하지 않음** — Nginx / Envoy / HAProxy는 기본적으로 `Upgrade` 헤더를 전달하지 않습니다
4. **JWT 쿠키 만료 또는 누락** — 30일 만료 후 다시 로그인하지 않음

**진단 방법**:

- 브라우저 DevTools → Network → "WS"로 필터링하여 연결 상태와 상태 코드를 확인하세요
- 서버 로그에서 `"rejected origin"` / `"websocket"`을 grep하세요 — origin 문제라면 명시적으로 표기됩니다
- `curl -i http://<server-host>:8080/ws`는 `101 Switching Protocols`를 반환해야 합니다(`Upgrade` 헤더 포함)

**해결 방법**:

- Origin 오류 → 서버의 `.env`에 `FRONTEND_ORIGIN=https://multica.yourdomain.com`을 설정(또는 쉼표로 구분한 `CORS_ALLOWED_ORIGINS`)하고 서버를 재시작하세요
- 프로토콜 불일치 → `FRONTEND_ORIGIN`의 프로토콜이 프런트엔드와 일치하는지 확인하세요
- 리버스 프록시 → Nginx에 `proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";`를 추가하세요
- 쿠키 만료 → 페이지를 새로고침하고 다시 로그인하세요

## 이메일이 수신되지 않음

**증상**: 로그인 또는 초대 수락 중 이메일을 제출했는데, 인박스에도 스팸 폴더에도 인증 코드가 없습니다.

**먼저 서버가 어떤 제공자를 활성 상태로 인식하는지 확인하세요.** 시작 시 백엔드는 다음 중 하나를 출력합니다:

- `EmailService: SMTP relay <host>:<port> from=<addr>` — SMTP 사용(`SMTP_HOST`가 비어 있지 않으면 Resend보다 우선)
- `EmailService: Resend API from=<addr>` — Resend 사용
- `EmailService: DEV mode — codes printed to stdout …` — 제공자가 구성되지 않음

```bash
docker compose -f docker-compose.selfhost.yml logs backend | grep "EmailService:"
```

기대했던 줄이 보이지 않는다면 환경 변수가 프로세스에 도달하지 않은 것입니다 — `.env`와 `docker compose -f docker-compose.selfhost.yml exec backend env | grep -E 'RESEND_|SMTP_'`를 확인하세요. 이 시작 로그 줄에는 자격 증명이 절대 기록되지 않습니다.

### Resend가 활성 제공자일 때

**가능한 원인**:

1. **`RESEND_API_KEY`가 설정되지 않음** — 서버는 조용히 폴백하여 **코드를 자체 stdout에 기록**하며 오류를 내지 않습니다. 프로덕션에서 쉽게 빠질 수 있는 함정입니다
2. **Resend API 키가 유효하지 않거나 할당량 소진** — 서버 로그에 `"failed to send verification code"`가 표시됩니다
3. **`RESEND_FROM_EMAIL`의 도메인이 Resend에서 검증되지 않음** — Resend가 발송을 거부합니다
4. **이메일은 발송되었으나 수신자 ISP가 스팸으로 표시** — Resend 대시보드와 스팸 폴더를 확인하세요

**진단 방법**:

- 서버 로그에서 `"[DEV] Verification code for"`를 grep하세요 — 있다면 Resend가 구성되지 않았고 코드가 stdout에 기록된 것입니다
- [Resend 대시보드](https://resend.com/) → Emails에서 발송 이력을 확인하세요
- `RESEND_FROM_EMAIL`의 도메인이 Resend 콘솔의 "Verified Domains" 목록에 나타나는지 확인하세요

**해결 방법**:

- API 키 누락 → [로그인 및 회원가입 구성 → 이메일 동작 방식](/auth-setup#how-email--verification-code-sign-in-works)을 따라 구성한 뒤 서버를 재시작하세요
- 도메인 미검증 → Resend 콘솔에서 DNS 검증 절차를 진행하세요(SPF / DKIM 레코드 추가)
- 긴급 상황(내부 테스트) → 서버 로그에서 `[DEV]` 아래에 출력된 코드를 복사하세요

### SMTP가 활성 제공자일 때

SMTP 경로는 모든 실패를 실패한 단계와 함께 감싸므로, 서버 로그가 이미 relay가 어느 단계에서 세션을 거부했는지 알려줍니다. `"failed to send verification email"` / `"failed to send invitation email"`을 grep하고 감싸진 오류를 확인하세요:

| 기록된 오류 | 의미 | 해결 방법 |
|---|---|---|
| `smtp dial <host>:<port>: dial tcp …: connect: connection refused` / `i/o timeout` | 백엔드 컨테이너가 relay에 도달할 수 없음 — 잘못된 host, 잘못된 port, 방화벽, 또는 relay가 수신 대기 중이 아님 | 컨테이너 내부에서 `SMTP_HOST` / `SMTP_PORT`가 해석되는지 확인하세요(`docker compose -f docker-compose.selfhost.yml exec backend nslookup <host>` 및 `nc -vz <host> <port>`). Multica를 실행하는 호스트에서 relay로 향하는 방화벽을 여세요 |
| `smtp starttls: x509: certificate signed by unknown authority`(또는 `certificate is not valid for any names`) | relay가 사설 CA / 자체 서명 인증서를 사용하며, 컨테이너의 신뢰 저장소가 이를 거부함 | CA를 컨테이너에 설치하거나, relay가 신뢰할 수 있는 네트워크 구간에서 도달 가능함을 확인한 후에만 `SMTP_TLS_INSECURE=true`를 설정하세요 |
| `smtp auth: 535 5.7.8 Authentication credentials invalid`(또는 `534`/`530`) | `SMTP_USERNAME` / `SMTP_PASSWORD`가 잘못되었거나, relay가 `PLAIN`이 아닌 다른 인증 방식을 요구함 | 메일 관리자에게 서비스 계정 자격 증명을 다시 확인하세요. Exchange 익명 내부 relay의 경우 둘 다 비워 두세요(`SMTP_USERNAME=`, `SMTP_PASSWORD=`) |
| `smtp MAIL FROM: 550 5.7.1 Client does not have permissions to send as this sender` | relay가 `RESEND_FROM_EMAIL`을 봉투 발신자로 수락하지 않음 — 전형적인 Exchange "anonymous users not allowed" 또는 DMARC 정렬 문제 | `RESEND_FROM_EMAIL`을 relay가 수락하는 도메인으로 설정하세요. Exchange에서는 receive connector에서 원본 IP에 `ms-Exch-SMTP-Accept-Any-Sender`를 부여하세요 |
| `smtp RCPT TO <addr>: 550 5.7.1 Unable to relay` | relay의 receive connector가 여러분의 서브넷이 외부 수신자에게 중계하는 것을 허용하지 않음(외부 도메인과 통신하는 익명 내부 relay에서 가장 흔함) | 초대를 내부 수신자로 제한하거나, Multica 호스트의 서브넷을 Exchange "Anonymous Users → Relay" 권한 목록에 추가하세요 |
| `smtp DATA` / `smtp write body` / `smtp end data` | 세션은 수락되었으나 relay가 본문을 폐기함 — 보통 메시지 크기 제한, 콘텐츠 필터링, 또는 전송 중 연결 재설정 때문 | relay 로그에서 동일한 `Message-ID`(로그에는 `<unixnano>@<host>` 형식)를 확인하세요. 필요하면 메시지 크기 제한을 높이세요 |

`MAIL FROM`, `RCPT TO`, `DATA` 오류는 항상 relay의 응답 코드와 함께 기록되므로 반대편의 Exchange / Postfix 로그와 대조할 수 있습니다. 인증 코드와 초대 토큰은 감싸진 오류에 **절대** 포함되지 않습니다.

**진단 방법**:

- 시작 시 `"EmailService: SMTP relay"`를 한 번 grep하고, 런타임 실패에 대해서는 `"failed to send"`를 grep하세요
- 백엔드 컨테이너 내부에서 연결성을 점검하세요: `docker compose -f docker-compose.selfhost.yml exec backend sh -c 'nc -vz $SMTP_HOST $SMTP_PORT'`
- 환경 변수가 프로세스에 도달했는지 확인하세요: `docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP_`(출력에 비밀번호가 포함되므로 신뢰할 수 있는 셸에서만 실행하세요)

**해결 방법**:

- 잘못된 host / port → `SMTP_HOST` / `SMTP_PORT`를 조정하고 백엔드를 재시작하세요. 지원되는 relay 모드는 [인증 설정 → Option B: SMTP relay](/auth-setup)를 참고하세요
- 인증서 불일치 → relay의 CA를 컨테이너에 설치하거나, 신뢰할 수 있는 네트워크 구간에서 임시로 `SMTP_TLS_INSECURE=true`를 설정하세요
- 인증 실패 → 자격 증명을 다시 확인하세요. 익명 내부 relay의 경우 `SMTP_USERNAME`과 `SMTP_PASSWORD`를 비워 두세요
- `Unable to relay` → 내부 수신자로 제한하거나, Exchange receive connector에서 Multica 호스트의 IP에 중계 권한을 부여하세요

## 고정 로컬 테스트 코드가 동작하지 않음

**증상**: 자체 호스팅 인스턴스에서 `888888` 같은 고정 로컬 테스트 코드로 로그인하려 했지만 `invalid or expired code`로 거부됩니다.

**가능한 원인**(상호 배타적):

1. **`MULTICA_DEV_VERIFICATION_CODE`가 비어 있음** — 고정 코드는 기본적으로 비활성화되어 있습니다
2. **`APP_ENV=production`** — 이것은 **올바른** 프로덕션 구성입니다. 고정 로컬 테스트 코드는 프로덕션에서 무시됩니다
3. **구성된 코드가 6자리가 아님** — 이 단축 코드는 6자리 값만 허용합니다

**진단 방법**:

```bash
cat .env | grep -E 'APP_ENV|MULTICA_DEV_VERIFICATION_CODE'
docker exec <container> env | grep -E 'APP_ENV|MULTICA_DEV_VERIFICATION_CODE'
```

인박스(스팸 포함)에서 실제 인증 코드를 확인하세요.

**해결 방법**:

- 프로덕션에서는 `MULTICA_DEV_VERIFICATION_CODE`를 비워 두고, Resend를 구성하여 실제 코드를 사용하세요
- 로컬 개발이나 내부 테스트의 경우 서버 로그에서 생성된 코드를 복사하거나, `APP_ENV=development`와 `MULTICA_DEV_VERIFICATION_CODE=888888`을 설정하세요 — 공개 인스턴스에서는 고정 코드를 절대 활성화하지 마세요(자세한 내용은 [로그인 및 회원가입 구성 → 고정 로컬 테스트 코드](/auth-setup#fixed-local-testing-codes) 참고)

## 사용량 대시보드가 0으로 유지됨

**증상**: 에이전트가 작업을 완료하고 원시 토큰 사용량이 데이터베이스에 기록되지만, **설정 → 사용량**과 **설정 → 런타임**에서 입력 / 출력 / 비용이 전부 0으로 표시됩니다. 이것은 조용히 발생하는 현상으로 — 백엔드 로그에 오류가 없습니다.

**가능한 원인**:

1. **`rollup_task_usage_hourly()`가 클레임되지 않음** — 사용량 / 런타임 대시보드는 파생 테이블 `task_usage_hourly`에서 읽으며, 이 테이블은 해당 함수로 채워집니다. MUL-2957부터 백엔드는 DB 기반 스케줄러(`sys_cron_executions`)를 통해 인프로세스로 rollup을 실행합니다. 오래된 빌드, 적용되지 않은 migration `113`, 또는 레플리카가 남아있지 않은 장기간의 백엔드 중단이 있으면 최근 SUCCESS 행이 없는 테이블이 남을 수 있습니다.
2. **`pg_cron`이 호환성 용도로 구성되었지만 잘못된 데이터베이스를 가리킴** — `pg_cron.database_name`의 기본값은 `postgres`입니다. Multica 데이터베이스 이름이 다르면 스케줄된 작업이 `rollup_task_usage_hourly()`를 전혀 보지 못합니다. 인프로세스 스케줄러는 이에 의존하지 않지만, 인프로세스 스케줄러를 제거하고 `pg_cron`에 의존한다면 DB 이름이 일치해야 합니다.
3. **핸들러가 클레임되지만 조용히 오류를 냄** — 예: 마이그레이션이 일부만 적용되어 SQL 함수가 누락되었거나, DB 역할 / search_path가 잘못 구성됨. `sys_cron_executions`의 FAILED 감사 행을 확인하세요.

**진단 방법**:

```sql
-- Confirm raw events exist but the hourly table is empty.
SELECT count(*) AS raw_rows FROM task_usage;
SELECT count(*) AS hourly_rows FROM task_usage_hourly;

-- Inspect the in-process scheduler's audit log.
SELECT plan_time, status, attempt, runner_id,
       error_code, error_msg, started_at, finished_at
  FROM sys_cron_executions
 WHERE job_name = 'rollup_task_usage_hourly'
 ORDER BY plan_time DESC
 LIMIT 20;

-- Watermark — if this is 1970-01-01, the rollup has never run.
SELECT watermark_at FROM task_usage_hourly_rollup_state;

-- Compatibility path: if you previously registered pg_cron, confirm
-- it is (or isn't) available and pointing at the right database.
SELECT * FROM pg_available_extensions WHERE name = 'pg_cron';
SHOW shared_preload_libraries;
SELECT jobname, schedule, database, active FROM cron.job;
```

**해결 방법**:

- 적어도 하나의 백엔드 레플리카에서 스케줄러가 실제로 실행 중인지 확인하세요 — 30초마다 `sys_cron_executions`의 `rollup_task_usage_hourly`에 SUCCESS 행이 추가되어야 합니다.
- SQL 경로를 검증하기 위해 rollup을 수동으로 한 번 호출하세요: `SELECT rollup_task_usage_hourly();` — 대시보드를 새로고침하세요. 숫자가 나타나면 SQL 함수는 정상이며, 문제는 스케줄러 클레임 경로에 있습니다.
- migration `113_sys_cron_executions`가 아직 적용되지 않았다면 백엔드를 재시작해 마이그레이션을 실행하거나 수동으로 `migrate up`을 호출하세요.
- 인프로세스 스케줄러 이전의 레거시 `pg_cron` 이력이 있어도 SQL 함수가 내부적으로 advisory lock 4246을 잡고 있어 두 경로가 이중 쓰기할 수 없습니다 — 선택적 `cron.unschedule` 정리는 [자체 호스팅 빠른 시작 → 사용량 롤업](/self-host-quickstart#7-usage-rollup-no-operator-action-required)을 참고하세요.

## 마이그레이션 `103`이 `refusing to drop legacy daily rollups`로 실패함

**증상**: `v0.3.4`에서 `v0.3.5+`로 업그레이드할 때 백엔드 컨테이너가 시작되지 않거나(또는 `migrate up`이 중단됨) 다음과 같은 오류가 발생합니다:

```text
ERROR: refusing to drop legacy daily rollups:
  task_usage_hourly_rollup_state.watermark_at (1970-01-01 ...) trails
  task_usage latest event (...) by more than 01:00:00 — backfill is
  incomplete or pg_cron is not running. Run cmd/backfill_task_usage_hourly
  (and let pg_cron catch up) before re-running migrate
```

**가능한 원인**: 이것은 마이그레이션 `103`의 fail-closed 가드입니다. `task_usage_hourly`가 원시 `task_usage`를 따라잡을 때까지 레거시 daily rollup 삭제를 거부합니다. 기존 행이 존재하고 rollup 워터마크가 여전히 epoch에 머물러 있을 때 — 즉 아직 어떤 이력도 hourly 테이블로 rollup되지 않았을 때 — 이 가드가 발동합니다.

MUL-2957부터 migrate 명령은 migration `103`을 적용하기 직전에 멱등한 월별 슬라이스 backfill(advisory lock 4246 보호)을 자동으로 실행하므로, v0.3.4 → v0.3.5+ 직접 업그레이드는 단일 `migrate up` 호출로 완료됩니다. 그래도 이 오류가 보인다면, MUL-2957 이전 바이너리를 사용 중이거나 훅 자체가 실패한 것입니다 — migrate 로그에서 `task_usage hourly rollup hook` 로그를 확인하세요.

**해결 방법**:

1. MUL-2957 이전 바이너리를 사용 중이고 바이너리를 먼저 업그레이드할 수 없다면, 같은 데이터베이스에 대해 스탠드얼론 backfill을 실행하세요(멱등하며, 중단해도 안전하고, 다시 실행해도 안전합니다):

   ```bash
   # Docker Compose
   docker compose -f docker-compose.selfhost.yml exec backend \
     ./backfill_task_usage_hourly --sleep-between-slices=2s

   # Kubernetes
   kubectl -n multica exec deploy/multica-backend -- \
     ./backfill_task_usage_hourly --sleep-between-slices=2s
   ```

2. 업그레이드를 다시 실행하세요 — 백엔드 컨테이너를 재시작하는 것으로 충분하며, 마이그레이션은 시작 시 실행됩니다. 이제 가드가 최신 워터마크를 확인하고 `103`을 적용하도록 허용합니다.
3. 인프로세스 스케줄러가 이후 워터마크를 계속 진행시킵니다 — [자체 호스팅 빠른 시작 → 사용량 롤업](/self-host-quickstart#7-usage-rollup-no-operator-action-required)을 참고하세요.

`--sleep-between-slices=2s`는 수년 치 이력이 있는 프로덕션 데이터베이스에서 적절한 기본값입니다. 최근 N개월만 보관하고 더 오래된 버킷을 영구히 포기해도 괜찮다면 `--months-back N --force-partial`을 사용하세요.

## 포트 충돌

**증상**: `multica server`나 `multica daemon start`가 `address already in use`로 실패합니다.

**가능한 원인**:

1. **서버 포트 점유됨**(기본값 `8080`)
2. **데몬 health 포트 점유됨**(기본값 `19514`, 프로필마다 해시로 오프셋됨)
3. **Web dev 서버 포트 충돌**(`3000` / `5173`)
4. **포트에 대한 권한 부족**(`< 1024` 특권 포트 바인딩에는 sudo 필요)

**진단 방법**:

```bash
lsof -i :8080        # macOS / Linux
netstat -ano | findstr :8080    # Windows
```

**해결 방법**:

- 충돌하는 프로세스를 종료하거나(`kill -9 <PID>`), `PORT=9000`으로 포트를 변경하세요
- 80 / 443을 사용하려면 → 직접 바인딩하지 말고, 앞에 리버스 프록시(Nginx / Caddy)를 두어 높은 포트로 전달하세요

## 로그를 찾는 위치

| 구성 요소 | 위치 | 명령 |
|---|---|---|
| **데몬** | `~/.multica/daemon.log`(백그라운드 모드) 또는 포그라운드 stdout | `multica daemon logs -f --lines 100` |
| **서버(Docker)** | 컨테이너 stdout | `docker logs -f <container>` |
| **서버(systemd)** | journal | `journalctl -u multica-server -f` |
| **프런트엔드(dev)** | `pnpm dev`를 실행 중인 터미널 | 직접 확인 |
| **프런트엔드(브라우저)** | DevTools → Console | `F12`를 누르세요 |

더 자세한 데몬 로그가 필요하면, 데몬을 백그라운드에서 포그라운드로 옮기세요: `multica daemon stop && multica daemon start --foreground`.
