본문으로 건너뛰기
Piyak Labs
돌아가기

VPS 하나를 AI 에이전트 + 어디서든 접근하는 개발 거점으로

시작은 이전에 유행했던 Openclaw

남는 맥북에어 노트북이 하나 있었습니다. Openclaw(개인 PC에서 돌리는 오픈소스 AI 에이전트)가 뜨기 시작했을 때, 개발자로서 활용해보고 싶어 남는 맥북에어에 설치하고 돌려봤었습니다. 이리저리 굴려봤었지만 실제로 크게 활용하진 못했고 결국 나중에는 경제/테크 관련 Daily report나 일정관리 정도로만 쓰였습니다.

이후 점점 기억에서 잊혀졌다가 이번에 사이드 프로젝트를 진행하면서 다시 생각이 바뀌었습니다. 기능 개발이야 하면 되는거지만 이에 대한 기록을 남기고, 내용을 홍보하고, 더 나아가 홍보전용 아이템들을 제작해야 하는 것들에 시간이 많이 소요가 되었습니다. 이런 반복 작업을 자동화해야겠다고 생각이 들었고, 요즘 많이 쓴다는 Hermes Agent를 알게 되었습니다. Hermes는 도구·스크립트를 직접 실행하며 정해둔 작업(글 정리, 홍보 소재 제작 등)을 자동으로 처리해 주는 AI 에이전트입니다.

맥북에어는 현재 가족이 다시 쓰고 있어 VPS 쪽으로 살펴보았고, Hostinger가 가격이 저렴했기에 선택하게 되었습니다.

이왕이면 이 서버를

문제는 이 둘을 한 서버에 욱여넣되 서로 죽이지 않게 만드는 일이었습니다. 그리고 외부에서 접근하려면 포트를 여는 순간 스캐너의 먹잇감이 되니, 포트는 하나도 열지 않고 안전하게 노출하는 방법이 필요했습니다.

정리하면 목표는 세 가지였습니다.

  1. Hermes 에이전트를 격리해서 돌린다.
  2. 그 옆에 Claude Code로 원격 개발을 붙인다 — 폰에서도.
  3. 둘 다 포트 개방 없이, 그리고 아무나 못 들어오게 노출한다.

큰 그림

먼저 완성된 구조부터 보겠습니다.

                     인터넷

              Cloudflare Zero Trust  ← 여기서 인증 (이메일 OTP)

              Cloudflare Tunnel (아웃바운드 연결, 인바운드 포트 0개)

        ┌──────────────┴──────────────┐
        ▼                             ▼
  sub1.example.com            sub2.example.com
   → 127.0.0.1:4860              → 127.0.0.1:7681
        │                             │
 ┌──────┴───────┐              ┌──────┴────────┐
 │ Hermes 컨테이너 │              │ ttyd 웹터미널   │  ← 호스트 레벨
 │  (격리)        │              │  → Claude Code  │
 └──────────────┘              └───────────────┘

핵심은 두 가지입니다. Hermes만 컨테이너 안에 있고, 관리 도구(웹터미널·Claude Code)는 호스트에 있습니다. 그리고 외부 노출은 전부 Cloudflare 터널을 통해서만 일어나고, 그 앞단을 Zero Trust가 지킵니다.

왜 이렇게 나눴는지 하나씩 풀어보겠습니다.

1. Hermes는 컨테이너 안에 가둔다

Hermes 에이전트는 Docker 컨테이너로 띄웠습니다. 에이전트는 도구를 자유롭게 실행하는 존재라, 호스트와 같은 공간에 두기엔 부담스럽습니다. 컨테이너로 파일시스템·프로세스·네트워크를 격리해두면, 에이전트가 무슨 짓을 하든 그 안에서 끝납니다.

포트도 외부로 직접 열지 않았습니다. 오직 루프백(127.0.0.1)에만 바인딩합니다.

# docker-compose.yml (발췌)
services:
  hermes-agent:
    image: ghcr.io/.../hermes-agent:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:4860:4860" # 외부 X, 로컬에서만 접근
    volumes:
      - ./data:/opt/data # 설정·상태 영속화

127.0.0.1:4860이라 호스트 바깥에서는 이 포트가 존재하지도 않는 것처럼 보입니다. 외부로 내보내는 일은 전부 뒤에서 Cloudflare 터널이 맡습니다.

여기서 127.0.0.1: 접두사가 핵심입니다. Docker는 포트를 4860:4860처럼 IP 없이 매핑하면 0.0.0.0(모든 인터페이스)에 바인딩하면서 UFW 같은 방화벽 규칙을 우회해 버리는 유명한 함정이 있습니다. 그래서 굳이 앞에 127.0.0.1:을 붙여 루프백에만 묶었습니다.

참고로 요즘엔 VPS에서 Docker·AI 에이전트용 템플릿을 기본 제공해, 비슷한 셋업을 손쉽게 올릴 수 있는 옵션이 잘 갖춰져 있습니다.

2. 제일 중요한 결정 — 관리 도구는 컨테이너 “밖”에

처음엔 웹터미널이랑 Claude Code도 Hermes 컨테이너 안에 같이 넣을까 고민했습니다. 한 곳에 모이면 편하니까요. 그런데 한 가지 시나리오에서 막혔습니다.

