집 NAS는 Claude Code로 관리한다. Windows PC에서 Claude Code를 열고, SSH로 NAS에 붙어 컨테이너를 점검하고, Caddy 설정을 고치고, 작업 기록을 git 저장소에 남긴다. 편했지만 두 가지가 아쉬웠다. Claude가 보는 건 SSH 터미널뿐이라 Jenkins 빌드 로그나 Gitea 이슈는 내가 복사해서 붙여 줘야 했다. 그리고 PC가 꺼져 있으면 아무것도 할 수 없었다. 밖에서 휴대폰으로 "컨테이너 하나 재시작해 줘" 정도는 시키고 싶었다.
이 글은 그 두 가지를 해결한 기록이다. 하나는 MCP로 Jenkins·Gitea를 Claude에 직접 연결한 것, 다른 하나는 Claude Code를 NAS 컨테이너에 상주시키고 Remote Control로 휴대폰에서 접속하게 한 것이다.
MCP: AI에게 도구를 쥐여 주는 규격
MCP(Model Context Protocol)는 AI 클라이언트가 외부 도구를 부르는 방식을 정해 둔 규격이다. 서버가 "이런 도구가 있다"(get_build_log, list_issues 같은)고 알리면, Claude가 대화 중에 필요한 도구를 골라 호출한다. 서버 쪽은 표준 입출력으로 붙는 로컬 프로세스일 수도 있고, HTTP 엔드포인트일 수도 있다. 집 서버에서는 HTTP 쪽이 편하다. 서버를 컨테이너 하나로 띄워 두면 PC든 NAS든 주소만 등록하면 된다.
붙인 건 두 개다.
| MCP 서버 | 형태 | 인증 | 노출 |
|---|---|---|---|
| Jenkins | Jenkins 플러그인 mcp-server (도구 21개) | Jenkins 사용자 API 토큰 (Basic) | Jenkins 경로 /mcp-server/mcp |
| Gitea | 공식 gitea-mcp-server 컨테이너 | Gitea 액세스 토큰 (Bearer) | LAN 전용 포트 |
Jenkins: 플러그인 하나, 그런데 경로가 함정
Jenkins는 MCP 서버를 플러그인으로 제공한다. 설치하면 Jenkins 자신이 Streamable HTTP 엔드포인트가 된다. 컨테이너 안에서 jenkins-plugin-cli로 설치했는데, 처음에는 재시작해도 플러그인이 보이지 않았다.
# 경로를 안 주면 /usr/share/jenkins/ref/plugins 에 받아진다 (이미지 빌드용 경로)
jenkins-plugin-cli --plugins mcp-server
# 실행 중인 Jenkins가 읽는 곳은 JENKINS_HOME
jenkins-plugin-cli --plugin-download-directory /var/jenkins_home/plugins --plugins mcp-serverjenkins-plugin-cli의 기본 경로는 Docker 이미지를 만들 때 플러그인을 미리 넣어 두는 ref 폴더다. 이미 돌고 있는 Jenkins에 넣으려면 JENKINS_HOME/plugins를 직접 지정해야 한다. 의존성(jackson3-api)까지 받고 재시작하자 도구 21개로 초기화됐다. 빌드 목록·로그 검색·재빌드·테스트 결과 같은 것들이다.
인증은 Jenkins 사용자 API 토큰이다. 관리자 계정 대신 Claude용 claude 계정을 따로 만들어 토큰을 발급했다.
Gitea: 서버에는 토큰을 두지 않는다
Gitea는 공식 MCP 서버 이미지가 있다. 표준 입출력 모드가 기본인데, HTTP 모드로 띄워 상주시켰다.
# Gitea MCP Server (HTTP, LAN 전용). 토큰은 서버에 두지 않고 클라이언트가 Authorization 헤더로 보낸다.
services:
gitea-mcp:
image: docker.gitea.com/gitea-mcp-server:latest
container_name: gitea-mcp
command: ["/app/gitea-mcp", "-t", "http", "-p", "8080"]
environment:
GITEA_HOST: http://192.168.50.10:13330
ports:
- "13331:8080"
restart: unless-stopped환경 변수로 토큰을 넣을 수도 있지만 일부러 넣지 않았다. 서버에 토큰이 있으면 이 포트에 닿는 누구나 내 권한으로 Gitea를 만질 수 있다. 토큰을 클라이언트가 Authorization 헤더로 보내게 하면 서버는 그냥 중계기가 된다. 헤더 없이 부르면 token is required로 거절한다. 그래도 리버스 프록시에는 올리지 않고 LAN에서만 열어 두었다.
등록은 Claude Code CLI 한 줄이다.
claude mcp add --transport http gitea http://192.168.50.10:13331/mcp \
--header "Authorization: Bearer <토큰>"토큰 권한은 필요한 만큼만 줬다. 저장소·이슈 쓰기, 사용자·조직·알림 읽기. 관리자 권한은 없다.
토큰은 화면에 남기지 않는다
토큰을 넣는 일은 스크립트로 한다. 입력이 화면에 보이지 않고, 등록 전에 토큰이 맞는지 먼저 확인한다.
read -r -s -p "API 토큰 (화면에 표시되지 않음): " TOKEN
echo
[ -n "$TOKEN" ] || { echo "토큰이 비어 있음 — 중단"; exit 1; }
# 등록 전에 토큰이 실제로 통하는지 확인
code=$(curl -s -o /dev/null -w '%{http_code}' -u "$USER_ID:$TOKEN" "$URL/me/api/json")
[ "$code" = 200 ] || { echo "토큰 확인 실패 (HTTP $code)"; exit 1; }
AUTH=$(printf '%s:%s' "$USER_ID" "$TOKEN" | base64 -w0)
claude mcp add --scope user --transport http jenkins "$URL/mcp-server/mcp" \
--header "Authorization: Basic $AUTH" | grep -v Authorization
unset TOKEN AUTH출력에서 Authorization 줄을 거르는 것도 같은 이유다. 토큰은 터미널 기록에도, Claude와의 대화에도 남기지 않는다.
Claude Code를 NAS에 상주시키기
MCP를 붙이자 PC에서는 할 수 있는 일이 많아졌다. 남은 문제는 PC가 꺼져 있을 때다. Claude Code에는 Remote Control이라는 기능이 있다. claude remote-control을 실행해 두면 그 머신의 Claude Code가 claude.ai/code와 Claude 앱의 세션 목록에 나타나고, 휴대폰이나 브라우저에서 그 세션에 말을 걸 수 있다. 실제 작업은 그 머신에서 돈다.
그렇다면 24시간 켜져 있는 NAS에서 돌리면 된다. 연결은 NAS에서 바깥으로 나가는 아웃바운드뿐이라 포트를 열 필요도 없다.
컨테이너 구성
DSM에 직접 설치하는 대신 컨테이너로 만들었다. 이미지는 최소한의 도구만 넣은 Debian이다.
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl git openssh-client tmux ripgrep jq less procps \
iputils-ping dnsutils nano locales \
&& rm -rf /var/lib/apt/lists/*
# NAS 사용자(uid 1026, gid 100 users)와 맞춰 바인드 마운트 권한 문제를 없앤다
RUN useradd -u 1026 -g 100 -m -s /bin/bash claude
COPY --chmod=755 entrypoint.sh rc-loop.sh /usr/local/bin/
ENV LANG=C.UTF-8 TZ=Asia/Seoul PATH=/home/claude/.local/bin:$PATH
USER claude
WORKDIR /workspace
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]Claude Code 자체는 이미지에 넣지 않았다. 홈 폴더를 볼륨으로 빼고, 처음 기동할 때 공식 설치 스크립트로 그 안에 설치한다. 이렇게 하면 Claude Code가 스스로 자동 업데이트해도 컨테이너를 다시 만들 때 사라지지 않고, 로그인 정보·MCP 설정·SSH 키도 함께 남는다. 이미지는 거의 바뀔 일이 없으니 Watchtower 대상에서도 뺐다.
#!/bin/bash
# 컨테이너 시작: Claude Code 설치(홈 볼륨에 영구 보관, 이후 자동 업데이트) → tmux에서 Remote Control 루프
set -e
cd "$HOME"
if [ ! -x "$HOME/.local/bin/claude" ]; then
curl -fsSL https://claude.ai/install.sh | bash
fi
if [ ! -f "$HOME/.ssh/id_ed25519" ]; then
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
ssh-keygen -q -t ed25519 -N '' -C claude@sejong-nas -f "$HOME/.ssh/id_ed25519"
fi
# tmux 세션 main: 창 0 = Remote Control 서버, 이후 창은 수동 작업용
exec tmux -u new-session -s main -n rc /usr/local/bin/rc-loop.shtmux 안에서 돌리는 이유는 사람이 들여다볼 창구가 필요해서다. 권한 확인 프롬프트가 떠 있거나 로그인이 풀렸을 때 docker exec -it claude-code tmux attach -t main으로 들어가 바로 볼 수 있다.
끊겨도 다시 살아나게
Remote Control 서버 모드는 네트워크가 10분 정도 끊기면 스스로 종료된다. 집 인터넷이 잠깐 나갔다 들어오는 일은 흔하니, 루프로 감싸 다시 띄운다.
#!/bin/bash
# Remote Control 서버를 계속 살려 둔다.
# 서버 모드는 네트워크가 ~10분 끊기면 종료되므로 루프로 재시작한다.
DIR=/workspace/claude-server-manage
[ -d "$DIR" ] || DIR=/workspace
cd "$DIR"
while true; do
echo "[$(date '+%F %T')] claude remote-control 시작 ($DIR)"
claude remote-control --name sejong-nas
echo "[$(date '+%F %T')] 종료(코드 $?) — 30초 후 재시작"
sleep 30
done작업 디렉터리는 NAS 관리 저장소의 클론이다. 그 안의 CLAUDE.md에 접속 방법, 서비스 구조, "삭제는 확인받고 할 것" 같은 작업 규칙이 적혀 있어서, 휴대폰에서 연 세션도 PC에서와 똑같은 규칙으로 움직인다.
인증에서 하나 걸린 점이 있다. Remote Control은 claude auth login으로 하는 구독 계정 로그인에서만 동작한다. API 키나 setup-token으로 발급한 장기 토큰으로는 안 된다. 그래서 첫 기동 후 tmux 창에 들어가 직접 로그인했다.
컨테이너에서 NAS를 다루는 방법
이 컨테이너는 NAS 안에 있지만 NAS를 직접 만지지는 못한다. Docker 소켓을 마운트하면 쉽겠지만, 그러면 컨테이너가 곧 NAS의 root가 된다. 대신 PC에서 하던 것과 똑같이 SSH로 NAS에 붙게 했다.
- 컨테이너가 처음 뜰 때 만든 SSH 키를 NAS 사용자의
authorized_keys에 등록한다. - 그 키에는
from="10.250.0.0/28"을 붙여, 이 컨테이너 네트워크에서 온 접속만 받게 한다. - NAS에서 비밀번호 없이 쓸 수 있는 sudo는
docker하나뿐이다. 나머지 sudo는 비밀번호가 필요하니, Claude는 그런 명령을 나에게 건넨다.
services:
claude-code:
build: .
image: claude-code:local
container_name: claude-code
hostname: sejong-nas-claude
restart: unless-stopped
tty: true # tmux를 포그라운드로 돌리기 위해
stdin_open: true
init: true
mem_limit: 4g
volumes:
- ./home:/home/claude # Claude Code·로그인·설정·SSH 키 (비밀값)
- ./workspace:/workspace # 저장소 클론
# 포트 공개 없음 — Remote Control은 아웃바운드 연결만 사용
labels:
- com.centurylinklabs.watchtower.enable=false
networks:
default:
ipam:
# NAS authorized_keys의 from="10.250.0.0/28" 제한과 맞춘다 — 바꾸면 함께 수정
config:
- subnet: 10.250.0.0/28솔직히 말하면 docker를 쓸 수 있다는 건 사실상 root 권한이다. 마음먹으면 호스트 디렉터리를 마운트한 컨테이너를 띄울 수 있다. 그래서 이 방식의 안전장치는 기술보다 운영 쪽에 있다. 키가 쓰일 수 있는 위치를 좁히고, 위험한 작업은 확인받게 규칙을 두고, 모든 변경은 저장소에 기록으로 남긴다.
배포하며 걸린 것들
홈 폴더가 읽기 전용으로 보였다. Synology 공유폴더 아래에 만든 폴더는 상위의 Windows식 ACL을 상속한다. 컨테이너 안에서는 이게 r-x로 보여서 Claude Code 설치가 실패했다. 바인드 마운트할 폴더만 synoacltool -del로 ACL을 지우고 일반 권한으로 돌렸다. 배포 스크립트에 넣어 두었다.
네트워크 대역이 키 제한과 어긋났다. compose가 자동으로 잡은 대역은 192.168.240.0/20이었는데, 키에는 다른 대역을 적어 두었다. 자동 할당은 다시 만들 때마다 바뀔 수 있으니 compose에서 10.250.0.0/28로 고정했다. 컨테이너 몇 개면 /28(16개)로 충분하다.
브라우저 MCP는 붙였다가 뗐다
블로그 글에 넣을 캡처를 위해 Claude 전용 크로미움 컨테이너와 Playwright MCP도 띄웠다. 내가 웹 화면(KasmVNC)으로 한 번 로그인해 두면, Claude가 같은 브라우저를 조작해 로그인된 화면을 찍는다.
문제는 토큰이었다. Playwright MCP는 페이지를 열거나 클릭할 때마다 페이지의 접근성 트리 전체를 응답에 실어 보낸다. Claude가 화면을 "보기" 위한 정보지만, 캡처 한 장 찍는 데에도 수천 토큰씩 대화에 쌓였다. 그래서 MCP 등록은 해제하고, 같은 MCP 엔드포인트를 curl로 부르는 작은 스크립트를 만들었다. 응답은 버리고 PNG만 남긴다.
# browser-shot <url> <파일명.png> [가로] [세로] [full]
call "{... \"name\":\"browser_resize\",\"arguments\":{\"width\":$W,\"height\":$H}}}"
call "{... \"name\":\"browser_navigate\",\"arguments\":{\"url\":\"$URL\"}}}"
sleep 2
call "{... \"name\":\"browser_take_screenshot\",\"arguments\":{\"filename\":\"/captures/$OUT\"}}}"MCP는 Claude가 직접 붙어야만 쓸 수 있는 게 아니다. 결국 HTTP 위의 JSON-RPC라서, 응답이 필요 없을 때는 스크립트에서 불러도 된다. 무엇을 MCP로 연결하고 무엇을 스크립트로 둘지는 응답 크기를 보고 정하게 됐다.
지금 쓰는 모습
휴대폰 Claude 앱의 세션 목록에 sejong-nas가 떠 있다. 들어가서 하는 일은 대체로 이런 것들이다.
- "컨테이너 상태 점검해 줘" — 재시작 루프나 unhealthy가 있는지 훑어본다.
- "Jenkins 마지막 빌드 왜 실패했어?" — Jenkins MCP로 로그를 검색해 원인 부분만 가져온다.
- "오늘 밤 9시 방송 녹화 대기 걸어 줘" — NAS의 녹화 컨테이너에 명령을 넣는다.
- "이 작업 README에 기록하고 커밋해 줘" — Gitea에 바로 푸시한다.
이 글의 초안도 그 세션에서 썼다. 저장소의 작업 기록과 설정 파일을 읽어 정리하고, Ghost Admin API로 임시저장까지 올렸다. 발행 버튼은 내가 누른다.
정리
| 무엇 | 어떻게 |
|---|---|
| Jenkins 연결 | Jenkins MCP 플러그인, 전용 계정 토큰 |
| Gitea 연결 | HTTP 모드 MCP 컨테이너, 토큰은 클라이언트 헤더로, LAN 전용 |
| 상주 실행 | NAS 컨테이너 + tmux + Remote Control 재시작 루프 |
| NAS 조작 | SSH 키(출발지 대역 제한) + docker만 NOPASSWD |
| 캡처 | 브라우저 MCP는 스크립트로만 호출 |
AI에게 서버를 맡긴다고 하면 거창하게 들리지만, 해 보니 대부분은 예전부터 하던 인프라 일이었다. 토큰을 어디에 둘지, 어떤 포트를 열지, 권한을 얼마나 줄지, 끊겼을 때 어떻게 살릴지. 그걸 정해 두고 나니 Claude는 그 틀 안에서 꽤 믿을 만한 동료가 됐다.