Synology NAS에서 Claude Code + Discord Bot 구축하기
Synology NAS에서 Claude Code + Discord Bot 구축하기
스마트폰에서 Discord 슬래시 커맨드 하나로 NAS 위의 Claude Code CLI에 작업을 지시하고, 실시간으로 진행상황을 보며 대화를 이어갈 수 있는 시스템을 만들었습니다.
완성된 모습
사용자: /claude task:현재 프로젝트 폴더 분석해줘
Bot: 🚀 새 작업 시작
Task ID: task-1771...
Bot: 🔄 작업 진행 중
도구 호출 3회
⚡ Bash → ls -la
📖 Read → package.json
📖 Read → src/index.ts
Bot: ✅ 작업 완료
결과: 이 프로젝트는 ...
💡 /chat 으로 이어서 대화 가능
사용자: /chat message:package.json 내용을 더 자세히 설명해줘
Bot: 💬 이어서 대화
(이전 컨텍스트를 기억한 상태로 답변)
시스템 아키텍처
Discord (/claude, /chat, /todo)
↓
Discord Bot (Node.js, port 3001)
↓
n8n Webhook (안전 검사 + 라우팅)
↓
Claude Code API Server (Node.js, port 3002)
↓
Claude CLI (stream-json + verbose + resume)
↓ (실시간 스트리밍)
Discord Bot ← 진행상황 / 결과 전송
구성 요소
| 컨테이너 | 역할 | 포트 |
|---|---|---|
claude-code |
Claude CLI 실행 + API 서버 | 3002 |
discord-bot |
Discord 슬래시 커맨드 처리 + 결과 수신 | 3001 |
redis |
작업 상태 관리 | 6379 (내부) |
n8n (별도) |
Webhook 라우팅 + 안전 검사 | 5678 |
1. Docker Compose 구성
version: '3.8'
services:
claude-code:
build:
context: ./claude-code
dockerfile: Dockerfile
container_name: claude-code
restart: unless-stopped
command: sh -c "node /opt/scripts/api-server.js & tail -f /dev/null"
ports:
- "3002:3002"
environment:
- N8N_WEBHOOK_URL=http://YOUR_HOST:5678/webhook/claude-result
volumes:
- ./claude-code/scripts:/opt/scripts # 볼륨 마운트 → 재시작만으로 반영
- ./research:/workspace/research
- /path/to/obsidian:/workspace/obsidian
- claude-data:/root/.claude
- claude-logs:/workspace/.claude-logs
networks:
- claude-net
depends_on:
- redis
discord-bot:
build:
context: ./discord-bot
dockerfile: Dockerfile
container_name: discord-bot
restart: unless-stopped
environment:
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
- N8N_WEBHOOK_URL=http://YOUR_HOST:5678/webhook/discord-command
ports:
- "3001:3001"
networks:
- claude-net
depends_on:
- redis
redis:
image: redis:7-alpine
container_name: claude-redis
restart: unless-stopped
volumes:
- redis-data:/data
networks:
- claude-net
volumes:
claude-data:
claude-logs:
redis-data:
networks:
claude-net:
driver: bridge
핵심 포인트
api-server.js는 볼륨 마운트 → 파일 수정 후docker restart claude-code만 하면 반영bot.js는 COPY 빌드 → 코드 변경 시 반드시docker compose build discord-bot필요- Obsidian vault도 마운트하여 Claude가 노트에 직접 접근 가능
2. API Server (Claude CLI 실행 서버)
동작 흐름
- Discord Bot 또는 n8n으로부터 HTTP POST 요청 수신
- Claude CLI를
spawn으로 실행 (stream-json + verbose 모드) - stdout을 줄 단위로 파싱하여 tool_use 이벤트 감지
- 3초마다 Discord Bot으로 진행상황 전송
- 완료 시 최종 결과 + 도구 사용 로그 전송
- 세션 ID를 저장하여
/chat명령으로 대화 이어가기 지원
삽질 포인트 3가지
(1) --output-format stream-json은 --verbose가 필수
# ❌ 에러 발생
claude -p "hello" --output-format stream-json --max-turns 1
# ✅ --verbose 추가해야 정상 작동
claude -p "hello" --output-format stream-json --verbose --max-turns 1
(2) Node.js spawn에서 stdin을 /dev/null로 연결해야 함
이것이 가장 오래 걸린 디버깅이었습니다. Node.js의 spawn으로 Claude CLI를 실행하면, stdin이 열려있어서 Claude가 입력 대기 상태에 빠져 영원히 응답하지 않습니다.
// ❌ Claude CLI가 무한 대기
const claude = spawn('claude', ['-p', 'hello', ...]);
// ✅ stdin을 /dev/null로 연결해야 정상 동작
const devnull = fs.openSync('/dev/null', 'r');
const claude = spawn('claude', ['-p', 'hello', ...], {
stdio: [devnull, 'pipe', 'pipe']
});
터미널에서 직접 실행하면 정상 작동하기 때문에, "터미널에서는 되는데 Node.js에서 안 되는" 상황이 발생하여 원인 파악에 시간이 걸렸습니다.
(3) Claude CLI stream-json 형식은 Anthropic API와 완전히 다름
처음에 Anthropic API의 스트리밍 형식(content_block_start, content_block_delta)을 기대했으나, Claude CLI의 stream-json은 완전히 다른 구조입니다.
{"type":"system","subtype":"init","session_id":"...","tools":[...]}
{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Bash","input":{"command":"ls -la"}}]}}
{"type":"user","message":{"content":[{"type":"tool_result","content":"..."}]}}
{"type":"result","subtype":"success","result":"최종 결과 텍스트","session_id":"..."}
도구 사용을 감지하려면 type === 'assistant'인 이벤트에서 message.content 배열을 순회하며 type === 'tool_use'인 블록을 찾아야 합니다.
3. Discord Bot (bot.js)
슬래시 커맨드
| 명령어 | 설명 | 옵션 |
|---|---|---|
/claude task:... |
새 세션으로 작업 시작 (n8n 경유, 안전 검사 포함) | task (필수), dir (선택, 기본: /workspace) |
/chat message:... |
이전 대화 이어서 (claude-code 서버 직접 호출) | message (필수) |
/status |
진행 중인 작업 목록 | 없음 |
/cancel task_id:... |
작업 취소 | task_id (필수) |
/newsession |
세션 초기화 (새 대화 시작) | 없음 |
/todo |
오늘의 할 일 확인 (Obsidian #todo + Todoist P1) | 없음 |
/patient |
관심환자 등록 (마스터 테이블 + 개별 노트 자동 생성) | id (필수), name (필수), diagnosis (필수), memo (필수), consult (선택) |
/patient 명령어 상세
Discord에서 /patient를 실행하면 n8n → Claude Code로 전달되어 자동으로:
obsidian/관심환자_목록.md마스터 테이블에 새 행 추가obsidian/patients/{환자번호}_{이름}.md개별 노트 생성- 협진 내용 포함 시 협진 답변 평가 (잘한 점 / 보완할 점 / 개선 답변) 추가
/patient id:130055381 name:이종순 diagnosis:Both ACA infarction memo:뇌경색후 보행회복→허리수술후 보행악화
Discord 메시지 유형
| type | 설명 | 표시 방식 |
|---|---|---|
log |
작업 시작 알림 | Embed |
progress |
실시간 도구 사용 현황 | Embed (수정하며 업데이트) |
task_result |
최종 결과 | Embed (결과 + 도구 수) |
approval_request |
위험 명령 승인 요청 | Embed + 버튼 |
진행상황 업데이트 방식
같은 task_id에 대한 progress 메시지는 새로 보내지 않고 기존 메시지를 수정(edit) 합니다.
const progressMessages = new Map(); // taskId → messageId
// 기존 메시지가 있으면 수정, 없으면 새로 생성
const existingMsgId = progressMessages.get(data.task_id);
if (existingMsgId) {
const msg = await channel.messages.fetch(existingMsgId);
await msg.edit({ embeds: [embed] });
} else {
const newMsg = await channel.send({ embeds: [embed] });
progressMessages.set(data.task_id, newMsg.id);
}
작업 완료 시 @멘션 알림
작업 완료 시 요청자에게 멘션으로 알림을 보냅니다. Redis에 저장된 user_id를 활용합니다.
// Redis에서 요청자 user_id 가져오기
let mention = '';
const userId = await redis.hget(`task:${data.task_id}`, 'user_id');
if (userId) mention = `<@${userId}>`;
await channel.send({ content: mention || undefined, embeds: [embed] });
HTTP 서버 (역방향 통신)
Discord Bot은 HTTP 서버(port 3001)를 운영하여 API Server로부터 진행상황/결과를 수신합니다.
const httpServer = http.createServer(async (req, res) => {
// POST로 들어오는 progress, task_result, approval_request 처리
const data = JSON.parse(body);
const channel = await client.channels.fetch(data.channel_id);
if (data.type === 'progress') { /* embed 수정 */ }
else if (data.type === 'task_result') { /* 결과 전송 */ }
else if (data.type === 'approval_request') { /* 버튼 전송 */ }
});
httpServer.listen(3001);
4. n8n 워크플로우 (안전 검사 + 라우팅)
Discord Command Webhook (POST /discord-command)
→ Parse & Safety Check (위험 명령어 필터링)
→ Approval Needed? (If 분기)
→ Yes: Send Approval to Discord (승인 버튼)
→ No: Run Claude Code (POST to API Server)
/claude 명령은 n8n을 경유하여 위험한 명령어(rm -rf, git push --force 등)를 필터링합니다.
/chat 명령은 n8n을 거치지 않고 claude-code 서버에 직접 전송합니다 (이미 안전한 세션 내 대화이므로).
5. 세션 관리 (대화 이어가기)
Claude CLI는 -p 모드에서 기본적으로 단발성이지만, --resume <session_id> 옵션으로 이전 세션을 이어갈 수 있습니다.
구현 방식
api-server.js에서 채널별로 마지막 session_id를 메모리에 저장 (Map)/claude→type: 'new_task'→ 기존 세션 삭제, 새로 시작/chat→type: 'continue'→ 저장된 session_id로--resume실행- 세션은 1시간 미사용 시 자동 만료
// 세션이 있으면 resume 인자 추가
if (sessionId) {
args.push('--resume', sessionId);
}
// stream-json의 system init에서 session_id 추출하여 저장
if (event.type === 'system' && event.session_id) {
newSessionId = event.session_id;
}
6. 코드 수정 및 배포
핵심 개념: 볼륨 마운트 vs COPY 빌드
| 방식 | 대상 | 반영 방법 |
|---|---|---|
| 볼륨 마운트 | api-server.js |
파일 수정 → docker restart claude-code |
| COPY 빌드 | bot.js |
파일 수정 → docker compose build discord-bot → docker compose up -d discord-bot |
파일 수정 완료
│
├─ 볼륨 마운트? → docker restart 컨테이너명
│
└─ COPY 빌드? → docker compose build 서비스명
docker compose up -d 서비스명
주의: docker compose는 서비스명, docker는 컨테이너명
# docker compose 명령 → 서비스명 사용
sudo docker compose build discord-bot # ✅
sudo docker compose build discord-bot-1 # ❌
# docker 명령 → 컨테이너명 사용
sudo docker restart discord-bot # ✅
재빌드 시 사라지는 데이터
progressMessagesMap (메모리) → 진행 중인 작업의 progress embed 추적이 끊김- 진행 중인 작업이 없으면 영향 없음
- Redis, 볼륨 마운트 데이터, .env 등은 모두 보존
7. 폴더 구조
claude-system/
├── docker-compose.yml
├── .env # DISCORD_BOT_TOKEN 등
├── claude-code/
│ ├── Dockerfile
│ └── scripts/
│ └── api-server.js # ← 볼륨 마운트 (재시작만으로 반영)
├── discord-bot/
│ ├── Dockerfile
│ ├── package.json
│ └── bot.js # ← COPY 빌드 (재빌드 필요)
└── research/ # 기본 작업 디렉토리
8. 트러블슈팅
컨테이너 로그 확인
sudo docker logs claude-code --tail 20
sudo docker logs discord-bot --tail 20
Claude CLI가 실행 안 될 때
# claude 명령어 존재 확인
sudo docker exec claude-code which claude
# 직접 실행 테스트
sudo docker exec claude-code timeout 60 bash -c \
'claude -p "hello" --output-format stream-json --verbose --max-turns 1 2>&1 | head -5'
# 실행 중인 claude 프로세스 정리
sudo docker exec claude-code pkill -f "claude -p"
Discord Bot 코드 변경이 반영 안 될 때
bot.js는 COPY 빌드이므로 단순 파일 교체 + restart로는 반영되지 않습니다.
# ❌ restart만으로는 코드 변경 반영 안 됨
sudo docker restart discord-bot
# ✅ 반드시 build 후 up
sudo docker compose build discord-bot
sudo docker compose up -d discord-bot
슬래시 커맨드가 Discord에 안 보일 때
- 봇 재시작 후 1-2분 대기 (Discord API 전파 시간)
sudo docker logs discord-bot --tail 10에서✅ 슬래시 커맨드 등록 완료확인- Discord 앱 재시작 또는 서버 재접속
9. SSH로 Claude Code 직접 사용하기
Discord 외에도 SSH로 NAS에 접속하여 직접 Claude를 사용할 수 있습니다.
# 한 줄 명령
docker exec -it claude-code claude -p "프로젝트 분석해줘"
# 대화형 세션
docker exec -it claude-code claude
# 특정 디렉토리에서 작업
docker exec -it -w /workspace/research claude-code claude
# 결과를 파일로 저장
docker exec claude-code claude -p "요약해줘" > output.md
유용한 별칭
# ~/.bashrc에 추가
alias cc='docker exec -it claude-code claude'
alias ccp='docker exec -it claude-code claude -p'
10. Claude Cowork와의 비교 — 왜 직접 구축했는가
2026년 1월 Anthropic이 출시한 Claude Cowork는 Claude Code의 에이전트 실행 엔진을 GUI로 감싸고 비즈니스 도구 커넥터를 내장한 데스크톱 앱입니다. 비개발자도 AI 자동화를 사용할 수 있게 한 제품인데, 그렇다면 왜 굳이 NAS + Docker + Discord로 직접 구축했을까?
동일한 것
| 항목 | Claude Cowork | 이 시스템 (NAS Docker) |
|---|---|---|
| LLM 모델 | Opus 4.6 | Opus 4.6 |
| 에이전트 아키텍처 | Claude Code 기반 | Claude Code CLI 직접 |
| 파일 읽기/쓰기 | ✅ | ✅ |
| 셸 명령 실행 | ✅ | ✅ |
| MCP 서버 연결 | ✅ | ✅ |
| Extended Thinking | ✅ | ✅ |
내부적으로 Cowork의 실행 엔진은 Claude Code 그 자체입니다. 모델도 동일하고, 도구 사용 능력이나 추론 품질에 차이가 없습니다.
다른 것
| 항목 | Claude Cowork | 이 시스템 |
|---|---|---|
| 인터페이스 | 데스크톱 GUI | Discord (모바일) + SSH + Web VS Code |
| 실행 환경 | 격리된 VM (샌드박스) | NAS Docker (제한 없음) |
| 파일 접근 | 허용한 폴더만 | NAS 전체 볼륨 마운트 |
| 비즈니스 커넥터 | 12개 내장 (Google Drive, Gmail, DocuSign 등) | 없음 (n8n으로 대체 가능) |
| 자동화 | 수동 실행 위주 | n8n 스케줄 + Webhook + Discord 봇 |
| 다중 채널 | 단일 세션 | 채널별 독립 세션 + 세션 이어가기 |
| 모바일 접근 | 데스크톱 전용 | Discord 앱으로 어디서든 |
| 비용 통제 | Anthropic 서버 의존 | 자체 NAS, API 키 직접 관리 |
| 커스터마이징 | 제한적 | 무제한 (bot.js, api-server.js 직접 수정) |
이 시스템이 더 나은 점
1. 모바일 우선
Cowork는 데스크톱 앱입니다. 외출 중에 스마트폰에서 작업을 지시하려면 이 시스템의 Discord 봇이 유일한 방법입니다.
병원 점심시간에 Discord:
/claude task:어제 작업한 프레젠테이션 수정해줘
/presentation topic:Frozen Shoulder
2. 자동화
Cowork는 사람이 앱을 열고 명령해야 합니다. 이 시스템은 n8n을 통해 완전 자동화가 가능합니다.
매일 07:00 → n8n 스케줄 → Claude가 Obsidian #todo + Todoist 조회 → Discord에 알림
매주 월요일 → Claude가 논문 업데이트 검색 → 결과 정리 → Discord에 전송
3. 파일시스템 자유도
Cowork는 보안을 위해 VM 안에서 실행되며, 허용한 폴더만 접근합니다. 이 시스템은 NAS의 전체 볼륨을 마운트하여 Obsidian vault, 연구 데이터, SaaS 프로젝트 등 모든 파일에 직접 접근합니다.
# docker-compose.yml에서 원하는 만큼 마운트
volumes:
- /volume1/docker/jupyter/obsidian:/workspace/obsidian
- /volume1/docker/claude-system/research:/workspace/research
- /volume1/docker/saas:/workspace/saas
4. 완전한 커스터마이징
/patient 명령어처럼 내 워크플로우에 맞는 전용 슬래시 커맨드를 만들 수 있습니다. /presentation으로 한글 제목 + 영문 본문 + References 포함 PPTX를 자동 생성하는 것도 Cowork에서는 매번 긴 프롬프트를 써야 하지만, 이 시스템에서는 한 줄 명령입니다.
Cowork가 더 나은 점
1. 진입 장벽 제로
설치, Docker, YAML 설정 없이 Claude 데스크톱 앱만 열면 됩니다. 비개발자 가족이나 동료에게 추천하기 쉽습니다.
2. 비즈니스 도구 커넥터 내장
Google Drive, Gmail, DocuSign 등 12개 커넥터가 기본 내장되어 있어, 별도 설정 없이 "Google Drive에서 파일 찾아서 요약해줘" 같은 작업이 바로 됩니다. 이 시스템에서 같은 기능을 구현하려면 n8n이나 MCP 서버를 별도로 구성해야 합니다.
3. 보안 격리
VM 샌드박스에서 실행되므로 시스템 파일이나 네트워크에 대한 우발적 접근이 원천 차단됩니다. 이 시스템은 Docker 내부에서 자유롭게 실행되므로 rm -rf /workspace 같은 실수의 위험이 있어 n8n 안전 검사와 승인 버튼으로 보완합니다.
정리: 누구에게 어떤 것을
| 사용자 | 추천 |
|---|---|
| 개발자 + NAS 보유 | 이 시스템 (Docker + Discord) |
| 비개발자 / 빠른 시작 | Cowork (데스크톱 앱) |
| 모바일 중심 | 이 시스템 (Discord 봇) |
| 팀/기업 환경 | Cowork (관리형 보안) |
| 자동화가 핵심 | 이 시스템 (n8n 연동) |
결론적으로 Cowork와 이 시스템은 같은 엔진(Claude Code) 위에 다른 인터페이스를 씌운 것입니다. Cowork는 편의성과 보안을, 이 시스템은 자유도와 자동화를 택한 것입니다. 둘 다 필요하면 공존도 가능합니다 — 코딩은 NAS Docker, 문서 작업은 Cowork처럼.
마무리
이 시스템의 장점:
- 어디서든 접근: 스마트폰 Discord에서 바로 Claude에게 코딩 작업 지시
- 실시간 진행상황: 어떤 파일을 읽고, 어떤 명령을 실행하는지 실시간으로 확인
- 대화 이어가기:
/chat으로 컨텍스트를 유지하며 추가 질문 가능 - 안전 검사: n8n을 통해 위험한 명령어 필터링 + Discord 승인 버튼
- 세션 관리: 채널별 독립 세션, 1시간 자동 만료
- 유연한 배포: API Server는 재시작만, Bot은 재빌드로 구분하여 운영
- Cowork 수준의 에이전트 능력: 동일한 Opus 4.6 모델 + MCP + 도구 사용, 더 높은 커스터마이징 자유도
NAS 하나로 개인 AI 코딩 어시스턴트를 만들 수 있다는 게 핵심입니다. Claude Code CLI의 stream-json + --resume 기능 덕분에 Discord와의 자연스러운 연동이 가능했습니다.