Multica Docs

자체 호스팅 빠른 시작

Docker로 자체 서버나 기기에서 Multica를 실행합니다(Kubernetes에서는 Helm 사용 가능). 약 10분 소요됩니다.

이 페이지는 Docker로 Multica 서버(백엔드 + 프런트엔드 + PostgreSQL)를 자체 기기나 서버에서 실행하는 과정을 안내합니다. 완료하면 워크스페이스, 이슈, 댓글, 에이전트 구성을 비롯한 데이터가 완전히 본인의 통제하에 놓입니다.

에이전트 실행은 여전히 로컬에서 실행하는 데몬과 그 기기에 설치된 AI 코딩 도구에 의존합니다 — Cloud와 완전히 동일합니다. 자체 호스팅은 서버 계층을 교체할 뿐, 실행 계층을 교체하지는 않습니다.

사전 요구 사항

  • Docker가 설치되어 있고 docker compose를 실행할 수 있어야 함
  • Git(선택 사항이지만 소스를 받아올 수 있으므로 권장)
  • 계속 켜둘 수 있는 기기(로컬 / 내부 네트워크 / 클라우드 호스트 모두 가능)
  • 데몬을 실행하는 기기에 AI 코딩 도구가 최소 한 개 설치되어 있어야 함(서버를 실행하는 기기일 필요는 없으며, 개발용 노트북도 됩니다)

1. 프로젝트 받아오기 및 백엔드 시작하기

이미 Kubernetes를 쓰고 계신가요? Docker를 건너뛰고 Helm 차트를 사용하세요 — 아래 Kubernetes 배포로 이동한 다음, 첫 로그인을 위해 4단계로 돌아오세요.

git clone https://github.com/multica-ai/multica.git
cd multica
make selfhost

make selfhost는 다음을 수행합니다.

  1. .env가 없으면 .env.example로부터 생성하며 무작위 JWT_SECRET을 함께 만듭니다
  2. 공식 Docker 이미지(PostgreSQL, Multica backend, Multica frontend)를 받아옵니다
  3. docker-compose.selfhost.yml을 사용해 모든 서비스를 시작합니다
  4. 백엔드의 /health 엔드포인트가 준비될 때까지 기다립니다

시작 이후 프로덕션 프로브에는, 데이터베이스나 migration 문제 시 검사가 실패하도록 하려면 /readyz를 사용하세요.

백엔드 컨테이너는 시작 시 데이터베이스 migration을 자동으로 실행합니다(docker/entrypoint.sh가 서버 시작 전에 ./migrate up을 실행) — 백엔드 로그에서 migration 출력을 확인할 수 있습니다. 버전 업그레이드도 같은 방식으로 처리됩니다.

이미지가 아직 공개되지 않았나요? make selfhost가 이미지를 받아오지 못한다면 아직 릴리스되지 않은 버전 태그에 있을 수 있습니다. 안정 릴리스로 전환하거나 소스에서 빌드하세요: make selfhost-build.

시작되면 다음과 같습니다.

포트는 127.0.0.1에서만 수신합니다. docker-compose.selfhost.yml은 공개된 모든 포트를 loopback에 바인딩합니다 — ss -tlnp에서는 0.0.0.0:8080이 보이지 않으며, 설계상 다른 기기에서는 서비스에 접근할 수 없습니다. 기본 JWT_SECRET과 Postgres 자격 증명이 공개 인터넷에 노출되어서는 절대 안 됩니다. 기기 간 접근이 필요하면 TLS를 종료하는 리버스 프록시를 스택 앞에 두세요 — 5b단계 — 기기 간: 리버스 프록시를 앞에 두기를 참고하세요.

2. 중요: 프로덕션 안전 설정 유지하기

docker-compose.selfhost.yml은 기본적으로 APP_ENVproduction으로 설정하고 MULTICA_DEV_VERIFICATION_CODE를 비워 두므로, 공개 인스턴스에는 고정 코드가 없습니다.

MULTICA_DEV_VERIFICATION_CODE는 로컬 또는 비공개 테스트 자동화에서만 설정하세요. APP_ENV가 non-production일 때 고정 코드가 활성화되어 있으면, 코드를 요청할 수 있는 누구나 그 고정 값으로 로그인할 수 있습니다. 인증 설정 → 고정 로컬 테스트 코드를 참고하세요.

