TrendShift 딥다이브 · 2026-07-03 재분석

Project N.O.M.A.D. 딥다이브
— 인터넷이 끊겨도 살아있는 "오프라인 지식·AI 서버"

Project N.O.M.A.D.(Node for Offline Media, Archives, and Data)는 Wikipedia·Khan Academy·오프라인 지도·로컬 AI 챗봇을 한 대의 PC/서버에 몰아넣고, 인터넷 없이도 계속 작동하게 만드는 셀프호스팅 지식 서버다. 중심에는 AdonisJS로 만든 "Command Center"가 있어, Docker 컨테이너로 뜬 10여 개 앱의 설치·시작·중지·업데이트를 브라우저 하나로 관리한다. 최근 버전은 초기 소개 자료보다 훨씬 커져서, 자체 무중단 자동 업데이트 파이프라인"Supply Depot"이라는 선택형 앱 카탈로그(11종)까지 갖췄다. (저장소: Crosstalk-Solutions/project-nomad · Apache-2.0 · TypeScript 92.5% · ⭐25.9k · 최신 버전 v1.33.0 · 65개 릴리스)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

이 저장소가 대체 무엇인가.

핵심 메시지

"인터넷이 끊겨도 계속 켜져 있는 도서관 + AI 튜터" — Docker Compose 한 번이면 지식·교육·AI가 로컬에 통째로 들어온다.

재난 대비, 오프그리드 생활, 인터넷이 부실하거나 아예 없는 교육 현장, 프라이버시를 지키고 싶은 셀프호스팅 애호가 — 이들의 공통 문제는 "인터넷이 끊기는 순간 검색도 지식도 다 멈춘다"는 것이다. N.O.M.A.D.는 이 문제를 "필요한 지식을 통째로 내 디스크에 내려받아 두고, 그걸 브라우저로 서빙하는 서버"로 해결한다.

핵심 설계는 Command Center라는 관리 웹앱(AdonisJS)이 Docker Engine API를 직접 호출해서 Kiwix(Wikipedia), Kolibri(Khan Academy), ProtoMaps(지도), Ollama+Qdrant(로컬 AI+RAG) 같은 자식 컨테이너들의 설치·기동·정지·업데이트를 대신 처리해 준다는 점이다. 사용자는 Docker 명령어를 몰라도 된다 — 브라우저에서 버튼만 누르면 된다.

README 첫 문장은 이렇다: "Project N.O.M.A.D. is a self-contained, offline-first knowledge and education server." ("완전히 자급자족하는 오프라인 우선 지식·교육 서버.") GitHub 저장소 설명은 한 발 더 나가 "offline survival computer"(오프라인 생존 컴퓨터)라는 표현까지 쓴다 — 재난·정전·오지 상황을 염두에 둔 포지셔닝이다.

이름부터 의도적이다. N.O.M.A.D. = Node for Offline Media, Archives, and Data. "노드(Node)"라는 단어가 암시하듯, 이건 클라우드 서비스가 아니라 사용자가 소유한 물리적 하드웨어 위에서 도는 독립 노드다. 설치 후에는 인터넷 연결 없이도 100% 동작하며, README는 "ZERO built-in telemetry"(내장 텔레메트리 제로)를 명시적으로 못박는다.

용어
Command Center
N.O.M.A.D.의 중심 애플리케이션. AdonisJS(Node.js 풀스택 프레임워크)로 만든 관리 웹앱이자 API 서버다. 사용자가 보는 화면(설정, 앱 설치, 채팅 UI)과, 실제로 Docker 컨테이너를 켜고 끄는 백엔드 로직이 모두 이 하나의 서버 안에 있다. 소스 코드에서는 admin/ 디렉토리가 곧 Command Center다.
이 프로젝트의 트레이드마크
"오프라인 서버인데도 스스로 업데이트한다" — 셀프-업데이트 파이프라인

보통 "오프라인 전용 서버"는 업데이트도 수동이기 마련이다. N.O.M.A.D.는 반대로 GitHub Releases를 주기적으로 확인해 마이너/패치 버전을 스스로 설치하는 자동 업데이트 시스템을 갖췄다(주 버전은 항상 수동 승인). 이걸 위해 sidecar-updater라는 별도의 작은 컨테이너를 두고, 공유 볼륨에 요청/상태/로그 파일을 주고받는 방식으로 관리 서버 자신을 무중단에 가깝게 갱신한다. 뒤에서 이 메커니즘을 자세히 다룬다.

2왜 주목받는가

트렌딩 이유 · 경쟁 제품 대비 강점 · 최근 성장(Supply Depot 등).

기존 방식의 문제
오프라인 지식 서버 = 비싼 전용 하드웨어 + 빈약한 AI

PrepperDisk($199), Doom Box($699) 같은 상용 "생존 컴퓨터" 제품은 라즈베리파이 등 전용 하드웨어에 고정돼 있고, 탑재되는 AI 모델도 7B급 소형 모델에 머문다. 무료 오픈소스 대안은 사실상 없었다.

NOMAD의 해법
아무 PC + GPU 가속 + 완전 무료 + 확장 가능한 앱 생태계

N.O.M.A.D.는 어떤 x86_64/ARM64 PC에도 설치 가능하고, GPU가 있으면 70B급 대형 모델까지 Ollama로 구동한다. Ollama뿐 아니라 OpenAI 호환 API(LM Studio, llama.cpp 등)도 그대로 연결할 수 있어 AI 백엔드를 자유롭게 고를 수 있다. Apache-2.0으로 완전 무료이며, 여기에 더해 "Supply Depot"이라는 선택 설치형 앱 카탈로그(비밀번호 관리자, 미디어 서버, 무전 앱 등)까지 계속 늘고 있다.

