사진 보관창고였던 NAS에 Immich 올리기 — 무거운 일은 맥미니에게, 클릭은 Aside에게

처음에 선을 하나 긋고 시작하자. 이 글은 2026년 10월 27일 오후에 실제로 진행한 설치 기록이고, 기준 시각(17:30 KST)에서 색인 큐는 아직 처리 중이다. 마지막 완료 수치와 초기 검색 체감은 확정이 아니라 “예정된 자동 재확인” 항목으로 남긴다. 취미는 끝이 없어도 글은 일단 마무리해야 하니까…

1. 시작은 “집에 있는 NAS를 더 잘 써보자”였다

집 NAS는 DS725+. Ryzen R1600 2코어에 RAM 4GB, 볼륨은 SHR 2×4.5TB HDD 하나(volume3, ext4). 용도의 90%는 사진 백업 보관창고였고, 나머지는 SearXNG나 MCP 서버 같은 남는 김에 돌리는 컨테이너들이었다. 타임머신도 이미 붙어 있고.

“사진을 더 잘 써보자”는 이야기를 꺼냈을 때 첫 후보는 당연히 Immich이었다. 그런데 Immich 권장 사양 표를 내 4GB 앞에 놓으니 좀 처참하다. 검색·얼굴인식 임베딩을 NAS 안에서 돌리면 색인 한 번에 2코어가 전부 탈진하는 구조. “설치는 되지만 쓰는 재미는 없는” 전형적인 사례가 눈에 보였다.

그래서 결정을 반쯤 비틀었다. 서버는 NAS에 두되, 무거운 추론만 Mac mini M4(24GB, OrbStack)로 빼내는 Remote Machine Learning 구성. 공식 가이드가 지원하는 구성이니 지어낸 타협은 아니다.

2. 시놀로지 포토를 못 지우는 이유 — External Library로 엇갈리기

개인적으로 가장 고민했던 부분. 모바일 백업 습관은 이미 Synology Photos 앱에 박혀 있고, 가족 사진까지 그 계층에서 관리되고 있다. Immich 앱으로 백업을 통째 갈아타면 10만 건에 가까운 사진이 물리적으로 재업로드·재저장된다. 용량도 용량이지만 “어느 쪽이 원본인가”가 뒤섞이는 게 더 불안했다.

답은 External Library(읽기 전용 마운트)였다. 기존 시놀로지 포토 저장소를 Immich에 :ro로 붙이면 Immich은 목록을 읽고 파생 파일(썸네일·미리보기·임베딩)만 자기 공간에 만들 뿐, 원본을 물리적으로 삭제·이동할 수 없다.

등록 결과는 이렇다(오늘 실측, 관리자 화면 기준).

turbout 라이브러리: 사진 50,677 + 영상 4,973 = 55,650. edell81: 사진 18,261 + 영상 1,739 = 20,000. 스캔 총량 75,652건 완료. 역할 분담은 이렇게 확정했다.

  • Synology Photos = 모바일 백업·촬영·삭제 계층(유지)
  • Immich = 열람·검색·큐레이션 계층(읽기 전용이라 원본을 건드리지 않음)
  • Immich 앱 내 자동 백업은 OFF. 미리보기 생성 중에 이걸 켜면 원본이 재업로드돼 중복이 생기므로… 절대 켜지 말자(나 자신에게 하는 말).

3. 배치: NAS는 서버 3개, mini는 모델 저장소

/volume3/docker/immich에 immich-server(:2283) + Postgres(DB_STORAGE_TYPE: ‘HDD’ — SSD가 없으니 솔직하게 HDD 모드) + valkey. ML 컨테이너는 NAS에 하나도 넣지 않았다. mini 쪽 ~/immich-ml에 immich-machine-learning을 OrbStack으로 띄우고, Immich 관리자 화면에서 ML URL을 http://192.168.1.36:3003 하나로 교체(로컬 ML이 없으니 폴백이 아니라 완전 대체).