공개 배포 전에는 .envAPP_ENV=production이 설정되어 있고 MULTICA_DEV_VERIFICATION_CODE가 비어 있는지 반드시 확인하세요.

3. 이메일 서비스 구성하기(선택 사항이지만 권장)

이메일을 구성하지 않으면 사용자가 이메일로 인증 코드를 받을 수 없으며, 서버가 생성된 코드를 대신 stdout에 출력합니다.

두 가지 전송 백엔드를 지원합니다 — 네트워크에 맞는 것을 고르세요.

옵션 A — Resend(클라우드 / 공개 인터넷 배포):

  1. Resend에 가입하고 API key를 받습니다

  2. 본인이 관리하는 발송 도메인을 인증합니다

  3. .env에 다음을 설정합니다.

    RESEND_API_KEY=re_xxxxxxxxxxxx
    RESEND_FROM_EMAIL=noreply@yourdomain.com

옵션 B — SMTP relay(내부 네트워크 / 온프레미스):

배포 환경이 api.resend.com에 접근할 수 없거나, 이미 내부 메일 릴레이(Microsoft Exchange, Postfix, 온프레미스 SendGrid 등)가 있는 경우에 사용하세요. 둘 다 설정된 경우 SMTP_HOST가 Resend보다 우선하므로, 인증 및 초대 메일이 내부 릴레이에 머무릅니다. STARTTLS는 광고될 때 자동으로 업그레이드됩니다. 465 포트(SMTPS / 암묵적 TLS)는 연결 직후의 TLS 핸드셰이크를 자동으로 활성화하며, SMTP_TLS=implicit(별칭: smtps, ssl)는 비표준 SMTPS 포트에서 강제로 활성화합니다.

익명 Exchange 내부 릴레이(포트 25) — 호스트가 IP로 신뢰되며 자격 증명 없이 제출하는 경우:

SMTP_HOST=exchange.internal.example.com
SMTP_PORT=25
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_INSECURE=false
RESEND_FROM_EMAIL=noreply@yourdomain.com  # From: 헤더로도 재사용됨

인증 제출(포트 587, STARTTLS) — 릴레이에 서비스 계정이 필요하며, STARTTLS가 광고될 때 자동으로 업그레이드되는 경우:

SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587
SMTP_USERNAME=multica
SMTP_PASSWORD=...
SMTP_TLS_INSECURE=false        # 비공개 CA / 자체 서명 인증서일 때만 true로 설정
RESEND_FROM_EMAIL=noreply@yourdomain.com

암묵적 TLS / SMTPS(포트 465) — STARTTLS를 광고하지 않는 알리바바 클라우드 / 텐센트 기업 메일 같은 제공자용. 포트 465는 암묵적 TLS를 자동으로 활성화하므로, 여기서 SMTP_TLS는 생략할 수 있습니다:

SMTP_HOST=smtp.qiye.aliyun.com
SMTP_PORT=465
SMTP_USERNAME=multica@yourdomain.com
SMTP_PASSWORD=...
SMTP_TLS=implicit              # optional on 465; required on a non-standard SMTPS port
RESEND_FROM_EMAIL=noreply@yourdomain.com

공개 IP에서 보내는 기본 localhost greeting을 거부하는 엄격한 공개 relay(예: Google Workspace smtp-relay.gmail.com) 의 경우, relay가 기대하는 FQDN으로 SMTP_EHLO_NAME을 설정하세요 — 그렇지 않으면 연결이 끊기고, 이는 이후 명령에서 불투명한 EOF로 나타납니다. 기본값은 컨테이너 호스트명이며, 보통 유효한 FQDN이 아닙니다:

SMTP_HOST=smtp-relay.gmail.com
SMTP_PORT=587
SMTP_EHLO_NAME=mail.yourdomain.com   # relay가 받아들이는 FQDN; 기본값은 (FQDN이 아닌) 컨테이너 호스트명
RESEND_FROM_EMAIL=noreply@yourdomain.com

