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

핵심 포인트


2. API Server (Claude CLI 실행 서버)

동작 흐름

  1. Discord Bot 또는 n8n으로부터 HTTP POST 요청 수신
  2. Claude CLI를 spawn으로 실행 (stream-json + verbose 모드)
  3. stdout을 줄 단위로 파싱하여 tool_use 이벤트 감지
  4. 3초마다 Discord Bot으로 진행상황 전송
  5. 완료 시 최종 결과 + 도구 사용 로그 전송
  6. 세션 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로 전달되어 자동으로:

  1. obsidian/관심환자_목록.md 마스터 테이블에 새 행 추가
  2. obsidian/patients/{환자번호}_{이름}.md 개별 노트 생성
  3. 협진 내용 포함 시 협진 답변 평가 (잘한 점 / 보완할 점 / 개선 답변) 추가
/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> 옵션으로 이전 세션을 이어갈 수 있습니다.

구현 방식

  1. api-server.js에서 채널별로 마지막 session_id를 메모리에 저장 (Map)
  2. /claudetype: 'new_task' → 기존 세션 삭제, 새로 시작
  3. /chattype: 'continue' → 저장된 session_id로 --resume 실행
  4. 세션은 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-botdocker 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           # ✅

재빌드 시 사라지는 데이터


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에 안 보일 때


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처럼.


마무리

이 시스템의 장점:

NAS 하나로 개인 AI 코딩 어시스턴트를 만들 수 있다는 게 핵심입니다. Claude Code CLI의 stream-json + --resume 기능 덕분에 Discord와의 자연스러운 연동이 가능했습니다.


Comments