맥미니에서 24시간 끊기지 않는 Hermes 셋팅 (ft. Tailscale, Hermes Desktop Remote)

집에 있는 맥미니 한 대를 "내 개인 AI 서버"로 만들어서, 노트북·아이패드·폰 어디서든 붙어 쓰는 이야기입니다. 터미널 창을 닫아도, 로그아웃해도, 재부팅해도 안 죽습니다.


소개

시도하고자 했던 것과 그 이유

저는 Hermes Agent(오픈소스 AI 에이전트)를 텔레그램·슬랙 봇으로 붙여서 씁니다. 메신저로 "이거 조사해줘", "이 파일 정리해줘" 하면 알아서 해주는 개인 비서예요.

이번에 헤르메스 에이전트 셋팅을 하면서 집에 있는 맥미니를 서버로 하여 24시간 내 비서가 가동하면서 메신저 앱, 데스크탑앱을 다른 디바이스에서도 끊김없이 작업하기 위한 환경을 셋팅하기 위해 아래와 같은 목표를 세워 진행했습니다.

목표

24/7 상주

터미널 닫아도, 로그아웃해도, 재부팅해도 알아서 살아있기

내 기기에서만 접속

노트북·아이패드·폰에서는 되고, 인터넷 아무나는 안 되기

데스크탑 앱으로

맥북에서는 브라우저 말고 정식 Hermes 앱으로 서버에 붙기

이 세 개를 launchd + Tailscale + Hermes Desktop remote 조합으로 풀었습니다.


진행 방법

전체 그림 먼저

┌─────────────────────── 맥미니 (24시간 켜둠) ───────────────────────┐
│                                                                    │
│   launchd (macOS 자동실행 관리자)                                  │
│     ├── ai.hermes.gateway     ← 메신저 봇 (죽으면 자동 재시작)     │
│     └── ai.hermes.dashboard   ← 웹 관리화면 (죽으면 자동 재시작)   │
│                                    │                               │
│                                    └─ 100.x.y.z:9119 에만 바인딩   │
│                                       (= Tailscale 주소에만 열림)  │
└────────────────────────────────────┼───────────────────────────────┘
                                     │  Tailscale 사설망 (암호화)
        ┌────────────────────────────┼────────────────────────────┐
        │                            │                            │
   ┌────▼────┐                  ┌────▼────┐                  ┌────▼────┐
   │  맥북    │                  │ 아이패드 │                  │   폰     │
   │ Hermes  │                  │ 브라우저 │                  │ 브라우저 │
   │ Desktop │                  └─────────┘                  └─────────┘
   │ (remote)│
   └─────────┘
                    ❌ 인터넷의 다른 사람들 — 아예 도달 불가

핵심 아이디어는 딱 두 줄입니다.

  1. launchd로 "죽으면 다시 켜주는 관리인"을 붙인다.

  2. Tailscale IP에만 포트를 연다 → 내 기기들만 보이고, 인터넷에는 아예 안 열린다.

준비물

  • 항상 켜둘 맥 한 대 (맥미니가 전기도 적게 먹고 조용해서 딱입니다)

  • Hermes Agent 설치 완료 (~/.hermes/hermes-agent/)

  • Tailscale 계정 (무료 플랜으로 개인 기기 100대까지 충분)

💡 맥미니를 서버로 쓸 거면 시스템 설정 → 에너지 절약에서 "가능하면 하드 디스크를 잠자기 상태로 두기" 해제, "정전 후 자동으로 다시 시작" 켜기, "디스플레이 꺼도 잠자기 안 함"을 먼저 잡아두세요. 이거 안 하면 launchd가 아무리 좋아도 맥이 자버립니다.


Step 1. Tailscale 깔고 내 서버 주소 확인하기

Tailscale은 내 기기들끼리만 통하는 가상의 사내망을 만들어줍니다. 공유기 포트포워딩도, 고정 IP도, DDNS도 필요 없어요. 설치하고 로그인만 하면 각 기기가 100.x.y.z 형태의 주소를 하나씩 받습니다.

맥미니, 맥북, 폰, 아이패드에 각각 깔고 같은 계정으로 로그인하세요.

맥미니에서 내 주소를 확인합니다.

tailscale ip -4
# 출력 예: 100.101.102.103
# 내 기기 목록 확인 — 여기 뜨는 기기들만 서버에 접근할 수 있게 됩니다
tailscale status