그런 다음 재시작합니다: docker compose -f docker-compose.selfhost.yml restart backend. 재시작 시 백엔드는 어떤 제공자를 선택했는지 출력합니다(EmailService: SMTP relay … / Resend API / DEV mode) — 자격 증명은 절대 로그에 남지 않으므로, 이 줄은 도움을 요청할 때 공유해도 안전합니다.

추가 인증 구성(OAuth, 가입 허용 목록)과 전체 SMTP 변수 레퍼런스는 인증 설정환경 변수 → 이메일을 참고하세요.

4. 첫 로그인 + 워크스페이스 생성

http://localhost:3000을 엽니다.

  • 이메일을 입력합니다
  • 구성한 이메일 백엔드(Resend 또는 SMTP relay)에서 인증 코드를 받습니다. 둘 다 구성하지 않았다면 서버 컨테이너 stdout에서 복사하세요 — [DEV] Verification code 줄을 찾으면 됩니다
  • non-production 비공개 인스턴스에서 MULTICA_DEV_VERIFICATION_CODE=888888을 명시적으로 설정한 경우가 아니라면 888888을 사용하지 마세요
  • 로그인하고 첫 워크스페이스를 생성합니다

5. CLI를 자체 서버로 연결하기

CLI 설치는 Cloud 빠른 시작 → 2. CLI 설치와 동일합니다 — Homebrew / 스크립트 / PowerShell 중 하나를 고르세요.

5a. 같은 기기

CLI와 서버가 같은 호스트에서 실행된다면 기본값으로 이미 동작합니다.

multica setup self-host

이렇게 하면 CLI가 http://localhost:8080(백엔드)과 http://localhost:3000(프런트엔드)을 가리키고, 브라우저 로그인을 안내하며, PAT를 로컬에 저장하고, 데몬을 자동으로 시작합니다.

5b. 기기 간: 리버스 프록시를 앞에 두기

compose 스택은 127.0.0.1에서만 수신하므로, 다른 기기에 있는 데몬은 http://<server-ip>:8080에 직접 연결할 수 없습니다 — 그리고 그렇게 되기를 원해서도 안 됩니다. 그렇지 않으면 기본 JWT_SECRET이 공개 인터넷에서 접근 가능해지기 때문입니다. 서버에 TLS를 종료하고 127.0.0.1:8080(백엔드)과 127.0.0.1:3000(프런트엔드)으로 전달하는 리버스 프록시를 두고, CLI를 공개 HTTPS URL로 연결하세요.

multica setup self-host \
  --server-url https://<your-domain> \
  --app-url https://<your-domain>

단일 호스트네임에서 프런트엔드와 백엔드를 모두 앞단에 두는(데몬과 웹 앱 모두에 필요한 WebSocket 지원 포함) 최소 Caddyfile은 다음과 같습니다.