버전은 양쪽 모두 v3.3.5 태그로 고정. remote ML은 서버와 컨테이너 버전이 어긋나면 조용히 이상해지므로 .env의 IMMICH_VERSION과 mini 이미지를 같이 올리는 규칙을 미리 정해뒀다. mini의 model-cache 볼륨(약 2.2GB)은 CLIP ViT-B-32, buffalo_l(얼굴), PP-OCRv5_mobile 세 패밀리 — 이 캐시는 백업 대상이다, 지우면 다운로드부터 다시 시작이니까.

4. 여기서부터는 2체제의 작업 기록

이 설치의 특이점은, 내가 키보드를 거의 안 쳤다는 것이다(…). 우리 집에는 터미널·ssh를 잡는 에이전트(Hermes)와 로그인된 브라우저 안에서 DSM 화면을 실제로 클릭하는 브라우저 에이전트(Aside)가 있고, 이 날은 그 조합으로 굴렸다.

내가 한 일(Hermes, 터미널 쪽):

  • ssh m4 접속부터 한 번 막혔다. passphrase가 걸린 키는 게이트웨이 무접속 ssh에서 거부된다. 터미널에서 ssh-add –apple-use-keychain ~/.ssh/id_ed25519_m4 한 번, 키체인 등록 이후 무접속 접속 통과.
  • mini에 OrbStack 설치(DMG 마운트 오류·중복 앱 정리·PATH를 ~/.zshenv에 박아 넣기까지 꽤 구불구불한 경로였다), docker 29.8.2 확인, ~/immich-ml compose 작성·기동, healthcheck와 docker stats로 실측.

Aside이 한 일(브라우저 쪽, 이 NAS는 DSM 관리용 SSH이 닫혀 있어서):

  • File Station에서 compose.yaml 확인과 env 수정, Container Manager로 프로젝트 빌드·재시작
  • Immich 관리자 계정 생성, External Library 두 개 등록(스캔 시작까지), ML URL 교체 저장
  • 세션이 유지되므로 resume으로 이어 작업했고, 지시는 매번 범위를 명시했다 — “읽기 전용, 예외는 이 버튼 1회뿐, 비밀번호 출력 금지”처럼.

교차 검증도 구조화돼 있었다. Aside이 “큐 수치 0/0/0, healthy”라고 보고하면 그건 자기 보고일 뿐이므로, Hermes가 mini에서 독립적으로 다시 잰다: docker logs에 새 모델 캐시 다운로드(buffalo_l 415MB, OCR 51MB)가 찍히고 CPU 30초 표본 약 58% — “실제로 추론이 건너오고 있다”는 물증. 브라우저 에이전트의 숫자를 그대로 믿지 않는 이 규칙 덕분에 아래 5번의 세 번째 함정을 조기 발견했다.

5. 트러블 3건 — 다 기록으로 남긴다

1) Postgres 28P01 루프. 처음 작성한 최소 compose에서 immich-server의 env_file 줄 누락. 컨테이너가 죽어라 재시작하며 비밀번호 불일치를 외웠다. 이미 initdb된 데이터 디렉터리는 첫 비밀번호에 굳어 있으므로 .env 재생성 금지, postgres 볼륨 내용만 비우고 fresh initdb로 회수했다. (원본 사진 볼륨은 절대 손대지 않았다 — 이 절차에서 무서운 건 그쪽이다.)

2) 원격 ML 연결 플랩. healthy/unhealthy가 2~4분 주기로 교대하고 일부 작업이 ‘failed for all URLs’로 튀었다. LAN에서는 흔한 패턴: 기본 keep-alive 2초 + 모델 지연 로드 블립. mini compose에 전 모델 preload, MODEL_TTL=0, MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S=60을 넣고 재구축 — 17:17:00 healthy 이후 재발 0건으로 안정화.