이 IP는 노드마다 고정이라 웬만해선 안 바뀝니다. 아래 설정에 계속 쓰이니 메모해두세요. (이 글에서는 100.101.102.103을 예시로 씁니다. 본인 IP로 바꿔서 따라 하세요.)

컴퓨터의 장치 설정 스크린샷

Step 2. 대시보드를 "Tailscale 주소에만" 열기

여기가 이 셋팅의 심장입니다.

hermes dashboard --host 100.101.102.103 --port 9119

--hostTailscale IP를 박는 것이 포인트예요. 이러면 9119 포트는 Tailscale 인터페이스에만 열립니다.

  • ✅ 내 Tailscale 기기들 → 접속됨

  • ❌ 같은 집 와이파이의 다른 기기 → 안 됨

  • ❌ 인터넷 → 안 됨

⚠️ 절대 하면 안 되는 것: --host 0.0.0.0. 이건 "모든 인터페이스에 열어라"는 뜻이라 공유기 설정에 따라 인터넷에 노출될 수 있습니다. API 키가 들어있는 화면이니 절대 금지.

인증도 반드시 켜세요. 루프백(127.0.0.1)이 아닌 주소에 바인딩할 때 Hermes는 인증을 요구합니다. ~/.hermes/.env에 아래 세 줄을 넣습니다.

HERMES_DASHBOARD_BASIC_AUTH_USERNAME=원하는아이디
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=충분히_긴_랜덤_비밀번호
HERMES_DASHBOARD_BASIC_AUTH_SECRET=세션서명용_랜덤_문자열

비밀번호와 시크릿은 이렇게 뽑으면 편합니다.

openssl rand -base64 24   # 두 번 실행해서 각각 PASSWORD / SECRET 에 사용

💬 Hermes에는 인증을 끄는 --insecure 옵션도 있습니다. 저는 처음에 "어차피 내 기기뿐인데" 하고 그걸 썼는데, 기기를 하나 추가하는 순간 그 기기도 API 키 화면에 그냥 들어갈 수 있다는 걸 깨닫고 BASIC_AUTH를 켜는 쪽으로 바꿨습니다. 자물쇠는 두 개 채우는 게 맞아요.


Step 3. launchd로 24시간 상주시키기

리눅스 글들은 대부분 systemd로 설명하는데, 맥에는 systemd가 없습니다. 맥의 짝은 launchd예요. 사용자 서비스는 ~/Library/LaunchAgents/에 plist(XML 설정파일) 하나 넣어주면 끝입니다.

아래 파일을 ~/Library/LaunchAgents/ai.hermes.dashboard.plist로 저장하세요. YOUR_NAME(맥 계정명)과 100.101.102.103(Tailscale IP), 그리고 PATH의 노드 버전은 본인 환경에 맞게 바꿔야 합니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>ai.hermes.dashboard</string>
​
    <key>ProgramArguments</key>
    <array>
        <string>/Users/YOUR_NAME/.hermes/hermes-agent/venv/bin/python</string>
        <string>-m</string>
        <string>hermes_cli.main</string>
        <string>dashboard</string>
        <string>--host</string>
        <string>100.101.102.103</string>
        <string>--port</string>
        <string>9119</string>
        <string>--no-open</string>
        <string>--skip-build</string>
    </array>
​
    <key>WorkingDirectory</key>
    <string>/Users/YOUR_NAME/.hermes/hermes-agent</string>
​
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/Users/YOUR_NAME/.hermes/hermes-agent/venv/bin:/Users/YOUR_NAME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
        <key>VIRTUAL_ENV</key>
        <string>/Users/YOUR_NAME/.hermes/hermes-agent/venv</string>
        <key>HERMES_HOME</key>
        <string>/Users/YOUR_NAME/.hermes</string>
        <key>PYTHONUNBUFFERED</key>
        <string>1</string>
    </dict>
​
    <key>RunAtLoad</key>
    <true/>
​
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
​
    <key>StandardOutPath</key>
    <string>/Users/YOUR_NAME/.hermes/logs/dashboard.log</string>
​
    <key>StandardErrorPath</key>
    <string>/Users/YOUR_NAME/.hermes/logs/dashboard.error.log</string>
</dict>
</plist>

각 옵션이 하는 일 (이게 이해되면 다른 서비스도 다 만들 수 있습니다)

