WechatOnCloud(줄여서 WOC, "云微")는 NAS나 리눅스 서버 위에서 PC용 微信(위챗)을 통째로 돌리고, 그 화면을 브라우저로 원격 접속해서 쓰게 해주는 셀프호스팅 시스템입니다. 微信 데스크탑 앱은 한 글자도 수정하지 않고, 리눅스 컨테이너 안에서 "원본 그대로" 실행한 다음 화면만 웹으로 중계합니다.
핵심 구조는 한 문장으로 요약됩니다: 微信 실례 1개 = 컨테이너 1개. 컨테이너 안에서 Xvfb(가상 화면) + 원본 微信이 돌고, KasmVNC가 그 화면을 브라우저로 스트리밍합니다. 같은 실례에 여러 브라우저가 접속하면 = 같은 微信 세션을 공유하는 것이 됩니다.
옛날에는 微信을 쓰려면 내 노트북에 깔아야 했고, 그 노트북을 꺼두면 끝이었습니다. WOC는 微信을 항상 켜져 있는 서버 안의 가상 PC로 옮겨놓습니다. 휴대폰으로 한 번 QR을 찍어 로그인해두면, 집 데스크탑·회사 노트북·태블릿 어디서든 같은 주소를 열어 똑같은 대화창을 봅니다.
그리고 가장 앞에는 직접 만든 관리 패널(panel)이 문지기로 서 있어, 로그인·권한·실례 생성/삭제를 모두 통제합니다. 브라우저는 오직 이 패널하고만 대화합니다.
WOC는 TrendShift 주간 6위·월간 4위에 오른 셀프호스팅 프로젝트입니다. 微信은 같은 계정을 여러 PC에서 동시에 쓰기 어렵고, 데스크탑 세션이 자주 끊기며, 외부에서 접속하기 번거롭다는 오래된 불편이 있습니다. WOC는 이 문제를 "微信을 개조하지 않고" 컨테이너 + 원격 데스크탑 기술로 우회합니다.
微信 데스크탑은 내가 켜둔 그 컴퓨터에서만 돕니다. 외출 중 그 대화를 보려면 PC를 계속 켜두고 TeamViewer 같은 원격 도구로 들어가야 합니다. 상용 "클라우드폰(云手机)"은 월 구독료가 들고, 메신저 클라이언트를 개조하는 방식은 계정 차단 위험이 큽니다.
微信을 NAS/서버의 컨테이너로 옮기고 KasmVNC로 브라우저에 띄웁니다. 클라이언트를 수정하지 않아 차단 위험이 낮고, 패널에서 여러 微信 실례를 따로따로 만들어 사용자별로 접근 권한을 나눠 줄 수 있습니다(RBAC). 휴대폰처럼 홈 화면에 추가하는 PWA도 됩니다.
| 비교 항목 | WechatOnCloud | 상용 클라우드폰 | TeamViewer류 원격 | 단일 docker-wechat |
|---|---|---|---|---|
| 비용 | 무료/셀프호스팅 | 월 구독 | 개인 무료/상용 유료 | 무료 |
| 여러 微信 동시 관리 | 멀티 인스턴스 | 대수만큼 과금 | PC 1대=1세션 | 보통 1개 |
| 접근 권한 관리 | 사용자별 RBAC | 제한적 | 없음 | 없음 |
| 클라이언트 개조 | 안 함(원본) | 경우에 따라 | 안 함 | 안 함 |
| 접속 방식 | 브라우저/PWA | 전용 앱 | 전용 앱 | 브라우저(VNC) |
WOC는 크게 세 덩어리입니다. ① 문지기이자 두뇌인 패널(panel), ② 微信이 실제로 도는 微信 실례 이미지, ③ 이 둘을 빌드·배포하는 인프라/CI. 언어 구성은 TypeScript 78%, CSS 13%, Shell 6%로, "타입스크립트로 쓴 게이트웨이 + 셸 스크립트로 짠 실행 환경"이라는 성격이 그대로 드러납니다.
| 기술 | 역할 | 세부 사항 |
|---|---|---|
| Node.js + TypeScript | 패널 런타임/언어 | 전체 게이트웨이 로직 |
| Fastify | 웹 서버 프레임워크 | 고성능 HTTP API + 정적 SPA 서빙 + 리버스 프록시 |
| 쿠키 인증 | 로그인 세션 | cookie 기반 인증, KasmVNC 자격증명은 서버에서만 주입 |
| dockerode | Docker 제어 라이브러리 | docker.sock으로 微信 실례 컨테이너를 동적 생성/시작/삭제 |
| 계정/권한 저장 | 데이터 영속화 | accounts.json(사용자·실례 메타·비밀번호 해시) |
| 기술 | 역할 | 세부 사항 |
|---|---|---|
| React + TypeScript | SPA UI | 로그인 / 실례 그리드 / 자식계정 / 데스크탑 진입 화면 |
| Vite | 빌드 도구 | 프론트엔드 번들링(패널 Dockerfile에서 빌드) |
| PWA | 앱처럼 설치 | iOS Safari "홈 화면에 추가", 데스크탑 Chrome "설치" |
| 테마 | 비주얼 | "우유빛 천(牛奶布艺) + 微信 그린" 테마 |
| 기술 | 역할 | 세부 사항 |
|---|---|---|
| linuxserver KasmVNC 베이스 이미지 | 원격 데스크탑 기반 | Xvfb + openbox + KasmVNC + 웹 클라이언트(3000/3001) 내장, 멀티아치 |
| Xvfb | 가상 디스플레이 | 모니터 없이 GUI를 그릴 "보이지 않는 화면" |
| openbox | 창 관리자 | 가벼운 X11 윈도우 매니저, autostart로 微信 상주 실행 |
| 원본 微信 Linux 버전 | 실제 앱 | 이미지에 안 넣고 런타임에 텐센트 CDN에서 다운로드(약 190~210MB) |
| Qt / xcb / GTK3 라이브러리 | 微信 구동 의존성 | 微信은 Qt 앱, 내장 WeChatAppEx는 Chromium 커널이라 GTK3 필요 |
| 중문 폰트 (Noto CJK 등) | 한자 렌더링 | 없으면 글자가 네모(두부)로 깨짐 |
| 기술 | 역할 | 세부 사항 |
|---|---|---|
| Docker + Compose | 컨테이너 오케스트레이션 | compose에는 패널 1개 서비스만(微信 실례는 런타임 동적 생성) |
| Docker Hub / GHCR | 이미지 레지스트리 | 기본 레지스트리는 Docker Hub(docker.io/gloridust). GHCR(ghcr.io/gloridust)은 동기화된 백업 소스 — .env의 WOC_IMAGE_PREFIX로 전환 가능 |
| docker buildx + QEMU | 멀티아치 빌드 | amd64 + arm64 동시 빌드/푸시 |
| GitHub Actions | CI/CD | vX.Y.Z 태그/릴리스 시 멀티아치 이미지 자동 빌드·푸시 |
| fnOS(飞牛) fpk | NAS 네이티브 패키징 | 飞牛 NAS용 앱 패키지 프로젝트(로드맵 진행 중) |
전체 그림은 "브라우저 → 패널 → Docker 엔진 → 微信 실례 컨테이너"의 한 줄짜리 흐름입니다. 단, 브라우저는 微信 컨테이너에 직접 닿지 못합니다. 모든 트래픽은 패널을 거치고, 패널이 자격증명을 서버 쪽에서 끼워 넣어 微信 실례로 리버스 프록시합니다.
가장 핵심적인 트릭입니다. 보통 컨테이너는 docker-compose.yml에 미리 적어두지만, WOC의 微信 실례는 사용자가 "새 微信 실례" 버튼을 누른 순간 패널이 만들어 냅니다. 패널 컨테이너에 호스트의 /var/run/docker.sock을 마운트해 두면, 패널 안의 dockerode가 호스트 Docker 엔진을 "리모컨"처럼 조종해 docker run을 대신 실행합니다.
# docker-compose.yml — 서비스는 패널 하나뿐. docker.sock을 마운트하는 게 포인트.
services:
panel:
image: ${WOC_IMAGE_PREFIX:-docker.io/gloridust}/woc-panel:${WOC_VERSION:-latest}
container_name: woc-panel
environment:
- PORT=8080
- WOC_WECHAT_IMAGE=${WOC_IMAGE_PREFIX:-docker.io/gloridust}/wechat-on-cloud:${WOC_VERSION:-latest}
- PANEL_ADMIN_USER=${WOC_USER:-admin}
- PANEL_ADMIN_PASSWORD=${WOC_PASSWORD:-wechat} # 기본 비번 꼭 변경!
volumes:
- ./data-panel:/data # 패널 계정/실례 메타
- /var/run/docker.sock:/var/run/docker.sock # Docker 리모컨 = 호스트 root 권한
ports:
- "${WOC_HTTP_PORT:-36080}:8080" # 유일한 대외 입구
restart: unless-stopped
docker.sock을 쥔 컨테이너는 사실상 호스트를 통째로 지배할 수 있습니다. 그래서 WOC는 ① 패널을 절대 공인 IP에 그대로 노출하지 말 것(내부망/VPN/내부망 침투 도구로만 접근), ② 실례 증감·微信 설치 같은 Docker를 건드리는 동작은 관리자만, ③ Docker API를 프론트엔드에 절대 내려보내지 않음을 원칙으로 합니다.
브라우저는 KasmVNC의 비밀번호를 절대 받지 못합니다. 사용자가 데스크탑에 들어가면, 패널이 /desktop/:id/*로 들어온 요청에 Basic 인증 헤더를 서버에서 끼워 넣어 해당 실례로 프록시합니다. 즉 "열쇠는 문지기만 들고, 손님은 문지기를 통해서만 방에 들어간다"는 구조입니다. 실례 컨테이너 이름도 내부 랜덤 ID에서 파생해 추측/주입을 막습니다.
微信은 x86_64와 arm64용 deb만 제공합니다. WOC 이미지는 멀티아치라 어디서든 같은 docker compose up -d로 뜨고, 微信을 내려받는 순간 컨테이너 안에서 dpkg --print-architecture로 CPU를 감지해 맞는 패키지를 가져옵니다.
| 실행 머신 | 아키텍처 | 자동 다운로드 |
|---|---|---|
| Intel/AMD NAS·x86 서버 | amd64 | WeChatLinux_x86_64.deb |
| ARM NAS·Apple Silicon Mac | arm64 | WeChatLinux_arm64.deb |
微信 앱 자체는 이미지에 넣지 않습니다. 새 실례를 만든 뒤 패널에서 "다운로드 및 설치"를 누르면, 그때 텐센트 CDN에서 받아 그 실례의 데이터 볼륨에 풀어 설치합니다. 덕분에 이미지가 작아 빌드가 빠르고, 텐센트 CDN에 빌드가 묶이지 않으며, 微信만 따로 "최신으로 업데이트"하기도 쉽습니다. 다운로드·설치·상태는 셸 스크립트 wechat-ctl.sh가 맡고, 패널이 docker exec로 이를 트리거합니다.
원격 데스크탑에서 한자/한글 입력은 악명 높은 난제입니다. WOC는 컨테이너 안에 입력기를 깔지 않고, KasmVNC의 IME 입력 모드를 켜서 "내 PC의 입력기로 완성된 글자만" 컨테이너로 보냅니다. 원래 noVNC 구현은 키 입력을 글자 단위로 다시 쏘다가 글자가 새거나 누락되는데, WOC는 입력 완료 시점(compositionend)에 완성 문자열만 직접 보내도록 웹 클라이언트 번들을 패치했습니다.
| 경로 | 역할 |
|---|---|
| panel/server | 두뇌. Fastify로 API를 열고, dockerode로 微信 실례를 만들고 지우며, KasmVNC로 리버스 프록시. 자격증명은 여기서만 주입 |
| panel/web | 얼굴. React SPA. 로그인 → 실례 그리드 → 데스크탑 진입. PWA로 설치 가능 |
| docker/Dockerfile | 微信이 돌 "가상 PC" 이미지. KasmVNC 베이스 + 폰트 + Qt/GTK 의존성. 微信 본체는 안 넣음 |
| docker/wechat-ctl.sh | 微信 다운로드/해제/업데이트 셸 스크립트. 상태를 볼륨의 /config/.woc-state에 기록 |
| docker/autostart | 컨테이너가 뜨면 微信을 자동 실행하고, 꺼지면 다시 살리는 "지킴이" |
| .github/workflows/release.yml | 버전 태그를 밀면 멀티아치 이미지를 빌드해 GHCR에 자동 배포 |
| data-panel/ (런타임 생성) | 호스트에 남는 패널 데이터. accounts.json에 사용자·실례 메타·비번 해시 |
| woc-data-<id> (런타임 볼륨) | 각 微信 실례의 데이터. /config에 微信 본체·로그인 상태·메시지 캐시 |
요점: 소스 트리에 "微信 실례"는 폴더로 보이지 않습니다. 그것은 실행 중에 패널이 만들어내는 동적 컨테이너이기 때문입니다. 소스에는 "그 실례를 어떻게 만들지"의 설계도(docker/)와 "누가 그것을 만드는지"의 두뇌(panel/)만 있습니다.
이 레포의 가장 값진 패턴입니다. "프로그램이 코드로 컨테이너를 만들어내는" 방식을 배웁니다. dockerode로 컨테이너를 생성하고, 볼륨을 붙이고, 같은 네트워크에 연결하는 흐름을 익히면 "셀프호스팅 PaaS"의 축소판을 만들 수 있습니다.
// dockerode로 微信 실례 컨테이너를 동적으로 만드는 개념 코드
import Docker from "dockerode";
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
const c = await docker.createContainer({
Image: process.env.WOC_WECHAT_IMAGE, // 微信 실례 이미지
name: `woc-wx-${id}`, // 랜덤 id로 컨테이너 이름
HostConfig: {
Binds: [`woc-data-${id}:/config`], // 실례 전용 볼륨
NetworkMode: net || undefined, // 패널 컨테이너와 공유 중인 네트워크(동적 탐지, 없으면 기본 브리지)
SecurityOpt: ["seccomp=unconfined"], // 微信 구동에 필요
ShmSize: 1024 * 1024 * 1024, // 1 GB
},
});
await c.start();
"GUI 앱을 서버에서 돌려 브라우저로 보여주기"의 표준 레시피를 배웁니다. 모니터 없는 서버에 가상 화면(Xvfb)을 만들고, 가벼운 창 관리자(openbox)로 앱을 띄운 뒤, KasmVNC로 그 화면을 웹으로 중계합니다. 微信 자리에 어떤 GUI 앱을 넣어도 같은 원리입니다.
"백엔드의 비밀을 프론트에 노출하지 않고 안전하게 중계"하는 패턴입니다. 사용자는 패널 쿠키로만 인증하고, 패널이 뒤쪽 서비스(KasmVNC)의 Basic 인증을 서버에서 끼워 넣습니다. API 키를 숨겨야 하는 모든 프록시에 응용됩니다.
amd64 NAS와 arm64 NAS 양쪽에서 같은 명령으로 도는 이미지를 만드는 법을 배웁니다.
# QEMU 설치(리눅스에서 크로스 빌드 시) → buildx 빌더 생성 → 멀티아치 빌드/푸시
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --name woc --use
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/gloridust/woc-panel:1.0.1 -t ghcr.io/gloridust/woc-panel:latest \
--push ./panel
버전 태그를 푸시하면 멀티아치 이미지가 자동으로 빌드·배포되는 CI/CD 파이프라인을 배웁니다. "태그 = 배포"라는 깔끔한 릴리스 규칙을 직접 경험할 수 있습니다.
# 태그를 밀면 Actions가 멀티아치 이미지를 빌드해 GHCR로 푸시
git tag v1.0.0
git push origin v1.0.0
# latest까지 갱신하려면 Release 발행 또는 workflow_dispatch 사용
웹앱을 네이티브 앱처럼 설치하는 PWA, 그리고 원격 데스크탑의 고질적 난제인 한자/한글 입력을 compositionend 시점에 완성 문자열만 보내 해결하는 기법을 배웁니다. 한국어 IME 문제와도 똑같은 원리라 한글 입력 처리에 그대로 응용됩니다.
36080)만. 微信 실례는 내부 네트워크에만| 항목 | 왜 필요한가 |
|---|---|
| 볼륨 exec 권한 | 微信 본체가 볼륨에서 직접 실행됨. noexec 볼륨에 두면 微信이 안 뜸 |
seccomp=unconfined | 微信(Chromium 커널 포함) 구동에 필요한 시스템콜 허용 |
shm_size 확대 | 공유 메모리 부족 시 화면이 검게 나오거나 微信이 죽음 |
| 중문 폰트 | fonts-noto-cjk 등이 없으면 글자가 네모(두부)로 깨짐 |
| 기본 비밀번호 변경 | 기본값 admin / wechat은 반드시 즉시 변경 |
| 외부 TLS 반영 | 패널 앞에 Caddy/Nginx 같은 HTTPS 리버스 프록시 한 겹 권장 |
패널 데이터는 호스트 ./data-panel에, 각 微信 실례는 Docker 명명 볼륨 woc-data-<id>에 저장됩니다. 실례를 삭제해도 기본적으로 볼륨은 남겨(같은 이름으로 재생성하면 복구), "완전 삭제"를 명시해야 볼륨까지 지워집니다. 특정 실례 백업 = 그 woc-data-<id> 볼륨 백업입니다.
WOC를 실제로 올려보며 "컨테이너 = 가상 PC" 감각을 잡습니다. 微信 계정이 없으면 KasmVNC 진입 화면까지만 확인해도 충분합니다.
git clone https://github.com/Gloridust/WechatOnCloud.git
cd WechatOnCloud
cp .env.example .env # WOC_PASSWORD 기본 비번 변경
docker compose up -d # 패널 컨테이너 woc-panel 기동
docker logs -f woc-panel # 로그 확인 후 브라우저로 :36080 접속
WOC의 심장인 동적 오케스트레이션만 떼어 연습합니다. 작은 Fastify 서버에서 버튼 클릭 시 nginx 컨테이너를 동적으로 만들고 지우는 API를 구현해 봅니다.
// POST /spawn → 새 컨테이너 생성, DELETE /spawn/:id → 제거
import Docker from "dockerode";
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
app.post("/spawn", async (req, reply) => {
const c = await docker.createContainer({ Image: "nginx:alpine" });
await c.start();
return { id: c.id.slice(0, 12) };
});
KasmVNC 베이스 이미지 위에 微信 대신 다른 GUI 앱(예: GIMP, 계산기)을 올려, "원격 데스크탑 스트리밍"이 앱 종류와 무관함을 체감합니다.
FROM lscr.io/linuxserver/baseimage-kasmvnc:debianbookworm
RUN apt-get update && apt-get install -y --no-install-recommends gimp
COPY autostart /defaults/autostart # 시작 시 gimp 실행
EXPOSE 3000 3001
내 GitHub 계정으로 amd64+arm64 이미지를 buildx로 만들어 GHCR에 올리고, 태그 푸시 시 자동 빌드되는 Actions 워크플로를 작성합니다.
echo $GITHUB_PAT | docker login ghcr.io -u <user> --password-stdin
docker buildx create --name demo --use
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/<user>/my-app:1.0.0 --push .
WOC의 보안 모델을 재현합니다. 쿠키로 사용자 인증을 받고, 뒤쪽 보호된 서비스(Basic 인증이 걸린)로 요청을 프록시할 때 자격증명을 서버에서 끼워 넣어 프론트엔드가 비밀을 보지 못하게 합니다.
# 개념: 쿠키 검증 통과 시에만, 서버가 Authorization 헤더를 주입해 프록시
proxy.on("proxyReq", (proxyReq) => {
const cred = Buffer.from("user:secret").toString("base64");
proxyReq.setHeader("Authorization", "Basic " + cred);
});
이미지 / 컨테이너 / 볼륨 / 네트워크 / docker.sock의 관계 이해. docker compose로 서비스 정의, 명명 볼륨과 바인드 마운트의 차이, 컨테이너 간 이름 기반 통신(내부 네트워크) 실습.
dockerode(Node) 또는 docker-py(Python)로 컨테이너를 생성/시작/삭제. docker.sock의 권한 위험과 대안(소켓 프록시, rootless Docker) 이해. WOC의 "동적 실례 생성" 흐름 따라 만들기.
Xvfb 가상 디스플레이, X11과 창 관리자(openbox), VNC의 역사(VNC → noVNC → KasmVNC). 헤드리스 서버에서 GUI 앱을 띄워 브라우저로 보는 파이프라인 구축. 코덱·지연·IME 이슈 파악.
Fastify(또는 Express)로 쿠키 세션 인증, 리버스 프록시로 백엔드 자격증명 숨기기, 역할 기반 접근 제어(관리자/자식계정)와 리소스별 권한 매트릭스 설계.
buildx + QEMU 멀티아치, GitHub Actions로 태그 기반 릴리스, GHCR 배포와 공개 설정. 그리고 공인망 비노출·TLS·기본 비번 변경·감사 로그 등 셀프호스팅 보안 체크리스트 정립.
| 키워드 | 설명 |
|---|---|
| 셀프호스팅 | 남의 클라우드 대신 내 서버에 직접 소프트웨어를 올려 운영하는 방식 |
| docker.sock | Docker 엔진의 "리모컨" 소켓(/var/run/docker.sock). 쥐면 호스트 root와 동급의 힘 |
| dockerode | Node.js에서 Docker 엔진을 프로그래밍으로 제어하는 라이브러리 |
| 컨테이너 오케스트레이션 | 여러 컨테이너의 생성/배치/연결/제거를 자동으로 관리하는 것 |
| KasmVNC | 서버 화면을 브라우저로 스트리밍하는 현대판 원격 데스크탑(코덱·IME 개선) |
| noVNC | 웹 표준만으로 VNC를 구현한 고전 라이브러리. KasmVNC의 뿌리 |
| Xvfb | X Virtual Framebuffer. 모니터 없이 GUI를 그리는 "보이지 않는 화면" |
| openbox | 가벼운 X11 창 관리자. WOC에서 微信을 상주 실행시키는 세션 담당 |
| 리버스 프록시 | 클라이언트 요청을 받아 뒤쪽 서버로 대신 전달. WOC에선 자격증명을 주입해 중계 |
| Fastify | Node.js의 고성능 웹 프레임워크. WOC 패널의 백엔드 |
| PWA | Progressive Web App. 웹앱을 네이티브 앱처럼 홈 화면에 설치 |
| RBAC | Role-Based Access Control. 역할(관리자/자식계정)별로 접근 권한을 나누는 방식 |
| GHCR | GitHub Container Registry. GitHub가 제공하는 컨테이너 이미지 저장소 |
| buildx / QEMU | docker buildx로 멀티아치 빌드, QEMU로 다른 CPU 아키텍처 에뮬레이션 |
| 멀티아치 이미지 | amd64·arm64 등 여러 CPU에서 같은 태그로 도는 이미지 |
| seccomp | 컨테이너가 쓸 수 있는 시스템콜을 제한하는 보안 기능. 微信은 unconfined 필요 |
| 명명 볼륨 | 이름을 가진 Docker 영속 디스크. 컨테이너를 지워도 데이터가 남음 |
| IME / compositionend | 입력기(한자·한글 조합)와, 글자 조합이 끝나는 브라우저 이벤트. 완성 문자만 전송해 누락 방지 |