경쟁 제품 비교

항목NOMADPrepperDiskDoom Box
비용무료 (오픈소스)$199$699
하드웨어 자유도아무 Debian 계열 PC라즈베리파이전용 기기
AI 백엔드Ollama + OpenAI 호환 API 선택 가능, 최대 70B급7B 기본형7B 기본형
RAG 지원Qdrant 벡터 DB + ZIM(위키피디아) 임베딩없음없음
앱 확장성Supply Depot(11+ 선택형 앱) + 커스텀 Docker고정고정
자동 업데이트시간창·쿨다운·사전점검을 갖춘 자동 마이너 업데이트미공개미공개
프라이버시텔레메트리 제로(명시)미공개미공개
GitHub Stars⭐25.9k비공개비공개
비유

PrepperDisk가 "완제품 라디오"라면, NOMAD는 "내 컴퓨터에 까는 방송국 전체 세트"다. 완제품은 정해진 채널만 나오지만, NOMAD는 라이브러리·교육·지도·AI·비밀번호 관리·미디어 서버까지 필요한 만큼 골라서 얹을 수 있고, 시간이 지나도 스스로 갱신된다.

주목도를 끌어올린 최근 성장 포인트는 다음과 같다.

① Supply Depot — "선택형 앱 카탈로그"로 확장. 초기 버전이 Kiwix·Ollama·Kolibri·ProtoMaps·CyberChef·FlatNotes 6~7종의 핵심 앱으로 시작했다면, 지금은 Stirling-PDF·FileBrowser·Calibre-Web·IT-Tools·Excalidraw·Meshtastic/MeshCore(무전)·HomeBox(재고관리)·Vaultwarden(비밀번호 관리자)·Jellyfin(미디어 서버)까지 총 11개 추가 앱이 "설치 버튼 한 번"으로 붙는다.
② RAG가 지식 라이브러리 전체로 확장. 단순 PDF 업로드형 RAG를 넘어, 설치된 ZIM 라이브러리(Wikipedia 등)를 통째로 벡터 임베딩해 AI 챗봇이 오프라인 위키피디아 지식으로 답하게 만드는 ZIMExtractionService가 추가됐다.
③ 스스로 진화하는 인프라. 자동 업데이트(코어 앱 + Supply Depot 앱 + 콘텐츠 각각 별도 파이프라인), 디스크 사용량 수집 사이드카, 컨테이너 로그 뷰어(Dozzle) 통합까지 — "설치 후 방치해도 되는" 운영 편의성이 크게 강화됐다.
④ 결정론적 테스트 가능한 자동화. 자동 업데이트 로직을 --dry-run으로 실제 업데이트 없이 시뮬레이션할 수 있어, "언제·왜 업데이트했는지/안 했는지"를 코드로 검증 가능하게 설계했다.

3기술 스택 전체 지도

AdonisJS 풀스택 백엔드 + React/Inertia 프론트엔드 + Docker 오케스트레이션 인프라.

언어 구성은 TypeScript 92.5%, Shell 6.6%, 나머지는 Dockerfile류다. 즉 프로덕트 로직은 거의 전부 TypeScript 한 언어로 통일돼 있고(백엔드도 프론트엔드도), 셸 스크립트는 설치·업데이트·사이드카 같은 인프라 자동화에 집중돼 있다.

① 백엔드 — Command Center (AdonisJS 6)

