자체 호스팅 빠른 시작
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 selfhostmake selfhost는 다음을 수행합니다.
.env가 없으면.env.example로부터 생성하며 무작위 JWT_SECRET을 함께 만듭니다- 공식 Docker 이미지(PostgreSQL, Multica backend, Multica frontend)를 받아옵니다
docker-compose.selfhost.yml을 사용해 모든 서비스를 시작합니다- 백엔드의
/health엔드포인트가 준비될 때까지 기다립니다
시작 이후 프로덕션 프로브에는, 데이터베이스나 migration 문제 시 검사가 실패하도록 하려면 /readyz를 사용하세요.
백엔드 컨테이너는 시작 시 데이터베이스 migration을 자동으로 실행합니다(docker/entrypoint.sh가 서버 시작 전에 ./migrate up을 실행) — 백엔드 로그에서 migration 출력을 확인할 수 있습니다. 버전 업그레이드도 같은 방식으로 처리됩니다.
이미지가 아직 공개되지 않았나요? make selfhost가 이미지를 받아오지 못한다면 아직 릴리스되지 않은 버전 태그에 있을 수 있습니다. 안정 릴리스로 전환하거나 소스에서 빌드하세요: make selfhost-build.
시작되면 다음과 같습니다.
- 프런트엔드: http://localhost:3000
- 백엔드: http://localhost:8080
포트는 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_ENV를 production으로 설정하고 MULTICA_DEV_VERIFICATION_CODE를 비워 두므로, 공개 인스턴스에는 고정 코드가 없습니다.
MULTICA_DEV_VERIFICATION_CODE는 로컬 또는 비공개 테스트 자동화에서만 설정하세요. APP_ENV가 non-production일 때 고정 코드가 활성화되어 있으면, 코드를 요청할 수 있는 누구나 그 고정 값으로 로그인할 수 있습니다. 인증 설정 → 고정 로컬 테스트 코드를 참고하세요.
공개 배포 전에는 .env에 APP_ENV=production이 설정되어 있고 MULTICA_DEV_VERIFICATION_CODE가 비어 있는지 반드시 확인하세요.
3. 이메일 서비스 구성하기(선택 사항이지만 권장)
이메일을 구성하지 않으면 사용자가 이메일로 인증 코드를 받을 수 없으며, 서버가 생성된 코드를 대신 stdout에 출력합니다.
두 가지 전송 백엔드를 지원합니다 — 네트워크에 맞는 것을 고르세요.
옵션 A — Resend(클라우드 / 공개 인터넷 배포):
-
Resend에 가입하고 API key를 받습니다
-
본인이 관리하는 발송 도메인을 인증합니다
-
.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
}프록시를 올린 후에는 서버의 .env에 FRONTEND_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.yml의 pgvector/pgvector:pg17을 pgvector와 pg_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 up이refusing to drop legacy daily rollups로 실패함:v0.3.4 → v0.3.5+업그레이드 경로 가드입니다. 먼저backfill_task_usage_hourly를 실행하세요 — 7단계 → 옵션 C를 참고하세요