3) 큐 ‘completed = 0’의 함정. Jobs 카드 숫자가 계속 0이라 진행이 없는 줄 알았는데, 서버 재기동과 ML URL 저장 시 카운터가 리셋되는 구조였다. 진행 판단은 completed가 아니라 queued 감소 + mini CPU 사용으로 해야 한다. 이걸 재발 방지 스킬에 함정 항목으로 박아놨다.

6. 모바일 앱에서 지금 보이는 것들

오늘 기준 타임라인은 회색 타일 + 구름 아이콘과 실제 사진이 섞여 보인다. 전자는 “썸네일이 아직 없는” 항목이고, 앱 로그 화면에는 Failed to decode image for request가 줄줄이 쌓인다 — 앱이 서버에 미리보기를 요청했으나 아직 생성 전이라 응답을 디코딩하지 못한 것. 스택이 RemoteImageApi·ThumbnailWidget에서 멈춰 있는 걸로 경로가 확인됐고, 데이터 손상이 아니라 색인 천이의 정상 증상이다. 파생 파일이 채워지는 순서대로 타일이 사진으로 바뀐다.

NAS 2코어가 썸네일·메타데이터를 뽑는 병목이라 75,652건은 시간 단위가 아니라 하루 안팎으로 보는 게 솔직하다(추정; mini 임베딩 처리 속도는 예열 단계라 미확인). mini는 썸네일이 완성된 것부터 임베딩을 받아온다. 최종 수치는 이 글 작성 시각에 확정되지 않았다 — 대신 22:35 KST 자동 재확인을 걸어뒀다: 진행률·병목·이상 재검증, Smart Search 유입이 0이면 ‘누락(Missing)’ 재스캔 1회까지 처리하는 조건부 지시다. 결과가 나오면 이 초안에 업데이트 섹션으로 붙인다.

7. 접속과 앱 설정

  • 집 안: http://192.168.1.100:2283 · 밖에서는 Tailscale 노드 ds725p의 http://100.97.114.48:2283. 첫 연결만 라우트 탐색으로 수 초 지연, 이후 즉시 — ping/curl 실측 확인됨.
  • Immich 모바일 앱에서 위 URL을 입력해 로그인하고, 앱 자동 백업은 OFF로 유지.
  • NAS를 자는 컴퓨터로 두면 안 된다 — 색인 중 절전·디스크 슬립은 큐 지연의 원인이 될 수 있다(오늘은 해당 없음, 주의 항목으로만 남긴다).

정리

  • 무거운(?) NAS + 넉넉한 mini 조합에서 Immich은 “서버는 NAS, 뇌는 mini”로 갈라 쓰는 게 정답에 가까웠다(Remote ML 공식 지원).
  • 기존 Synology Photos 습관은 External Library :ro로 살릴 수 있다. 백업 계층과 검색 계층을 분리하는 설계.
  • 에이전트 둘이 각각 잘하는 축을 잡으니(Hermes = ssh·docker·실측, Aside = 로그인된 DSM 화면 클릭) 사람이 한 키 입력은 ssh-add와 최종 승인뿐이었다. 숫자 보고는 상호 검증 전제로만 신뢰한다.
  • 함정 3건(env_file, keep-alive 플랩, completed 카운터 리셋)은 이미 재발 방지 스킬로 기록 완료.

사진 75,652장이 검색 가능한 라이브러리가 되는 과정을 하루에 지켜본 기록. 색인이 끝나면 “그제 아침에 찍은 음료 사진 찾아줘” 같은 요청이 실제로 되는지 후기 한 줄을 추가할 예정이다. 될 것이다. 아마도. 이 취미는 다음 고민거리를 계속 공급해준다 — 이번엔 “썸네일 하루치 NAS 병목을 SSD로 승부 볼까”다. (아니, 그 전에 4GB RAM부터… 취미는 하나씩.)

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다