기술버전역할
AdonisJS6.19풀스택 MVC 프레임워크. Node.js 22 + TypeScript(ESM, #imports 서브패스 별칭)
Lucid ORM21.8AdonisJS 내장 DB ORM. 모델은 admin/app/models/
MySQL 8.0 + better-sqlite3-운영 DB는 MySQL(Docker Compose로 별도 컨테이너), 로컬 개발은 SQLite도 지원
Redis + BullMQ5.77비동기 작업 큐. 다운로드·모델 다운로드·벤치마크 큐를 분리 운영
Inertia.js2.3(서버) / 3.1(어댑터)REST API 없이 React 컴포넌트에 props를 서버가 직접 주입하는 SSR↔SPA 브릿지
Dockerode4.0.9Node.js → Docker Engine API. 컨테이너 라이프사이클 전체를 이걸로 제어
@adonisjs/transmit2.0SSE 기반 실시간 브로드캐스트(다운로드 진행률 등을 프론트에 실시간 push)
@vinejs/vine3.0요청 유효성 검사(validator)

② AI · RAG · 콘텐츠 처리 파이프라인

기술역할
ollama (0.6) + openai (6.38) SDK로컬 LLM(Ollama) 또는 OpenAI 호환 API(LM Studio 등) 이중 백엔드 지원
@qdrant/js-client-rest벡터 DB 클라이언트. RAG의 시맨틱 검색 담당
@chonkiejs/core (TokenChunker)토큰 기준 텍스트 청킹 — 임베딩 모델의 컨텍스트 한도(2K)에 맞춰 문서를 쪼갬
pdf-parse · pdf2pic · tesseract.jsPDF 텍스트 추출 → 이미지 변환 → OCR(스캔본 대응)
@openzim/libzimZIM(오프라인 위키피디아 등) 아카이브를 직접 읽어 콘텐츠를 벡터 임베딩용으로 추출
cheerioZIM/HTML 콘텐츠에서 본문 텍스트만 파싱 추출
stopword불용어 제거(검색 품질 개선)
sharp이미지 리사이즈·최적화(썸네일 등)

③ 프론트엔드

기술버전역할
React19.2UI 렌더링. Inertia 어댑터로 서버와 연결
Tailwind CSS4.2(Vite 플러그인)유틸리티 CSS. Tailwind v4의 신규 Vite 통합 방식 사용
@tanstack/react-query5.90서버 상태 캐싱·재검증
@tanstack/react-virtual3.13대량 리스트(ZIM 파일 목록 등) 가상 스크롤
fuse.js7.1클라이언트 퍼지(fuzzy) 검색
maplibre-gl + react-map-gl + pmtiles-오프라인 지도 렌더링(PMTiles 단일파일 포맷 서빙)
@markdoc/markdoc · react-markdown · remark-gfm-문서(docs)·채팅 답변의 마크다운 렌더링
@uppy/core · @uppy/dashboard · @uppy/xhr-upload-대용량 파일(PDF, 모델 등) 업로드 UI
@headlessui/react · @tabler/icons-react-접근성 갖춘 UI 프리미티브, 아이콘 세트

④ 인프라 · 컨테이너 오케스트레이션

구성 요소이미지/도구역할
admin (Command Center)ghcr.io/crosstalk-solutions/project-nomad메인 애플리케이션. 이 저장소를 빌드한 이미지
updaterproject-nomad-sidecar-updater공유 볼륨으로 요청을 받아 docker compose pull/up을 대신 실행하는 자기-업데이트 사이드카
disk-collectorproject-nomad-disk-collector호스트 디스크를 읽기 전용(ro,rslave)으로 마운트해 용량 정보만 admin에 전달하는 최소 권한 사이드카
dozzleamir20/dozzle:v10.0컨테이너 로그 웹 뷰어(선택 설치, 액션·셸 기능은 보안상 비활성화)
mysql / redis공식 이미지영속 DB / 큐·캐시
go-pmtiles CLI바이너리 다운로드(SHA256 핀 고정)지도 타일 추출용 CLI를 이미지 빌드 시 아키텍처별로 검증 후 설치
설계 통찰
"관리 앱과 실제 지식 서비스는 완전히 분리된 컨테이너"

Command Center 자신은 지식 콘텐츠를 직접 서빙하지 않는다. Wikipedia는 Kiwix 컨테이너가, 지도는 ProtoMaps 컨테이너가, LLM 추론은 Ollama 컨테이너가 각각 담당하고, Command Center는 오직 "오케스트레이터" 역할만 한다. 이 분리 덕분에 각 서비스를 독립적으로 켜고 끄고 업데이트할 수 있고, Command Center 자체가 죽어도 이미 떠 있는 Kiwix·Ollama는 계속 서빙을 이어갈 수 있다.

4아키텍처 심화 분석

Docker-of-Docker 오케스트레이션 + 파일 기반 사이드카 통신 + RAG 파이프라인.

전체 시스템 구조도

┌───────────────────────────────────────────────────────────────┐ │ 사용자 브라우저 (:8080) │ └───────────────────────────────┬─────────────────────────────────┘ │ HTTP / SSE(Transmit) ┌───────────────────────────────▼─────────────────────────────────┐ │ nomad_admin (Command Center, AdonisJS) │ │ ┌────────────┐ ┌───────────┐ ┌────────────┐ ┌─────────────┐ │ │ │ Controllers │ │ Services │ │ BullMQ Jobs │ │ Lucid Models │ │ │ │(easy_setup, │ │(docker_, │ │(download, │ │(Service, │ │ │ │ supply_depot│ │ rag_, │ │ embed_file, │ │ KVStore, ...)│ │ │ │ chats, ...) │ │ auto_upd.)│ │ benchmark) │ │ │ │ │ └──────┬─────┘ └─────┬─────┘ └──────┬─────┘ └───────┬──────┘ │ │ └──────────────┴───────┬───────┴────────────────┘ │ │ Dockerode (Docker Engine API) │ └───────────────────────────────┬───────────────────────────────────┘ │ /var/run/docker.sock ┌────────────┬──────────────┼──────────────┬────────────────┐ │ │ │ │ │ ┌───▼───┐ ┌────▼────┐ ┌─────▼─────┐ ┌─────▼──────┐ ┌──────▼──────┐ │ Ollama │ │ Kiwix │ │ Kolibri │ │ ProtoMaps │ │ Supply Depot │ │+Qdrant │ │(Wikipedia│ │(Khan │ │ (지도) │ │ (Vaultwarden,│ │ (AI+RAG│ │ 등 ZIM) │ │ Academy) │ │ │ │ Jellyfin, │ │ │ │ │ │ │ │ │ │ HomeBox...) │ └────────┘ └──────────┘ └───────────┘ └────────────┘ └──────────────┘ ── 자기-업데이트 서브시스템 (공유 볼륨으로 파일 기반 통신) ── ┌─────────────┐ update-request ┌───────────────────┐ │ nomad_admin │ ───────────────────▶│ nomad_updater │ │ (요청 생성) │ │ (sidecar-updater) │ │ │◀─────────────────── │ compose.yml sed 치환 │ └─────────────┘ update-status/log │ → pull → up -d │ (폴링으로 읽음) └───────────────────┘
용어
Docker-out-of-Docker (사이드카 오케스트레이션)
컨테이너 안에서 호스트의 Docker 소켓(/var/run/docker.sock)을 그대로 마운트해, 컨테이너 내부 프로세스가 호스트의 Docker 데몬에 직접 명령을 내리는 패턴. NOMAD의 admin 컨테이너, updater 컨테이너, disk-collector 컨테이너가 모두 이 방식으로 "자기 자신이 속한 호스트"를 제어한다. 강력하지만 사실상 호스트 root 권한과 동급이라, disk-collector처럼 필요한 컨테이너는 읽기 전용 마운트로 권한을 최소화하는 절제가 중요하다.

핵심 설계 패턴 1 — 파일 기반 사이드카 통신 (자기 자신을 업데이트하기)

"실행 중인 컨테이너가 자기 자신을 업데이트"하는 건 역설적이다 — 이미지를 갈아 끼우는 도중 그 프로세스 자신이 죽을 수 있기 때문이다. NOMAD의 해법은 별도의 작은 sidecar-updater 컨테이너를 옆에 세워 두고, admin과 sidecar가 공유 볼륨(nomad-update-shared)의 파일들로만 대화하게 만드는 것이다.

admin 컨테이너 updater 컨테이너 (sidecar-updater) ───────────────── ───────────────────────────────── 1. /shared/update-request 파일 생성 ──▶ 2. 파일 존재를 폴링으로 감지 { "target_tag": "1.33.0" } ↓ 3. compose.yml의 이미지 태그를 sed로 치환 ↓ 4. docker compose pull ↓ 5. /shared/update-status 폴링해서 ◀── write_status("pulling", 20, ...) 진행률을 UI에 실시간 표시 ↓ 6. docker compose up -d (admin 컨테이너 재생성됨) ↓ 7. write_status("complete", 100)

이 구조의 묘미는 admin이 죽어도 updater는 살아 있다는 것이다. docker compose up -d로 admin 컨테이너가 새 이미지로 재생성되는 순간에도, 그 작업을 수행하는 프로세스(sidecar)는 admin과 별개이므로 중단되지 않는다. 상태는 DB가 아니라 단순 텍스트/JSON 파일(update-status, update-log)로 주고받는데, 이는 admin의 DB 연결이 재시작 중 끊겨도 상태 추적이 흔들리지 않게 하는 실용적 선택이다.

핵심 설계 패턴 2 — 3단계 자동 업데이트 파이프라인과 안전장치

auto_update_service.ts는 코드 몇 줄이 아니라 완결된 의사결정 파이프라인이다. GitHub Releases API를 15분 캐시로 조회하고, 정규식 ^\d+\.\d+\.\d+$엄격한 semver 태그만 후보로 인정한다(형식이 이상한 값이 셸 스크립트의 sed 치환에 그대로 흘러들어가면 위험하기 때문 — 방어적 설계).

안전장치동작
업데이트 시간창기본 02:00~05:00(서버 로컬 시각)에만 자동 적용, 사용자가 커스터마이즈 가능
쿨다운(Cool-off)기본 72시간 — 막 나온 릴리스에 바로 뛰어들지 않고 안정화를 기다림
메이저 버전 제외메이저 버전 업은 항상 수동 승인만 허용(parseMajorVersion으로 감지)
사전점검(Pre-flight)디스크 여유 공간·진행 중인 다운로드/설치 유무를 확인 후 통과해야 실행
연속 실패 서킷브레이커3회 연속 실패 시 자동 업데이트를 스스로 비활성화(업데이트 루프 방지)
결정론적 dry-run현재 버전·릴리스 목록·시각을 모두 주입해 "실제로 뭐가 트리거됐을지"를 네트워크 없이 재현

같은 패턴이 세 갈래로 병렬 존재한다 — auto_update_service.ts(코어 admin 이미지), app_auto_update_service.ts(Supply Depot에서 설치한 개별 앱), content_auto_update_service.ts(ZIM·지도 같은 콘텐츠 파일). 각기 업데이트 대상은 다르지만, 시간창·쿨다운·사전점검·dry-run이라는 뼈대는 공유한다 — "같은 안전장치를 여러 도메인에 재사용"하는 좋은 예시다.

핵심 설계 패턴 3 — 스토리지 마운트 자동 추적

DockerService에는 _hostStorageRoot라는 흥미로운 필드가 있다. admin 컨테이너 자신의 /app/storage 마운트가 호스트의 어느 경로에 실제로 매핑돼 있는지를 컨테이너 안에서 역으로 알아낸 뒤, Kiwix·Ollama 같은 자식 서비스의 볼륨도 같은 호스트 경로 아래로 맞춰 생성한다. 이렇게 하면 사용자가 docker-compose.yml에서 저장 위치를 바꿨을 때, 자식 컨테이너들도 자동으로 새 위치를 따라간다 — 설정을 한 곳에서만 바꾸면 되는 "단일 진실 공급원(source of truth)" 원칙이다.

핵심 설계 패턴 4 — RAG: 문서뿐 아니라 "이미 설치된 지식"까지 임베딩

[경로 A] 사용자 업로드 문서 PDF/이미지 업로드 ↓ pdf-parse / pdf2pic + tesseract.js(OCR) 텍스트 추출 ↓ TokenChunker (@chonkiejs/core) — 목표 1500 토큰/청크 텍스트 청크 ↓ Ollama 임베딩 모델 (nomic-embed-text, 768차원) 벡터 ↓ Qdrant upsert (컬렉션: nomad_knowledge_base) 벡터 DB 저장 [경로 B] 이미 설치된 ZIM 라이브러리 (예: 오프라인 Wikipedia) ZIM 파일 (@openzim/libzim로 오픈) ↓ 아티클 순회 + cheerio로 본문만 추출 ↓ 배치 단위(ZIM_BATCH_SIZE)로 나눠 임베딩 처리 ↓ 동일하게 TokenChunker → Ollama 임베딩 → Qdrant upsert [질의 시점 — 두 경로 공통] 사용자 질문 → "search_query: " 프리픽스 → Ollama 임베딩 ↓ Qdrant 코사인 유사도 검색(top-k) + 리랭킹 관련 청크 → LLM 프롬프트에 주입 → Ollama/OpenAI 호환 API 생성 → 답변

두 경로가 같은 컬렉션·같은 임베딩 파이프라인을 공유한다는 점이 핵심이다. 즉 "내가 올린 PDF"와 "설치해 둔 오프라인 위키피디아"가 하나의 지식베이스로 합쳐져, AI 챗봇이 둘을 넘나들며 답을 만든다. 임베딩 프리픽스(search_document: / search_query:)를 문서용과 질의용으로 구분하는 것은 Nomic Embed Text 계열 모델의 권장 사용법을 그대로 반영한 디테일이다.

핵심 설계 패턴 5 — 상태 캐시 + 인플라이트 중복 제거

DockerService의 서비스 상태 조회(_servicesStatusCache, _servicesStatusInflight)를 보면, 같은 순간 여러 요청이 동시에 들어와도 Docker API 호출은 단 한 번만 나가도록 프라미스를 캐싱한다. UI가 여러 컴포넌트에서 동시에 "서비스 상태 다 보여줘"를 호출해도 Docker 데몬에 중복 부하를 주지 않는, 실전에서 자주 놓치는 최적화다.

5디렉토리 구조 해부

"admin/이 사실상 저장소 전체" — AdonisJS 표준 구조 위에 도메인별 서비스·잡 계층이 얹힌 형태.

project-nomad/ ├── admin/ ← 핵심 애플리케이션(AdonisJS). 사실상 저장소의 심장 │ ├── app/ │ │ ├── controllers/ ← HTTP 요청 처리(14개): easy_setup, supply_depot, │ │ │ chats, rag, ollama, benchmark, maps, zim... │ │ ├── services/ ← 비즈니스 로직(22개, 최대 계층) │ │ │ ├── docker_service.ts ← Dockerode 래핑, 컨테이너 라이프사이클 전체 │ │ │ ├── rag_service.ts ← 임베딩·청킹·Qdrant 검색 │ │ │ ├── zim_extraction_service.ts← ZIM 라이브러리 → 벡터 임베딩 │ │ │ ├── auto_update_service.ts ← 코어 이미지 자동 업데이트 의사결정 │ │ │ ├── app_auto_update_service.ts ← Supply Depot 앱 업데이트 │ │ │ ├── content_auto_update_service.ts ← ZIM/지도 콘텐츠 업데이트 │ │ │ ├── container_registry_service.ts ← 이미지 태그/레지스트리 조회 │ │ │ └── benchmark_service.ts ← 하드웨어 벤치마크 + 리더보드 제출 │ │ ├── jobs/ ← BullMQ 백그라운드 작업(9개) │ │ │ ├── run_download_job.ts / download_model_job.ts │ │ │ ├── embed_file_job.ts ← RAG 임베딩을 큐로 비동기 처리 │ │ │ ├── run_benchmark_job.ts / run_extract_pmtiles_job.ts │ │ │ └── auto_update_job.ts / app_auto_update_job.ts / content_auto_update_job.ts │ │ ├── models/ ← Lucid ORM 모델(12개): Service, KVStore, │ │ │ ChatSession, BenchmarkResult, KbIngestState... │ │ ├── middleware/ · validators/ · exceptions/ · utils/ │ ├── commands/ ← Ace CLI 커맨드 — dry-run 시뮬레이터 3종 + 벤치마크 CLI │ │ ├── auto_update/dry_run.ts · app_auto_update/dry_run.ts │ │ ├── content_auto_update/dry_run.ts │ │ └── benchmark/{run,submit,results}.ts · queue/work.ts │ ├── inertia/ ← React 프론트엔드(Inertia.js로 서버와 연결) │ │ ├── pages/ ← easy-setup, settings(+zim 서브), docs, errors │ │ ├── components/ · hooks/ · context/ · providers/ · layouts/ │ ├── database/migrations/ ← Lucid 마이그레이션(스키마 변경 이력) │ ├── tests/unit/ ← 16개 스펙 — 자동 업데이트/RAG 의사결정 로직 집중 검증 │ ├── constants/service_names.ts ← 모든 Docker 컨테이너 이름의 단일 진실 공급원 │ ├── config/ · start/ · providers/ ← AdonisJS 표준 설정·부트스트랩 │ └── adonisrc.ts ← AdonisJS 루트 설정 │ ├── install/ ← 설치/운영 스크립트 + 사이드카 이미지 소스 │ ├── install_nomad.sh ← 원커맨드 설치기(신규 호스트 부트스트랩) │ ├── management_compose.yaml ← 배포용 Docker Compose 템플릿(admin+updater+ │ │ disk-collector+dozzle+mysql+redis) │ ├── start_nomad.sh / stop_nomad.sh / update_nomad.sh / uninstall_nomad.sh │ ├── sidecar-updater/ ← 자기-업데이트 사이드카(별도 Dockerfile+셸 스크립트) │ ├── sidecar-disk-collector/ ← 디스크 사용량 수집 사이드카(읽기 전용) │ └── wikipedia_en_100_mini_*.zim ← 최초 부팅용 미니 위키피디아 시드 데이터 │ ├── collections/ ← 콘텐츠 카탈로그 메타데이터(JSON) │ ├── kiwix-categories.json / wikipedia.json / maps.json │ ├── Dockerfile ← 멀티스테이지 빌드(deps→production-deps→build→prod) ├── .releaserc.json ← semantic-release 설정(커밋 메시지 기반 자동 버전업) └── .github/workflows/ ← release.yml · build-primary-image.yml · build-sidecar-updater.yml · build-disk-collector.yml

세 덩어리로 나눠 읽으면 명료하다.

① 압도적 비중의 admin/. 저장소 전체 TypeScript/TSX 파일이 331개, admin/app/만 약 2만 1천 줄이다. 서비스 계층(services/)이 22개로 가장 크고, 그 아래 잡(jobs/)·모델(models/)·컨트롤러(controllers/)가 각각 명확히 역할을 나눈다 — Controller는 요청만 받고, 실제 로직은 Service에, 무거운 작업은 Job(큐)에 위임하는 전형적 계층 분리다.

② 독립적으로 빌드되는 install/sidecar-*. 이 두 폴더는 각각 자기 자신의 Dockerfile을 갖고 별도 GitHub Actions 워크플로(build-sidecar-updater.yml, build-disk-collector.yml)로 독립 이미지로 빌드된다. admin 이미지와 릴리스 주기가 분리돼 있어, 사이드카만 고쳐도 전체 admin을 재빌드할 필요가 없다.

③ 데이터가 코드로 관리되는 collections/. 다운로드 가능한 Wikipedia 언어판·카테고리·지도 목록이 JSON 파일로 저장소에 커밋돼 있다. .github/workflows/validate-collection-urls.yml이 이 URL들의 생존 여부를 CI에서 주기 검증하는 것으로 보아, "콘텐츠 카탈로그도 코드처럼 리뷰·검증한다"는 원칙이 엿보인다.

6학습 포인트 (기술별)

이 저장소에서 "무엇을 배울 수 있는가" — 개념 → 코드 위치 → 실습 아이디어.

학습 포인트 A

Dockerode로 "Docker를 코드로 다루기"

Node.js 앱이 Docker 컨테이너의 생성·시작·정지·삭제·이미지 풀·로그 조회까지 전부 프로그래밍적으로 제어하는 법을 실전 코드로 배운다. admin/app/services/docker_service.ts 하나가 수십 개 메서드로 이걸 다 담당한다. 특히 pullImage()followProgress의 에러 콜백을 놓쳐 실패한 풀을 성공으로 오인했던 버그(코드 주석에 이슈 번호 #790으로 기록)를 고친 흔적은, "콜백 기반 API에서 실패를 놓치지 않는 법"의 살아있는 사례다.

실습: dockerode를 설치해 docker.listContainers({all:true})로 로컬에 떠 있는 컨테이너 목록을 뽑고, 상태별로 그룹핑해 출력하는 CLI를 만들어 본다.

학습 포인트 B

"자기 자신을 업데이트하는 서버" 설계하기

실행 중인 프로세스가 자기 자신의 컨테이너를 갈아 끼우는 역설을 사이드카 분리 + 파일 기반 통신으로 푸는 패턴. install/sidecar-updater/update-watcher.shadmin/app/services/auto_update_service.ts를 나란히 읽으면 "요청 → 폴링 감지 → 실행 → 상태 파일 기록 → 폴링 확인"의 전체 사이클이 보인다.

실습: 공유 디렉토리에 JSON 파일을 쓰고 다른 프로세스가 fs.watch 또는 폴링으로 감지해 응답 파일을 쓰는 미니 IPC를 직접 구현해 본다.

학습 포인트 C

결정론적으로 테스트 가능한 자동화 설계

"시간에 따라 다르게 동작하는 로직"(업데이트 시간창, 쿨다운)을 어떻게 테스트하는가? NOMAD는 현재 시각·버전·릴리스 목록을 전부 함수 인자로 주입 가능하게 만들어(EvaluateOverrides 인터페이스), 실제 시계나 네트워크 없이도 "이 입력이면 이 결정"을 검증한다. node ace auto-update:dry-run --scenarios 명령이 이 사고방식의 실물이다.

실습: "지금 시각이 업무시간 내인지" 판정하는 함수를 만들되, Date.now()를 직접 부르지 않고 now 파라미터로 주입받게 리팩터링해 유닛 테스트를 짜 본다.

학습 포인트 D

RAG 파이프라인 — 청킹부터 리랭킹까지

rag_service.ts는 토큰 예산 계산(문자당 토큰 비율 추정치까지 문서화), 임베딩 프리픽스 구분, 배치 크기 조절 같은 "실전에서만 배우는" RAG 디테일이 상수 하나하나에 근거와 함께 주석으로 남아 있다. 여기에 더해 PDF뿐 아니라 ZIM 아카이브(오프라인 위키피디아)까지 같은 파이프라인으로 임베딩하는 zim_extraction_service.ts를 함께 보면, "이미 있는 대용량 콘텐츠를 RAG에 편입시키는 법"까지 배울 수 있다.

실습: Ollama + Qdrant를 로컬에 띄우고, 짧은 텍스트 파일을 @chonkiejs/coreTokenChunker로 청크 낸 뒤 임베딩해 저장, 질의 시 유사도 검색까지 미니 RAG를 직접 구현해 본다.

학습 포인트 E

Inertia.js — REST API 없는 서버-프론트 통합

AdonisJS 컨트롤러가 return inertia.render('settings/index', {...})처럼 React 페이지 컴포넌트에 props를 직접 서버에서 주입한다. 별도 REST/GraphQL API 계층을 만들지 않고도 SPA의 부드러운 UX를 얻는 방식으로, Laravel 생태계에서 유명한 패턴을 AdonisJS+React 조합에 적용한 사례다.

실습: AdonisJS 스타터에 Inertia를 붙이고, 컨트롤러에서 DB 조회 결과를 React 컴포넌트 props로 바로 넘기는 간단한 목록 페이지를 만들어 본다.

학습 포인트 F

최소 권한 사이드카 설계

disk-collector 컨테이너는 호스트 전체를 ro,rslave(읽기 전용 + 서브마운트 전파)로만 마운트한다. "필요한 것만 최소 권한으로" 원칙을 management_compose.yaml의 볼륨 설정 한 줄과 주석에서 직접 확인할 수 있다. 반대로 Dozzle은 기본적으로 컨테이너 정지·셸 접근 기능이 있는데, NOMAD는 이를 명시적으로 비활성화(DOZZLE_ENABLE_ACTIONS=false, DOZZLE_ENABLE_SHELL=false)해 둔다 — "Docker 소켓 접근 + 셸 = 권한 상승"이라는 위험을 주석으로 직접 경고한다.

실습: 같은 Docker 소켓을 마운트하는 두 개의 컨테이너를 만들어, 하나는 읽기 전용/제한된 API만, 다른 하나는 전체 권한을 갖게 구성해 차이를 비교해 본다.

7하드웨어 / 시스템 요구사항

관리 앱 자체는 가볍지만, "어떤 앱을 얼마나 얹느냐"가 실제 요구사항을 결정한다.

티어예산 목안권장 사양가능한 것
최소(관리 앱만)-2GHz 듀얼코어, RAM 4GB, 여유 디스크 5GB, Debian 계열 OSCommand Center 구동. AI/대용량 콘텐츠 제외
입문~$150구형 미니PC 수준정보 라이브러리(Kiwix)·노트·데이터 도구 위주, AI 없음
중급~$500Ryzen 5/i5, 16GB RAM, GTX 1660급소형 LLM(7B급) 구동 가능
풀스펙~$1,000+Ryzen 7/i7, RAM 32GB, RTX 3060 이상, SSD 250GB+중대형 모델(13B~34B). 70B는 RTX 4090(24GB) 이상 권장

핵심 공식: GPU VRAM이 곧 돌릴 수 있는 AI 모델 크기를 결정한다. RTX 3060(12GB)이면 13B급 모델, RTX 4090(24GB)이면 70B급까지 시야에 들어온다. AI를 아예 안 쓰거나 OpenAI 호환 API로 외부 서버에 추론을 위임하면 이 요구사항 자체가 사라진다.

항목세부
필수 OSDebian 계열(Ubuntu 권장). install_nomad.sh는 이 계열 전용
Windows공식 미지원이나 WSL2 경유 커뮤니티 가이드 존재(네이티브 Docker/Docker Desktop 두 경로)
네트워크최초 설치 시에만 필수. 이후 콘텐츠 추가 다운로드 때만 필요, 평상시는 완전 오프라인 동작
권한설치 스크립트는 sudo/root 필요(Docker 소켓 접근·시스템 서비스 등록 때문)
스토리지ZIM 파일(영어 위키피디아 전체는 수십GB)·모델(7B 기준 수GB~수십GB)이 용량을 좌우
현실적 주의
인증이 기본으로 없다 — 네트워크 노출에 각별한 주의

README가 명확히 밝힌다: "By design, Project N.O.M.A.D. is intended to be open and available without hurdles — it includes no authentication."로그인 화면이 없다. LAN 안에서 다른 기기가 접근하게 열어 둔다면 방화벽/포트 제어로 스스로 접근을 통제해야 하고, 인터넷에 직접 노출하는 것은 강하게 비권장된다. 다중 사용자 권한 분리는 공개 로드맵에만 있는 미구현 기능이다.

8직접 해볼 수 있는 실습 과제

난이도별로 손을 움직여 보는 5가지.

입문 ★☆☆☆☆

과제 1 — 로컬/가상머신에 실제 설치해 탐색

Ubuntu(실물 또는 VM)에서 원커맨드로 설치하고, 각 서비스를 직접 눌러 본다.

sudo apt-get update && sudo apt-get install -y curl
curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/install_nomad.sh \
  -o install_nomad.sh && sudo bash install_nomad.sh
# http://localhost:8080 접속 → Easy Setup 위저드부터 진행

설치 후 sudo bash /opt/project-nomad/start_nomad.sh / stop_nomad.sh로 전체 스택을 내리고 올려 보며 무엇이 살아남는지 관찰한다.

과제 2 — dry-run으로 자동 업데이트 의사결정 관찰

실제 업데이트 없이 "언제, 왜" 업데이트가 트리거되는지 시뮬레이션한다.

# admin/ 디렉토리에서 실행
node ace auto-update:dry-run --scenarios
node ace auto-update:dry-run --current=1.32.0 --force-enabled

출력되는 WOULD UPDATE / WOULD NOT UPDATE(사유) 판정을 보고, 시간창·쿨다운 값을 바꿔가며 결과가 어떻게 달라지는지 확인한다.

중급 ★★★☆☆

과제 3 — Dockerode로 미니 컨테이너 관리기 만들기

Node.js + Dockerode로 컨테이너 목록 조회·시작·중지 API를 직접 구현한다. admin/app/services/docker_service.tsaffectContainer() 메서드를 참고 삼아, 최소 기능만 뽑아서 재구현해 본다.

import Docker from 'dockerode'
const docker = new Docker()
const containers = await docker.listContainers({ all: true })
console.log(containers.map(c => ({ name: c.Names, status: c.Status })))

과제 4 — 미니 RAG 시스템 구축

Ollama + Qdrant를 로컬 설치하고, 텍스트 파일 파싱 → 토큰 청킹 → 임베딩 → 벡터 저장 파이프라인을 직접 구현한다. rag_service.ts의 토큰 예산 계산 로직(TARGET_TOKENS_PER_CHUNK 등 상수)을 참고해 청크 크기를 조절해 보고, 질의응답까지 완성하면 끝이다.

고급 ★★★★☆

과제 5 — 사이드카 패턴으로 미니 자기-업데이트 시스템 구현

install/sidecar-updater/update-watcher.sh를 참고해, 두 개의 컨테이너(메인 앱 + 감시자)를 공유 볼륨으로 연결한다. 메인 앱이 요청 파일을 쓰면 감시자가 이를 감지해 docker compose pull && docker compose up -d를 수행하고, 진행 상태를 상태 파일에 기록해 메인 앱이 폴링으로 읽게 만든다. 완성되면 "실행 중인 컨테이너가 스스로를 무중단에 가깝게 갱신하는" 전체 사이클을 손으로 경험하게 된다.

9관련 기술 심화 학습 로드맵

6주 코스 — AdonisJS 기초부터 자체 사이드카 오케스트레이션까지.

주차주제구체적 목표
1주차AdonisJS 6 & Lucid ORM 기초MVC 구조, 라우팅(start/routes.ts), Lucid 모델·마이그레이션 작성. admin/app/models/의 실제 모델 정독
2주차Dockerode & 컨테이너 오케스트레이션docker_service.ts 정독 → 직접 컨테이너 생성/네트워크/볼륨 마운트 API 실습. 이미지 풀 실패 처리(#790 사례) 이해
3주차Inertia.js + React 19SSR↔SPA 하이브리드 패턴, 서버 props 주입, TanStack Query와의 역할 분담(서버 상태 vs 클라이언트 상태)
4주차RAG 파이프라인 심화청킹 전략(토큰 예산), 임베딩 프리픽스, Qdrant 컬렉션 설계, 리랭킹까지 rag_service.ts 기준으로 재구현
5주차BullMQ & 비동기 아키텍처큐 분리 전략(downloads/model-downloads/benchmarks), Job 진행률을 SSE(Transmit)로 실시간 전달하는 흐름 추적
6주차자기-업데이트 & 사이드카 설계sidecar-updater/disk-collector 두 사이드카의 Dockerfile·셸 스크립트를 읽고, dry-run 테스트 가능한 자동화 설계 원칙을 내 프로젝트에 적용
곁들이면 좋은 배경 지식
Kiwix(ZIM) · Kolibri · PMTiles 생태계

NOMAD는 자체 개발보다 기존 오프라인 콘텐츠 생태계를 오케스트레이션하는 데 강점이 있다. Kiwix의 ZIM 포맷, Learning Equality의 Kolibri, Protomaps의 PMTiles를 각각 따로 알아 두면 "왜 이 조합을 골랐는지"가 훨씬 잘 보인다. 특히 "대용량 정적 콘텐츠를 단일 파일 포맷으로 오프라인 서빙한다"는 아이디어는 ZIM과 PMTiles 둘 다에 공통으로 깔려 있다.

10핵심 키워드 사전

이 프로젝트를 읽을 때 반복 등장하는 용어 정리.

키워드
Command CenterN.O.M.A.D.의 중심 관리 웹앱(AdonisJS). 자식 컨테이너들을 오케스트레이션
Supply DepotStirling-PDF·Vaultwarden·Jellyfin 등 선택 설치형 앱 카탈로그(11종+)
DockerodeNode.js에서 Docker Engine API를 호출하는 라이브러리. 오케스트레이션의 실행 도구
Sidecar메인 컨테이너 옆에 붙어 특정 기능(업데이트·디스크 수집)만 전담하는 보조 컨테이너
공유 볼륨(shared volume)admin과 updater 사이드카가 요청/상태/로그 파일을 주고받는 통신 매체
Kiwix / ZIM오프라인 웹 콘텐츠 아카이브 시스템과 그 저장 포맷. Wikipedia 등을 통째로 담음
KolibriLearning Equality의 오프라인 교육 플랫폼(Khan Academy 콘텐츠 등)
ProtoMaps / PMTiles지도 타일을 단일 파일로 서빙하는 오프라인 지도 솔루션/포맷
Ollama로컬 LLM 실행 도구. Docker와 유사한 CLI로 모델을 pull/run
OpenAI 호환 APILM Studio·llama.cpp 등 OpenAI SDK 형식을 흉내 낸 로컬/원격 추론 서버 규격
QdrantRust로 만든 벡터 데이터베이스. RAG의 시맨틱 검색 담당
RAGRetrieval-Augmented Generation — 외부 문서를 검색해 LLM 답변 품질을 높이는 기법
TokenChunker@chonkiejs/core의 토큰 기준 텍스트 분할기. 임베딩 컨텍스트 한도에 맞춰 청크 생성
Inertia.js서버사이드 라우팅(AdonisJS)과 클라이언트 렌더링(React)을 REST 없이 잇는 어댑터
BullMQRedis 기반 Node.js 작업 큐. 다운로드·임베딩·벤치마크를 비동기 처리
쿨다운(Cool-off) / 시간창(Window)자동 업데이트가 "언제" 적용되는지를 제한하는 안전장치 두 축
dry-run실제 부작용 없이 의사결정 로직만 시뮬레이션하는 테스트 실행 모드
Docker-out-of-Docker컨테이너 안에서 호스트의 Docker 소켓을 마운트해 호스트 Docker를 직접 제어하는 패턴

11참고 링크

원본을 직접 확인하고 싶을 때.