하는 일

Label

서비스 이름. launchctl 명령에서 이 이름으로 부릅니다

ProgramArguments

실행할 명령. 터미널에서 치던 걸 띄어쓰기 단위로 쪼개서 넣습니다

EnvironmentVariablesPATH

launchd는 내 셸 설정(.zshrc)을 안 읽습니다. PATH를 직접 안 주면 "command not found"가 납니다. ⚠️ 초보자 1번 함정

RunAtLoad

로그인/부팅 시 자동 실행

KeepAliveSuccessfulExit: false

비정상 종료 시 자동 재시작. 이게 "안 끊긴다"의 정체입니다

StandardOutPath / ErrorPath

로그 파일 경로. 문제 생기면 여기부터 봅니다

--skip-build

미리 빌드해둔 화면을 그대로 서빙. 재부팅할 때마다 npm 빌드를 돌리지 않아서 훨씬 튼튼합니다

등록하고 켜기

launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/ai.hermes.dashboard.plist
launchctl enable    "gui/$(id -u)/ai.hermes.dashboard"
launchctl kickstart -k "gui/$(id -u)/ai.hermes.dashboard"

잘 떴는지 확인

# state = running 이면 성공
launchctl print "gui/$(id -u)/ai.hermes.dashboard" | grep -E 'state|pid'
​
# HTTP 응답 확인 (인증을 켜면 302, 껐으면 200)
curl -s -o /dev/null -w '%{http_code}\n' http://100.101.102.103:9119/

설정을 고쳤을 때 / 끌 때

# plist 수정 후 반영
launchctl bootout   "gui/$(id -u)/ai.hermes.dashboard"
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/ai.hermes.dashboard.plist
​
# 로그 실시간으로 보기 (문제 생기면 1순위)
tail -f ~/.hermes/logs/dashboard.error.log

메신저 봇(게이트웨이)도 똑같이 하면 됩니다. 위 plist를 복사해서 Labelai.hermes.gateway로, ProgramArgumentsdashboard 자리를 게이트웨이 실행 인자로 바꾸면 끝이에요. 저는 이 패턴으로 용도가 다른 봇 7개를 각각 별도 서비스로 띄워서 쓰고 있습니다. (그 얘기는 길어서 다음 글에…)


Step 4. 배포 스크립트로 묶기 (재현 가능하게)

위 과정을 매번 손으로 하면 언젠가 한 단계를 빼먹습니다. 그래서 몇 번을 실행해도 결과가 같은(멱등) 스크립트로 묶어뒀어요. 저는 이걸 infra/launchd/install-dashboard.sh로 깃에 넣어 관리합니다.

#!/usr/bin/env bash
# Hermes 웹 대시보드 24/7 launchd 서비스 배포 (Tailscale IP 바인딩). 멱등 — 재실행 안전.
set -euo pipefail
​
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLIST_SRC="$REPO_DIR/ai.hermes.dashboard.plist"
LABEL="ai.hermes.dashboard"
PLIST_DST="$HOME/Library/LaunchAgents/$LABEL.plist"
HERMES_AGENT="$HOME/.hermes/hermes-agent"
WEB_DIR="$HERMES_AGENT/web"
DIST="$HERMES_AGENT/hermes_cli/web_dist"   # vite outDir — web/dist 아님!
BIND_IP="100.101.102.103"                  # ← 본인 Tailscale IP
PORT="9119"
​
echo "==> Pre-flight"
[ -x "$HERMES_AGENT/venv/bin/python" ] || { echo "✗ hermes venv python 없음"; exit 1; }
[ -f "$PLIST_SRC" ] || { echo "✗ plist 원본 없음: $PLIST_SRC"; exit 1; }
​
# 바인딩 IP가 이 호스트의 실제 Tailscale IP와 같은지 점검 (다르면 경고)
TS_IP="$(/Applications/Tailscale.app/Contents/MacOS/Tailscale ip -4 2>/dev/null || tailscale ip -4 2>/dev/null || true)"
if [ -n "$TS_IP" ] && [ "$TS_IP" != "$BIND_IP" ]; then
  echo "⚠ Tailscale IP($TS_IP) != plist 바인딩 IP($BIND_IP). plist를 갱신 후 진행하세요." >&2