multica.example.com {
    # WebSocket route — must come before the catch-all
    @ws path /ws /ws/*
    handle @ws {
        reverse_proxy 127.0.0.1:8080 {
            flush_interval -1
        }
    }

    # Backend API
    handle /api/* {
        reverse_proxy 127.0.0.1:8080
    }

    # Everything else → frontend
    reverse_proxy 127.0.0.1:3000
}

프록시를 올린 후에는 서버의 .envFRONTEND_ORIGIN=https://multica.example.com을 설정하고 백엔드를 재시작하세요 — 그렇지 않으면 WebSocket origin 검사가 브라우저를 거부합니다(문제 해결 → WebSocket이 연결되지 않음).

Cloudflare Tunnel도 견고한 선택지입니다 — 호스트에 어떤 포트도 노출하지 않고도 TLS와 공개 호스트네임을 제공합니다. Nginx로 동등하게 구성하는 방법(app. / api.을 별도 호스트네임으로 분리, WebSocket용 proxy_set_header Upgrade)도 똑같이 잘 동작합니다. 핵심 요구 사항은 TLS 종료와 /ws에서의 Upgrade 헤더 전달입니다.

6. 에이전트 생성 + 첫 작업 할당

Cloud와 동일한 흐름입니다 — Cloud 빠른 시작 → 5-6단계를 참고하세요.

7. 사용량 롤업 스케줄링(사용량 대시보드에 필수)

사용량 / 런타임 대시보드는 rollup_task_usage_hourly()가 채우는 파생 테이블 task_usage_hourly에서 데이터를 읽습니다. 번들된 pgvector/pgvector:pg17 Postgres 이미지에는 pg_cron이 포함되어 있지 않으며, 백엔드도 롤업을 인프로세스로 실행하지 않습니다. rollup_task_usage_hourly()를 스케줄링하는 것이 없으면, 원시 task_usage 행은 계속 들어오는데 대시보드는 영원히 0에 머무릅니다.

지원되는 옵션 중 하나를 고르세요 — 하나만 있으면 됩니다.

옵션 A — 외부 cron / systemd-timer(가장 간단함). 임의의 외부 스케줄러에서 5분마다 롤업을 실행합니다. 멱등하고 워터마크 기반이므로, 놓친 틱은 따라잡습니다.

# /etc/cron.d/multica-rollup — every 5 minutes
*/5 * * * * root docker compose -f /path/to/multica/docker-compose.selfhost.yml \
  exec -T postgres psql -U multica -d multica \
  -c "SELECT rollup_task_usage_hourly();" >/dev/null

옵션 B — Postgres를 pg_cron이 포함된 이미지로 교체. docker-compose.selfhost.ymlpgvector/pgvector:pg17pgvectorpg_cron을 모두 갖춘 이미지(supabase/postgres 또는 커스텀 빌드)로 교체하고, shared_preload_libraries=pg_cron을 설정한 뒤 재시작하고, 작업을 한 번 등록합니다.

CREATE EXTENSION IF NOT EXISTS pg_cron;
SELECT cron.schedule(
  'rollup_task_usage_hourly',
  '*/5 * * * *',
  $$SELECT rollup_task_usage_hourly()$$
);

옵션 C — 먼저 히스토리 백필(업그레이드 경로). v0.3.4 → v0.3.5+로 업그레이드하는 중이고 기존 task_usage 행이 있다면, migration 103이 hourly 테이블이 시드될 때까지 refusing to drop legacy daily rollups: ...와 함께 migrate up을 중단합니다. 번들된 백필을 한 번 실행한 다음, 옵션 A 또는 B를 설정하세요.

docker compose -f docker-compose.selfhost.yml exec backend \
  ./backfill_task_usage_hourly --sleep-between-slices=2s

--sleep-between-slices=2s는 바쁜 DB에서 읽기 부하를 조절합니다. 완료된 후 백엔드 컨테이너를 재시작하면(시작 시 migration이 실행됨) 업그레이드가 완료됩니다.

전체 레퍼런스 — Kubernetes CronJob 템플릿과 업그레이드 순서 포함 — 는 저장소의 SELF_HOSTING_ADVANCED.md → Usage Dashboard Rollup에 있습니다.

Kubernetes 배포(대체 방안)

이미 Kubernetes 클러스터를 운영 중이라면, 저장소에는 deploy/helm/multica/에 Helm 차트도 포함되어 있습니다. k8s용 make selfhost에 해당합니다 — 동일한 백엔드 이미지, 프런트엔드 이미지, pgvector/pgvector:pg17 Postgres를 Deployment / Service / Ingress로 패키징하고, values.yaml로 렌더링한 하나의 ConfigMap을 함께 제공합니다. k3s + Traefik + local-path를 기준으로 작성되었으며, Ingress 컨트롤러와 기본 ReadWriteOnce StorageClass가 있는 모든 클러스터에서 동작합니다.

이 차트는 시크릿 값을 템플릿화하지 않습니다. multica-secrets라는 이름의 Secret을 이름으로 참조하므로, 실제 JWT / DB / Resend / Google 키가 git이나 values.yaml에 들어갈 필요가 전혀 없습니다. 네임스페이스와 Secret을 kubectl로 한 번 생성하세요.

kubectl create namespace multica

kubectl -n multica create secret generic multica-secrets \
  --from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
  --from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
  --from-literal=RESEND_API_KEY="" \
  --from-literal=GOOGLE_CLIENT_SECRET="" \
  --from-literal=CLOUDFRONT_PRIVATE_KEY="" \
  --from-literal=MULTICA_DEV_VERIFICATION_CODE=""

그런 다음 차트를 설치합니다.

git clone https://github.com/multica-ai/multica.git
cd multica
helm install multica deploy/helm/multica -n multica

기본값은 호스트네임 multica.dev.lan(web)과 api.multica.dev.lan(백엔드)을 가정합니다. 이것들을 /etc/hosts(또는 로컬 DNS)에 추가해, Ingress에 도달 가능한 임의의 노드 IP를 가리키도록 하세요. 다른 호스트네임을 사용하려면 deploy/helm/multica/values.yaml을 복사한 뒤 ingress.frontend.host / ingress.backend.host와 그에 대응하는 backend.config.appUrl / frontendOrigin / localUploadBaseUrl / googleRedirectUri를 편집하고, -f my-values.yaml로 설치하세요.

콜드 클러스터에서는 백엔드가 Postgres를 기다리고 migration을 실행하는 동안 몇 분간 Running 상태이지만 Ready는 아닐 수 있습니다 — startupProbe가 이를 흡수하므로 파드는 재시작되지 않습니다. Ready가 되면:

curl -H "Host: api.multica.dev.lan" http://<ingress-ip>/healthz
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}