Hermes가 OOM으로 죽거나 컨테이너가 통째로 뻗으면?

관리 도구까지 같은 컨테이너에 있었다면, 고치러 들어갈 통로마저 같이 죽습니다. 정작 비상 복구가 필요한 순간에 손쓸 방법이 없어지는 거죠. 이건 “관리 대상과 관리 채널을 같이 묶지 마라”라는 오래된 교훈 그대로였습니다.

그래서 원칙을 정했습니다.

이렇게 두니 Hermes가 어떻게 되든 호스트의 웹터미널로 들어가서 docker 명령으로 살리거나 로그를 볼 수 있습니다. 관리 채널이 관리 대상보다 오래 살아남는 구조입니다.

3. 원격 개발 통로 — ttyd 웹터미널 + Claude Code

호스트에 ttyd를 띄웠습니다. ttyd는 터미널을 웹페이지로 그대로 띄워주는 작은 프로그램입니다. 브라우저만 있으면 어디서든 — 노트북이든 폰이든 — 서버 셸에 들어갈 수 있습니다.

systemd 서비스로 등록해서 항상 떠 있게 했고, 역시 루프백에만 바인딩합니다.

# /etc/systemd/system/ttyd-host.service (발췌)
[Service]
ExecStart=/usr/local/bin/ttyd-host-start.sh
Restart=always

ttyd-host-start.sh 스크립트의 핵심은 결국 이 한 줄입니다.

# ttyd는 127.0.0.1:7681 에만 묶는다 (외부 노출은 터널이 담당)
ttyd --interface 127.0.0.1 --port 7681 bash

한 가지 분명히 해둘 점은, ttyd ... bash인증 없이 곧장 서버 셸을 여는 설정이라는 것입니다. 즉 이 엔드포인트는 그 자체로는 무방비이고, 유일한 방어선은 뒤에서 설명할 Zero Trust Access입니다. Access가 뚫리면 곧 셸이 뚫리는 셈이니, 5번 단계는 선택이 아니라 필수입니다.

이 셸 안에서 Claude Code를 띄우면, 브라우저 탭 하나가 곧 원격 개발 환경이 됩니다. 폰에서 sub2.example.com을 열고 Claude Code에게 “이거 고쳐줘”라고 시키는 게 가능해진 거죠. (사실 이 글의 초안도 그렇게 작성했습니다.)

4. 포트를 열지 않고 노출하기 — Cloudflare Tunnel

여기까지 만든 두 서비스는 전부 127.0.0.1에만 있습니다. 외부에서는 접근이 안 됩니다. 보통은 여기서 방화벽 포트를 열고 리버스 프록시를 세우지만 — 그 순간부터 전 세계 봇이 그 포트를 두드립니다.

대신 Cloudflare Tunnel을 썼습니다. 동작 방식이 영리합니다.

설정은 어떤 도메인을 어떤 로컬 포트로 보낼지 매핑하는 게 전부입니다.

# cloudflared config (발췌)
tunnel: <tunnel-id>
ingress:
  - hostname: sub1.example.com
    service: http://127.0.0.1:4860 # Hermes UI
  - hostname: sub2.example.com
    service: http://127.0.0.1:7681 # 웹터미널
  - service: http_status:404

이제 sub1.example.com은 Hermes UI로, sub2.example.com은 웹터미널로 연결됩니다. 서버 포트는 하나도 열지 않은 채로요.

5. 마지막 잠금 — Zero Trust Access

터널을 뚫었으니 이제 누구나 그 주소로 들어올 수 있습니다. 특히 sub2.example.com서버 셸로 직결되는 문이라, 인증 없이 열어두는 건 아주 위험합니다.

그래서 터널 앞단Cloudflare Zero Trust Access를 걸었습니다. 요청이 내 서버에 닿기도 전에, Cloudflare 엣지에서 먼저 신원을 검사합니다.

서버에 별도 인증 로직을 짤 필요가 없습니다. 인증은 Cloudflare 엣지에서 끝나고, 통과한 요청만 내 서버로 내려옵니다. 셸 직결 엔드포인트 앞에 신원 게이트가 하나 더 생긴 셈입니다.

사실 이 블로그도 같은 패턴을 씁니다. /private/* 경로의 비공개 글은 Cloudflare Access로 잠가두고, 인증된 나만 봅니다. 한 번 익혀두면 여기저기 재활용됩니다.

마무리

정리하면 이렇습니다.

덕분에 VPS 하나가 항상 깨어있는 AI 에이전트이자 폰에서도 들어가는 개발 거점이 됐습니다. 그리고 외부에 열린 포트는 끝까지 0개입니다.

덤으로 가족용 Hermes 컨테이너를 하나 더 띄우거나, 구성원별로 프로필을 나눠 세팅하는 방안도 고민 중입니다. 이번에는 조금 더 잘 활용해 볼 수 있기를 바라며..



이전 글
K-Saju 개발기 1: 사주를 K-컬처 캐릭터로 다시 풀어보기
다음 글
로그인 없는 댓글, Cloudflare로 직접 만든 이야기

댓글

댓글 불러오는 중...

댓글 남기기

작성하신 댓글은 관리자 승인 후 게시됩니다.