fi
​
echo "==> web dist 확인 ($DIST)"
if [ ! -f "$DIST/index.html" ]; then
  echo "   dist 없음 — 빌드 (npm install && npm run build)…"
  ( cd "$WEB_DIR" && npm install && npm run build )
else
  echo "   dist 존재 — 빌드 생략 (강제 재빌드: '$DIST' 삭제 후 재실행)"
fi
[ -f "$DIST/index.html" ] || { echo "✗ dist 빌드 실패"; exit 1; }
​
echo "==> 잔여 대시보드 프로세스 정리 (메신저 봇은 건드리지 않음)"
hermes dashboard --stop || true
​
echo "==> plist 설치 -> $PLIST_DST"
mkdir -p "$HOME/Library/LaunchAgents"
cp "$PLIST_SRC" "$PLIST_DST"
​
echo "==> launchd 서비스 (재)로드"
launchctl bootout   "gui/$(id -u)/$LABEL" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$PLIST_DST" 2>/dev/null || launchctl load -w "$PLIST_DST"
launchctl enable    "gui/$(id -u)/$LABEL" 2>/dev/null || true
launchctl kickstart -k "gui/$(id -u)/$LABEL" 2>/dev/null || true
​
echo "==> 검증 (바인딩 대기…)"
sleep 4
launchctl print "gui/$(id -u)/$LABEL" 2>/dev/null | grep -E "state =|pid =" | head || true
code="$(curl -s -o /dev/null -w '%{http_code}' "http://$BIND_IP:$PORT/" || true)"
echo "   http://$BIND_IP:$PORT/ -> HTTP $code"
echo "✓ 완료. Tailscale 기기 브라우저에서 http://$BIND_IP:$PORT 접속 (이름 말고 IP로!)"
chmod +x infra/launchd/install-dashboard.sh
./infra/launchd/install-dashboard.sh

Step 5. 폰·아이패드·노트북에서 접속하기

이제 Tailscale이 켜진 기기의 브라우저에서 아래 주소를 열면 됩니다.

http://100.101.102.103:9119

⚠️ 여기서 제가 30분 날렸습니다. Tailscale은 내기기이름.tailXXXX.ts.net 같은 예쁜 이름(MagicDNS)도 주는데, 그 이름으로 접속하면 400 Invalid Host header가 뜹니다.

버그가 아니라 보안 기능이에요. 대시보드가 들어온 Host 헤더를 바인딩 주소와 엄격하게 대조합니다(DNS 리바인딩 공격 방어). IP로 바인딩했으면 IP로만 통과해요. 그냥 IP를 북마크에 넣으세요.

반대로 --host에 MagicDNS 이름을 넣으면 이름으로 접속되고 IP가 막힙니다. IP가 바뀔 걱정이 없으니 저는 IP 쪽을 택했습니다.


Step 6. 맥북에서 Hermes Desktop을 "리모트"로 붙이기

브라우저로도 충분하지만, 맥북에서는 정식 데스크탑 앱으로 쓰는 게 훨씬 낫습니다. 핵심은 맥북에서 AI를 돌리는 게 아니라, 맥미니 서버에 붙기만 한다는 점이에요. 맥북은 화면 역할만 합니다. 그래서 맥북 배터리도 안 닳고, 맥북을 덮어도 서버는 계속 일합니다.

맥북에 아래 파일을 만듭니다.

~/Library/Application Support/Hermes/connection.json

{
  "mode": "remote",
  "remote": {
    "url": "http://100.101.102.103:9119",
    "authMode": "oauth"
  }
}

이게 전부입니다. 앱을 실행하면 알아서 서버에 붙어요.

연결됐는지는 로그로 확인합니다.

tail -20 ~/.hermes/logs/desktop.log
# "Remote Hermes backend is ready" 가 보이면 성공
Adobe Adobe Adobe Adobe adobe a 설정의 스크린샷

데스크탑 앱 빌드에서 막힌다면 — 저는 hermes desktop 빌드가 electronDist does not exist로 실패해서 한참 헤맸습니다. 네트워크 문제가 아니라 npm workspaces가 Electron을 하위 폴더에 두는데 빌더는 루트에서 찾는 경로 불일치였어요. 심링크 한 줄이면 해결됩니다.

ln -sfn ~/.hermes/hermes-agent/apps/desktop/node_modules/electron \
        ~/.hermes/hermes-agent/node_modules/electron
cd ~/.hermes/hermes-agent/apps/desktop && npm run pack