그런 다음 http://multica.dev.lan을 열고 위의 4단계 — 첫 로그인에서 이어서 진행하세요. CLI를 Ingress 호스트네임으로 연결합니다.

multica setup self-host \
  --server-url http://api.multica.dev.lan \
  --app-url http://multica.dev.lan

차트를 변경하지 않고 최신 이미지만 받아오려면 kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontend를 실행하세요. 특정 Multica 릴리스를 고정하려면 values 파일에서 images.backend.tag / images.frontend.tag를 설정하고 helm upgrade를 실행하세요. helm -n multica uninstall multica는 워크로드를 제거하지만 PVC와 Secret은 유지합니다. kubectl delete namespace multica는 모든 것을 삭제합니다.

전체 레퍼런스 — 세 가지 로그인 모드, web 이미지에 빌드 타임에 굳혀진 REMOTE_API_URL에 대한 backend ExternalName 우회책, 리소스 제한, TLS — 는 저장소의 SELF_HOSTING.md에 있습니다.

자주 발생하는 문제

  • 백엔드가 시작되지 않음: docker compose -f docker-compose.selfhost.yml logs backend로 컨테이너 로그를 확인하세요. 보통 .env의 잘못된 DATABASE_URL 또는 JWT_SECRET이 원인입니다
  • 인증 코드를 받지 못함: 이메일 백엔드가 구성되지 않은 경우(Resend도 SMTP도 없음) → docker compose logs backend에서 [DEV] Verification code를 찾으세요
  • WebSocket이 연결되지 않음: 공개 배포에서는 반드시 FRONTEND_ORIGIN을 실제 프런트엔드 도메인으로 설정해야 합니다. 문제 해결 → WebSocket이 연결되지 않음을 참고하세요
  • 사용량 / 런타임 대시보드가 0에 머무름: rollup_task_usage_hourly()가 스케줄링되지 않고 있습니다 — 위의 7단계문제 해결 → 사용량 대시보드가 0으로 표시됨을 참고하세요
  • migrate uprefusing to drop legacy daily rollups로 실패함: v0.3.4 → v0.3.5+ 업그레이드 경로 가드입니다. 먼저 backfill_task_usage_hourly를 실행하세요 — 7단계 → 옵션 C를 참고하세요

다음 단계

  • 환경 변수 — 전체 env 레퍼런스
  • 인증 설정 — Resend / OAuth / 가입 허용 목록 상세
  • GitHub 연동 — GitHub App을 연결해 PR이 이슈에 자동 연결되고 머지 시 이슈가 닫히도록 설정
  • 문제 해결 — 문제가 생기면 여기서 시작하세요
  • 데스크톱 앱~/.multica/desktop.json을 통한 선택적 데스크톱 설정. 웹 프런트엔드 + CLI가 여전히 가장 빠른 자체 호스팅 경로입니다