빌드한 앱을 /Applications로 옮길 때는 cp -R을 쓰면 안 됩니다. 코드 서명이 깨져서 "앱이 손상되었습니다"가 떠요. ditto로 복사하고 재서명하세요.

ditto ~/.hermes/hermes-agent/apps/desktop/release/mac-arm64/Hermes.app /Applications/Hermes.app
xattr -cr /Applications/Hermes.app
codesign --force --deep --sign - /Applications/Hermes.app
codesign --verify /Applications/Hermes.app     # "valid on disk" 확인

빌드 없이 바로 써보고 싶다면 hermes desktop --source 한 줄이면 됩니다. (매번 40초쯤 빌드하지만 셋팅은 0)


Step 7. 최종 점검 체크리스트

여기까지 왔으면 아래 5개가 전부 통과해야 합니다.

# 1) 서비스가 running 인가
launchctl print "gui/$(id -u)/ai.hermes.dashboard" | grep -E 'state|pid'
​
# 2) 대시보드가 응답하는가 (인증 켰으면 302, 껐으면 200)
curl -s -o /dev/null -w '%{http_code}\n' http://100.101.102.103:9119/
​
# 3) 엉뚱한 이름으로는 막히는가 (400이 나와야 정상)
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: bogus.example.com' http://100.101.102.103:9119/
​
# 4) 메신저 봇도 살아있는가
hermes gateway status
​
# 5) 진짜 안 죽는지 — 강제로 죽여보고 되살아나는지 확인 (KeepAlive 테스트)
pkill -f 'hermes_cli.main dashboard' ; sleep 15
launchctl print "gui/$(id -u)/ai.hermes.dashboard" | grep -E 'state|pid'   # 새 pid로 running

5번이 이 글의 하이라이트입니다. 프로세스를 강제로 죽였는데 15초 뒤에 새 PID로 살아나 있으면, 그게 바로 "24시간 안 끊긴다"의 실물 증거예요.


결과와 배운 점

결과

  • 맥미니가 재부팅되든, 로그아웃하든, 프로세스가 죽든 알아서 되살아나는 상태가 됐습니다. 한 달 넘게 운영 중인데 제가 손댄 적은 없어요.

  • 폰·아이패드·노트북 어디서든 Tailscale만 켜져 있으면 내 AI 서버에 붙습니다. 카페에서도, 지하철에서도 됩니다.

  • 그런데 인터넷에는 포트가 하나도 안 열려 있습니다. 공유기 설정을 아예 안 건드렸어요.

  • 맥북 데스크탑 앱은 서버에 붙기만 하니까, 맥북을 덮어도 서버 쪽 작업은 계속 돌아갑니다.

시행착오

① MagicDNS 이름으로 접속하면 400 Invalid Host header 버그인 줄 알고 한참 뒤졌는데 보안 기능이었습니다. 바인딩한 주소 형태(IP면 IP, 이름이면 이름)로만 접속됩니다. → IP를 북마크하세요.

② launchd가 "command not found"를 뱉음 launchd는 .zshrc안 읽습니다. 터미널에서 되던 게 launchd에서 안 되는 이유의 90%가 이겁니다. → plist의 EnvironmentVariablesPATH를 직접 넣으세요.

③ 대시보드가 무한 새로고침(깜빡깜빡) 제가 쓰던 버전에 회귀 버그가 있었습니다. 인증 없는 모드에서 앱이 매번 인증 확인 API를 부르고 → 설계상 항상 401 → 그걸 "토큰 만료"로 오해해서 페이지를 새로고침 → 무한루프. 상위 저장소에 이미 수정 커밋이 있어서 그걸 가져다 붙였는데, 제 버전 컷팅 바로 다음 날 머지된 거라 한 끗 차로 못 받은 거였어요. → 증상이 이상하면 내 설정을 의심하기 전에 업스트림 커밋 로그부터 보세요. 그리고 그런 로컬 수정은 업데이트 때 덮어써지니 어딘가에 반드시 적어두세요.


궁금한 점 댓글 주시면 아는 만큼 답해드릴게요. 특히 "우리 집 맥미니로도 될까요?"는 됩니다. 항상 켜둘 수만 있으면 돼요.

2
1개의 답글
밀어주고 끌어주는

온·오프라인 AI 스터디

AI로 어디까지 할 수 있는지
직접 확인하실 분만 신청하세요.