노트북 한 대에 외장하드를 꽂으면 수십만 원짜리 NAS 장비 없이도 개인 미디어 서버가 완성된다. 기본 세팅을 넘어 실제 장기 운영에서 마주치는 난관들 — 한글 파일명 깨짐, Plex 고아 파일 누적, 트랜스코딩 임시 파일로 인한 디스크 포화, exFAT 마운트 실패 — 을 하나씩 해결하는 방법을 이 가이드에 모두 담았다.

전체 워크플로우 — 5단계 파이프라인

단순한 3단계 파이프라인이 아니라, 실제 장기 운영에서 필요한 정리·모니터링 단계까지 포함하면 5단계가 된다.

단계도구역할결과
1. 정리 Antigravity 파일명·폴더 구조 자동 교정 (TMDb/TheTVDB 대조) Plex 100% 인식 가능한 표준 구조
2. 서빙 Plex Media Server 메타데이터 스크래핑 · 트랜스코딩 · 스트리밍 앱·브라우저에서 Netflix처럼 재생
3. 원격 Tailscale Zero-Trust P2P 터널링 집 밖에서도 원본 화질로 접속
4. 사진 백업 Immich 스마트폰 사진·동영상 자동 백업 Google 포토 대체, 외장하드 저장
5. 정리·유지 fdupes · logrotate · cron 중복 파일 제거, 로그 순환, 임시파일 청소 디스크 포화 없는 장기 운영

외장하드 자동 마운트 설정

노트북에 외장하드를 꽂을 때마다 수동 마운트는 번거롭다. /etc/fstab에 UUID를 등록하면 부팅 시 자동으로 마운트된다.

bash
# 연결된 디스크 UUID 확인
lsblk -f
# 출력 예시:
# NAME   FSTYPE LABEL     UUID         MOUNTPOINT
# sdb
# └─sdb1 exfat  MY_DRIVE  A1B2-C3D4

# 파티션 상세 정보도 확인
blkid /dev/sdb1
# → /dev/sdb1: LABEL="MY_DRIVE" UUID="A1B2-C3D4" TYPE="exfat"

# 마운트 포인트 생성
sudo mkdir -p /mnt/external

# fstab에 자동 마운트 등록
# nofail: 외장하드가 없어도 부팅이 멈추지 않음
# uid/gid=1000: 일반 사용자로 읽기/쓰기 가능
echo 'UUID=A1B2-C3D4  /mnt/external  exfat  defaults,nofail,uid=1000,gid=1000,umask=0022  0  0' \
  | sudo tee -a /etc/fstab

# 즉시 마운트 테스트 (오류 없으면 성공)
sudo mount -a && ls /mnt/external

# 마운트 상태 확인
mount | grep /mnt/external
exFAT vs ext4 선택 기준

Windows·Mac 겸용이라면 exFAT을 선택한다. 파일 크기 제한이 없어 4K 영상 단일 파일(50GB+)도 문제없다. 리눅스 전용 서버라면 ext4가 더 빠르고 저널링으로 데이터 안전성이 높다. exFAT을 리눅스에서 쓰려면 exfatprogs (또는 구버전 커널에서는 exfat-fuse) 패키지가 필요하다 — 설치 안 됐을 때 마운트 실패하는 원인 1위다.

Antigravity 심화 — CLI 플래그 완전 해설

Antigravity는 단순한 파일 이동 도구가 아니다. TMDb와 TheTVDB API를 호출해 파일명에서 추론한 제목·연도를 검증하고, 표준 Plex 폴더 구조로 재배치하는 미디어 정규화 도구다. 플래그를 제대로 이해해야 사고를 막을 수 있다.

--dry-run을 반드시 먼저 실행해야 하는 이유

--dry-run은 실제 파일을 건드리지 않고 어떤 작업이 일어날지 시뮬레이션만 한다. 이 플래그 없이 바로 실행하면 수백 개 파일이 예상과 다른 경로로 이동하거나, 같은 이름의 파일이 덮어씌워질 수 있다. 특히 다음 세 가지 경우에는 반드시 dry-run부터 확인한다.

  • 한글·특수문자 파일명: 영문 외 문자 처리 결과를 미리 확인해야 한다
  • 연도 추론이 애매한 파일명: 영화_2023_재개봉.mkv처럼 연도가 두 개 이상 들어간 경우
  • 대용량 배치 작업: inbox에 파일이 100개 이상일 때 잘못된 이동 복구가 매우 번거롭다
bash
# ── Step 1: 반드시 dry-run 먼저 ──────────────────────────
antigravity \
  --source  /mnt/external/inbox \
  --movies  /mnt/external/Movies \
  --tv      /mnt/external/"TV Shows" \
  --music   /mnt/external/Music \
  --dry-run

# dry-run 출력 예시:
# [DRY-RUN] MOVE: 인셉션_2010_1080p.mkv
#           → /mnt/external/Movies/Inception (2010)/Inception (2010).mkv
# [DRY-RUN] MOVE: 브레이킹베드_S01E01.mkv
#           → /mnt/external/TV Shows/Breaking Bad/Season 01/Breaking Bad - S01E01.mkv
# [DRY-RUN] SKIP: unknown_file.nfo  (no match found)

# 출력 결과를 파일에 저장해 검토
antigravity --source /mnt/external/inbox \
            --movies /mnt/external/Movies \
            --tv     /mnt/external/"TV Shows" \
            --dry-run 2>&1 | tee /tmp/antigravity-preview.txt

# 건수 확인
grep -c 'MOVE' /tmp/antigravity-preview.txt
grep -c 'SKIP' /tmp/antigravity-preview.txt

# 문제 없으면 실제 실행
antigravity \
  --source  /mnt/external/inbox \
  --movies  /mnt/external/Movies \
  --tv      /mnt/external/"TV Shows" \
  --music   /mnt/external/Music \
  --move

주요 플래그 상세 설명

플래그의미예시 값주의사항
--source 정리되지 않은 원본 파일이 있는 inbox 폴더 /mnt/external/inbox 서브디렉토리까지 재귀 탐색
--dest 분류 불명 파일의 기본 도착지 (영화/TV 미판별 시 fallback) /mnt/external/Unsorted 지정 안 하면 source에 그대로 남음
--movies 영화 파일의 목적지 루트 /mnt/external/Movies Plex 라이브러리 경로와 동일하게 설정
--tv TV 시리즈의 목적지 루트 /mnt/external/TV Shows 공백 있는 경로는 따옴표 필수
--music 음악 파일(mp3/flac/m4a)의 목적지 루트 /mnt/external/Music Artist/Album 구조로 자동 분류
--dry-run 실제 파일 이동 없이 시뮬레이션만 출력 (값 없음, 플래그만) 항상 첫 실행 시 필수
--move 파일을 복사 후 원본 삭제 (이동) (값 없음, 플래그만) 기본값은 copy. 용량 2배 주의
--copy 원본을 유지하고 목적지에 복사 (값 없음, 플래그만) inbox 정리를 별도로 해야 함
--lang 메타데이터 스크래핑 언어 우선순위 ko,en 한국어 제목 우선 매칭에 사용
--sanitize 파일명 특수문자 자동 제거 (값 없음, 플래그만) 한글 포함 파일명도 처리됨

파일명 패턴: Movie Name (Year)/Movie Name (Year).mkv 구조의 이유

Plex가 영화를 인식하는 방식은 정규식 패턴 매칭이다. 내부적으로 (.+?) \((\d{4})\) 패턴으로 제목과 연도를 분리한다. 이 패턴이 왜 중요한지 세 가지 이유가 있다.

  • 같은 제목 다른 연도 구분: Dune (1984)Dune (2021)은 폴더명으로만 구별 가능하다. 파일명만으로는 Plex가 어느 쪽인지 확신할 수 없어 메타데이터 충돌이 생긴다
  • 폴더-파일명 일치로 확신도 상승: 폴더명과 파일명이 동일하면 Plex 내부 신뢰도 점수가 높아져 잘못된 메타데이터 매칭 가능성이 줄어든다
  • 멀티 버전 지원: 같은 영화의 감독판·극장판을 한 폴더에 넣을 때 구분자 표기가 가능하다 — Blade Runner 2049 (2017) - Director's Cut.mkv
text
# 올바른 Plex 표준 구조 (Antigravity 처리 후)
/mnt/external/
├── Movies/
│   ├── Inception (2010)/
│   │   └── Inception (2010).mkv          ← 폴더명 = 파일명 (연장자 제외)
│   ├── Dune (1984)/
│   │   └── Dune (1984).mkv
│   ├── Dune (2021)/
│   │   └── Dune (2021).mkv               ← 동명 다른 연도는 폴더로 구분
│   └── Blade Runner 2049 (2017)/
│       ├── Blade Runner 2049 (2017).mkv
│       └── Blade Runner 2049 (2017) - Director's Cut.mkv
└── TV Shows/
    ├── Breaking Bad/
    │   ├── Season 01/
    │   │   ├── Breaking Bad - S01E01 - Pilot.mkv
    │   │   └── Breaking Bad - S01E02 - Cat's in the Bag.mkv
    │   └── Season 02/
    │       └── Breaking Bad - S02E01 - Seven Thirty-Seven.mkv
    └── 오징어 게임/                        ← 한글 제목도 표준 구조 동일
        └── Season 01/
            └── 오징어 게임 - S01E01.mkv

# 처리 전 (Plex 인식 불가 예시)
영화_인셉션_2010_BluRay_1080p_x264.mkv
inception.2010.HDRip.720p.mkv
Breaking.Bad.S01E01.HDTV.XviD.avi
[SubGroup] 오징어게임 1화 (1080p).mkv

한글 파일명 처리 방법과 특수문자 제거

한글 파일명은 두 가지 문제를 일으킨다. 첫째, 일부 파일시스템이나 셸에서 인코딩 문제로 깨질 수 있다. 둘째, TMDb/TheTVDB 매칭 시 한국어 검색이 영어 검색보다 정확도가 낮을 수 있다. Antigravity의 --lang ko,en--sanitize 플래그를 조합하면 대부분 해결된다.

bash
# 한글 파일명 처리 전용 옵션 조합
antigravity \
  --source   /mnt/external/inbox \
  --movies   /mnt/external/Movies \
  --tv       /mnt/external/"TV Shows" \
  --lang     ko,en \       # 한국어 제목 우선 매칭, 실패 시 영어로 재시도
  --sanitize \             # 파일명에서 !, @, #, [, ] 등 특수문자 제거
  --dry-run

# sanitize 전/후 예시:
# 전: [SubGroup]_오징어게임_1화_(1080p)_[AAC].mkv
# 후: 오징어 게임 - S01E01.mkv

# 한글 파일명이 아예 인식 안 될 때: 먼저 파일명 확인
find /mnt/external/inbox -name "*.mkv" | head -20
# 한글이 ???로 깨진 경우: convmv로 인코딩 변환 (EUC-KR → UTF-8)
convmv -f euc-kr -t utf-8 --notest /mnt/external/inbox/*.mkv

# convmv 설치
sudo apt install convmv
TMDb API 키 설정

Antigravity는 내부적으로 TMDb API를 호출해 메타데이터를 검증한다. 기본 내장 API 키는 호출 횟수 제한이 있다. 영화·드라마가 많다면 themoviedb.org에서 무료 API 키를 발급받아 ~/.config/antigravity/config.tomltmdb_api_key = "your_key"로 설정하면 제한 없이 사용 가능하다.

Antigravity 실행 후 Plex 라이브러리 스캔 자동 트리거

Antigravity로 파일을 이동한 뒤 Plex가 자동으로 새 파일을 인식하려면 라이브러리 스캔을 트리거해야 한다. Plex는 REST API를 제공하므로 curl로 직접 스캔을 호출할 수 있다. 이 과정을 Antigravity 실행 직후 자동화하면 파일 추가 → 메타데이터 반영까지 완전 자동화된다.

bash
# ── Plex API로 라이브러리 스캔 트리거 ──────────────────────

# 1. Plex Token 확인 방법
#    Plex Web → 우측 상단 아이콘 → Account → 주소창에서
#    ?X-Plex-Token=XXXXXXXX 부분 복사
PLEX_TOKEN="XXXXXXXXXXXXXXXX"
PLEX_HOST="http://localhost:32400"

# 2. 라이브러리 목록 조회 (섹션 ID 확인용)
curl -s "${PLEX_HOST}/library/sections" \
     -H "X-Plex-Token: ${PLEX_TOKEN}" \
     -H "Accept: application/json" \
  | python3 -m json.tool | grep -E '"key"|"title"|"type"'

# 출력 예시:
# "key": "1",   "title": "Movies",   "type": "movie"
# "key": "2",   "title": "TV Shows", "type": "show"

# 3. 특정 섹션 스캔 (섹션 ID 1 = Movies)
curl -s "${PLEX_HOST}/library/sections/1/refresh" \
     -H "X-Plex-Token: ${PLEX_TOKEN}" \
     -X GET

# 4. 모든 섹션 스캔
for SECTION_ID in 1 2; do
  curl -s "${PLEX_HOST}/library/sections/${SECTION_ID}/refresh" \
       -H "X-Plex-Token: ${PLEX_TOKEN}" > /dev/null
  echo "Triggered scan for section ${SECTION_ID}"
done

# 5. Antigravity + Plex 스캔 완전 자동화 스크립트
cat > /usr/local/bin/media-import.sh << 'EOF'
#!/bin/bash
set -euo pipefail

PLEX_TOKEN="XXXXXXXXXXXXXXXX"
PLEX_HOST="http://localhost:32400"
LOG="/var/log/media-import.log"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting Antigravity..." | tee -a "$LOG"

antigravity \
  --source  /mnt/external/inbox \
  --movies  /mnt/external/Movies \
  --tv      /mnt/external/"TV Shows" \
  --lang    ko,en \
  --sanitize \
  --move 2>&1 | tee -a "$LOG"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Triggering Plex scan..." | tee -a "$LOG"

for SECTION_ID in 1 2; do
  curl -s "${PLEX_HOST}/library/sections/${SECTION_ID}/refresh" \
       -H "X-Plex-Token: ${PLEX_TOKEN}" > /dev/null
  echo "  Scanned section ${SECTION_ID}" | tee -a "$LOG"
done

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Done." | tee -a "$LOG"
EOF
chmod +x /usr/local/bin/media-import.sh

# 실행
media-import.sh

Antigravity 오류를 Claude Code로 디버깅하는 법

Antigravity가 특정 파일을 인식하지 못하거나 잘못 분류할 때 오류 로그를 직접 분석하는 것보다 Claude Code에 붙여넣는 것이 훨씬 빠르다. 특히 코덱 형식 문제나 API 응답 오류는 패턴이 있어서 Claude Code가 즉시 원인을 짚어준다.

bash
# Antigravity 상세 로그 출력 후 Claude Code에 전달
antigravity --source /mnt/external/inbox \
            --movies /mnt/external/Movies \
            --tv     /mnt/external/"TV Shows" \
            --verbose \
            --dry-run 2>&1 | tee /tmp/ag-debug.log

# Claude Code CLI로 오류 분석 요청
claude "antigravity 실행 오류 로그 분석해줘" < /tmp/ag-debug.log

# 또는 특정 오류 메시지만 전달
grep -i 'error\|warn\|skip\|fail' /tmp/ag-debug.log \
  | claude "이 antigravity 오류들의 원인과 해결법을 알려줘"

# Plex 컨테이너 로그 분석도 동일한 방식
docker logs plex 2>&1 | tail -100 \
  | claude "docker logs plex 오류 분석해줘"
"Antigravity에 --dry-run 없이 바로 --move를 실행했다가 200개 파일이 엉뚱한 폴더로 이동한 적이 있다. 5분짜리 드라이런이 3시간짜리 복구 작업을 막는다."

Plex Media Server 심화 — Docker 설치부터 트랜스코딩까지

Antigravity로 정리된 파일을 Plex가 서빙한다. 단순히 docker-compose를 올리는 것 이상으로, 모바일 대역폭에 맞는 트랜스코딩 설정, 라이브러리 스캔 스케줄, 고아 파일 정리까지 알아야 장기 운영이 가능하다.

bash
# plex.tv/claim 에서 클레임 토큰 발급 (로그인 필요, 4분 유효)
# → PLEX_CLAIM 값에 붙여넣기
yaml
# docker-compose.yml (최적화 버전)
services:
  plex:
    image: lscr.io/linuxserver/plex:latest
    container_name: plex
    network_mode: host        # GDM(자동 검색) + 성능을 위해 host 모드
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Seoul
      - VERSION=docker
      - PLEX_CLAIM=claim-xxxxxxxxxxxx    # plex.tv/claim 에서 발급 (4분 유효)
    volumes:
      - ./plex-config:/config            # 설정·DB·메타데이터 저장
      - /mnt/external/Movies:/movies:ro  # 읽기 전용으로 마운트 (안전)
      - /mnt/external/TV Shows:/tv:ro
      - /mnt/external/Music:/music:ro
      # 트랜스코딩 임시 파일을 RAM에 저장 (디스크 수명 보호)
      - /dev/shm:/transcode              # RAM 디스크 사용 (권장)
    devices:
      - /dev/dri:/dev/dri               # Intel QSV / VAAPI 하드웨어 트랜스코딩
    group_add:
      - "render"                         # /dev/dri 접근 권한 (109는 render 그룹 GID)
      - "video"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:32400/identity"]
      interval: 30s
      timeout: 10s
      retries: 3
bash
# Plex 시작
docker compose up -d

# 초기 설정 — 같은 네트워크 브라우저에서만 가능 (처음 한 번)
# http://192.168.1.xxx:32400/web

# 라이브러리 추가 경로:
#   영화       → /movies
#   TV 프로그램 → /tv
#   음악        → /music

# 컨테이너 상태 확인
docker ps | grep plex
docker logs plex --tail 50

# Plex 버전 확인
docker exec plex cat /etc/cont-init.d/99-custom-scripts 2>/dev/null || \
  docker exec plex plexmediaserver --version 2>/dev/null

트랜스코딩 품질 설정 — 모바일 대역폭 최적화

Plex의 트랜스코딩은 서버가 원본 파일을 실시간으로 다른 해상도·코덱으로 변환해 클라이언트에 전송하는 과정이다. LTE 환경에서 4K 원본을 그대로 보내면 버퍼링이 생기므로, 모바일 접속 시 자동으로 화질을 낮추도록 설정해야 한다.

bash
# ── 트랜스코딩 임시 폴더 RAM 디스크로 설정 ──────────────────
# docker-compose의 /dev/shm:/transcode 볼륨이 이를 담당
# RAM 디스크 사용량 확인
df -h /dev/shm
# → tmpfs 크기 = 시스템 RAM의 절반 (기본값)
# 필요시 늘리기 (재부팅 후 사라짐)
sudo mount -o remount,size=8G /dev/shm

# ── Plex 트랜스코딩 설정 (Web UI) ──────────────────────────
# Settings → Transcoder 섹션:
#
#  Transcoder quality:
#    Make my CPU work harder         ← CPU 인코딩 품질 최대
#    (하드웨어 가속 켜면 이 설정 무시됨)
#
#  Transcoder temporary directory:   /transcode  (← RAM 디스크)
#
#  Background transcoding x264 preset:
#    Fast  (속도 우선) 또는 Medium (균형)
#
#  Enable HDR tone mapping:           체크 (HDR→SDR 변환)

# ── 모바일 스트리밍 권장 화질별 대역폭 ──────────────────────
# 4K  원본 직접 재생:  ~25-40 Mbps 필요 (집 Wi-Fi만 가능)
# 1080p 트랜스코딩:    ~8 Mbps (LTE 충분)
# 720p  트랜스코딩:    ~4 Mbps (3G/약한 LTE)
# 480p  트랜스코딩:    ~2 Mbps (최소 품질, 데이터 절약)

# Plex 앱 설정 → Quality → Remote Streaming:
#   4 Mbps 720p 30fps  ← LTE 데이터 요금 아낄 때 권장
#   8 Mbps 1080p       ← Wi-Fi + 안정적인 LTE

# ── 하드웨어 트랜스코딩 동작 확인 ───────────────────────────
# 스트리밍 중 대시보드 확인: http://localhost:32400/status/sessions
# "hw" 아이콘이 보이면 하드웨어 가속 중
# Intel GPU 사용률 확인
intel_gpu_top   # sudo apt install intel-gpu-tools

Plex Pass vs 무료 — 기능 차이 완전 비교

기능무료Plex Pass ($4.99/월)
스트리밍 (집 안) 무제한 무제한
하드웨어 트랜스코딩 불가 가능 (Intel QSV, NVENC, AMD VCE)
모바일 앱 스트리밍 1분 미리보기만 무제한
오프라인 다운로드 (Sync) 불가 가능
Live TV · DVR 불가 가능 (튜너 카드 필요)
가족 공유 (Home) 불가 최대 15명
Plex Relay (원격) 가능 (속도 제한 있음) 가능 (제한 없음)
트랜스코더 프리셋 제어 기본만 세밀한 제어 가능
무료로도 충분한 경우

Tailscale로 직접 연결하면 Plex Relay를 거치지 않으므로 원격 스트리밍에서 Plex Pass 없이도 무제한 속도가 나온다. 하드웨어 트랜스코딩이 필요하지 않고 가족 공유도 필요 없다면 Tailscale + 무료 Plex 조합이 가장 효율적이다.

라이브러리 정기 스캔 스케줄 설정

Plex는 자체 스캔 스케줄 기능이 있지만, 새벽에 자동으로 실행되도록 cron으로 관리하면 더 세밀하게 제어할 수 있다.

bash
# ── Plex Web UI 스케줄 설정 경로 ────────────────────────────
# Settings → Library → Scan my library automatically     → ON
# Settings → Library → Run a partial scan when changes   → ON
# Settings → Library → Empty trash automatically         → ON
# Settings → Scheduled Tasks → Update all libraries      → 새벽 3시 (권장)

# ── cron으로 Plex API 스캔 트리거 (더 세밀한 제어) ──────────
# crontab 편집
crontab -e

# 매일 새벽 4시에 전체 라이브러리 스캔
0 4 * * * curl -s "http://localhost:32400/library/sections/all/refresh" \
               -H "X-Plex-Token: XXXXXXXXXXXXXXXX" > /dev/null

# 매주 일요일 새벽 3시에 메타데이터 전체 갱신 (포스터 등 업데이트)
0 3 * * 0 curl -s "http://localhost:32400/library/sections/all/refresh?force=1" \
               -H "X-Plex-Token: XXXXXXXXXXXXXXXX" > /dev/null

# 30분마다 inbox 변경 감지 후 스캔 (새 파일 빠른 반영)
*/30 * * * * /usr/local/bin/media-import.sh >> /var/log/media-import.log 2>&1

고아(Orphaned) 파일 정리 — Plex DB에 남는 유령 파일 해결

외장하드에서 파일을 직접 삭제하거나 Antigravity로 이동한 뒤 Plex DB가 갱신되지 않으면, 포스터는 보이지만 재생하면 "파일을 찾을 수 없음" 오류가 나는 고아 항목이 쌓인다. 특히 수백 GB를 정리한 뒤 이 문제가 두드러진다.

bash
# ── 방법 1: Plex Web UI에서 수동 정리 ──────────────────────
# 라이브러리 클릭 → [...] (더보기) → Clean Bundles
# → 물리적으로 존재하지 않는 파일 항목 제거

# ── 방법 2: Plex API로 자동 정리 ────────────────────────────
PLEX_TOKEN="XXXXXXXXXXXXXXXX"
PLEX_HOST="http://localhost:32400"

# Empty trash (고아 미디어 항목 제거)
curl -s -X PUT "${PLEX_HOST}/library/sections/all/emptyTrash" \
     -H "X-Plex-Token: ${PLEX_TOKEN}"

# Clean bundles (사용되지 않는 메타데이터 번들 제거)
curl -s -X PUT "${PLEX_HOST}/library/clean/bundles" \
     -H "X-Plex-Token: ${PLEX_TOKEN}"

# ── 방법 3: 고아 파일 사전 예방 스크립트 ────────────────────
# Plex DB에 있는 파일 목록과 실제 파일시스템을 비교
# (Plex DB는 SQLite 형식)
PLEX_DB="./plex-config/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db"

# DB에 등록된 파일 경로 중 실제로 없는 것 찾기
sqlite3 "$PLEX_DB" \
  "SELECT file FROM media_parts WHERE file LIKE '/movies/%'" \
  | while read filepath; do
      if [ ! -f "$filepath" ]; then
        echo "ORPHAN: $filepath"
      fi
    done | tee /tmp/plex-orphans.log

echo "Found $(wc -l < /tmp/plex-orphans.log) orphaned entries"

Plex 데이터베이스 비대화 방지 — 메타데이터 정기 정리

Plex는 포스터·썸네일·메타데이터를 로컬 DB와 캐시에 저장한다. 시간이 지나면 삭제한 파일의 메타데이터가 DB에 남아 수 GB까지 비대해진다. 정기적으로 정리하지 않으면 SSD 수명에도 영향을 미친다.

bash
# ── Plex 데이터 디렉토리 크기 확인 ────────────────────────
du -sh ./plex-config/Library/Application\ Support/Plex\ Media\ Server/*/
# 출력 예시:
# 2.3G  Cache/          ← 트랜스코딩 캐시, 썸네일
# 1.8G  Metadata/       ← 포스터, 배경, 메타데이터
# 890M  Plug-in Support/  ← DB, 플러그인 데이터

# ── Plex 캐시 경로 (컨테이너 내부 → 호스트 매핑) ──────────
PLEX_CACHE="./plex-config/Library/Application Support/Plex Media Server/Cache"
PLEX_META="./plex-config/Library/Application Support/Plex Media Server/Metadata"

# 트랜스코딩 임시 캐시만 삭제 (Transcoder 폴더)
rm -rf "${PLEX_CACHE}/Transcoder"
echo "Transcoder cache cleared"

# ── 전체 DB 최적화 (VACUUM) ─────────────────────────────────
PLEX_DB="./plex-config/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db"

# Plex 컨테이너 중단 후 DB VACUUM (실행 중엔 위험)
docker compose stop plex
sqlite3 "$PLEX_DB" "VACUUM;"
docker compose start plex
echo "DB vacuumed and Plex restarted"

# ── 주기적 메타데이터 정리 자동화 ───────────────────────────
# crontab -e 에 추가
# 매월 1일 새벽 2시에 캐시 정리 + DB 최적화
0 2 1 * * docker compose -f /path/to/docker-compose.yml stop plex && \
          rm -rf "/path/to/plex-config/Library/Application Support/Plex Media Server/Cache/Transcoder" && \
          sqlite3 "/path/to/plex-config/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db" "VACUUM;" && \
          docker compose -f /path/to/docker-compose.yml start plex

스마트폰에서 Plex 앱으로 스트리밍

App Store / Play Store에서 Plex 앱 설치 후 로그인하면 집 Wi-Fi 안에서는 자동으로 서버가 감지된다. 영상을 탭하면 즉시 스트리밍이 시작되며, 네트워크 속도에 따라 화질이 자동 조정된다.

플랫폼무료 여부특징
Plex (공식)iOS · Android · TV무료 (일부 기능 유료)자동 서버 감지, 오프라인 다운로드 (Pass 필요)
InfuseiOS · tvOS유료 ($9.99/년)최고 화질, HDR Dolby Vision, AV1 네이티브 지원
웹 브라우저모든 기기무료앱 설치 없이 100.x.x.x:32400/web 접속
Plex HTPCWindows · Linux TV무료TV용 10-foot UI, 리모컨 지원

Tailscale로 집 밖에서도 동일하게 접속

Plex는 기본적으로 Plex Relay 서버를 통한 원격 스트리밍을 지원하지만, 속도가 느리고 무료 플랜에서는 화질이 제한된다. Tailscale을 사용하면 집 서버와 직접 P2P 연결을 맺어 원본 화질 그대로 스트리밍할 수 있다. Plex Relay를 완전히 우회하기 때문에 무료 Plex로도 해외에서 4K를 볼 수 있다.

bash
# 서버에 Tailscale 설치 및 활성화
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh

# Tailscale IP 확인 (100.x.x.x 형태 고정 IP)
tailscale ip -4
# → 100.64.0.12 (예시)

# Tailscale 상태 확인
tailscale status

# ── 스마트폰 설정 ────────────────────────────────────────────
# 1. Tailscale 앱 설치 (iOS/Android)
# 2. 같은 계정으로 로그인
# 3. Plex 앱 → 설정 → 수동 서버 추가
#    주소: 100.64.0.12   포트: 32400
# 4. Plex Relay 비활성화 (중요!)
#    Plex 앱 → 설정 → Advanced → Custom server access URLs
#    → http://100.64.0.12:32400 추가
#    → Relay 사용 해제됨
bash
# ── Subnet Router: 홈 네트워크 전체 노출 (선택) ─────────────
# 이 설정을 하면 스마트폰에서 집 안 모든 기기(NAS, 라우터, Samba 등) 접근 가능
sudo tailscale up \
  --advertise-routes=192.168.1.0/24 \
  --ssh

# Tailscale Admin Console에서 승인 필요 (한 번만)
# → tailscale.com/admin → Machines → 서버 → Edit route settings

# 스마트폰 Files 앱에서 Samba 접근
# → smb://100.64.0.12/media

# ── Exit Node: 모든 트래픽을 집을 통해 라우팅 ───────────────
# 집 서버를 VPN 출구 노드로 설정 (선택)
sudo tailscale up --advertise-exit-node

# 스마트폰에서 Exit Node 활성화 → 공개 Wi-Fi에서 보안 강화

데이터·스토리지 정리 핵심 가이드

장기 운영에서 가장 자주 발생하는 문제는 디스크 포화다. 트랜스코딩 캐시, 중복 파일, Docker 볼륨, 로그 파일이 쌓이면서 수십 GB를 잠식한다. 이 섹션은 이 모든 것을 자동화하는 방법을 다룬다.

외장하드 용량 현황 확인

bash
# ── 전체 디스크 사용량 ───────────────────────────────────────
df -h
# 출력 예시:
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/sda1       120G   45G   75G  38% /
# /dev/sdb1       4.0T  2.8T  1.2T  70% /mnt/external

# ── 외장하드 내부 폴더별 용량 ────────────────────────────────
du -sh /mnt/external/*
# 출력 예시:
# 1.8T  /mnt/external/Movies
# 650G  /mnt/external/TV Shows
# 120G  /mnt/external/Music
#  45G  /mnt/external/Immich
#  12G  /mnt/external/inbox
#   3G  /mnt/external/Unsorted

# 용량 상위 폴더 트리 보기 (ncdu — 대화형 UI)
sudo apt install ncdu
ncdu /mnt/external

# 용량 상위 파일 TOP 20
find /mnt/external -type f -printf '%s %p\n' \
  | sort -rn \
  | head -20 \
  | awk '{printf "%.1fGB  %s\n", $1/1024/1024/1024, $2}'

# 특정 확장자 총 용량 (mkv 파일만)
find /mnt/external -name "*.mkv" -printf '%s\n' \
  | awk '{sum+=$1} END {printf "Total MKV: %.1f GB\n", sum/1024/1024/1024}'

Plex 트랜스코딩 임시 파일 정기 삭제

Plex 트랜스코딩 캐시는 스트리밍 후 자동 삭제되어야 하지만, 비정상 종료 시 임시 파일이 남는다. /dev/shm을 트랜스코딩 폴더로 쓰면 재부팅 시 자동 초기화되지만, 디스크에 저장하는 설정이라면 직접 정리해야 한다.

bash
# ── 트랜스코딩 캐시 경로 ────────────────────────────────────
# Docker 볼륨 마운트 기준 (호스트 경로):
TRANSCODE_DIR="./plex-config/Library/Application Support/Plex Media Server/Cache/Transcoder"
# 또는 /dev/shm 사용 시:
TRANSCODE_RAM="/dev/shm"

# 현재 트랜스코딩 캐시 크기 확인
du -sh "$TRANSCODE_DIR" 2>/dev/null || echo "RAM transcode: $(du -sh /dev/shm)"

# 트랜스코딩 중인 세션이 있는지 확인 후 삭제
ACTIVE_SESSIONS=$(curl -s "http://localhost:32400/status/sessions" \
  -H "X-Plex-Token: XXXXXXXXXXXXXXXX" | python3 -c \
  "import sys,json; d=json.load(sys.stdin); print(d.get('MediaContainer',{}).get('size',0))")

if [ "$ACTIVE_SESSIONS" -eq 0 ]; then
  rm -rf "${TRANSCODE_DIR:?}"/*
  echo "Transcoder cache cleared (no active sessions)"
else
  echo "Skipped: ${ACTIVE_SESSIONS} active session(s)"
fi

# ── cron으로 매일 자동 정리 (새벽 5시, 세션 없을 때) ─────────
# crontab -e 에 추가:
0 5 * * * SESSIONS=$(curl -s "http://localhost:32400/status/sessions" -H "X-Plex-Token: TOKEN" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('MediaContainer',{}).get('size',0))"); [ "$SESSIONS" -eq 0 ] && rm -rf "./plex-config/Library/Application Support/Plex Media Server/Cache/Transcoder"/*

중복 파일 탐지 및 제거

같은 영화를 여러 번 받거나, Antigravity 실행 전 수동으로 이동한 파일이 남아 중복이 생긴다. fdupes는 파일 내용을 MD5/SHA로 비교해 완전히 동일한 파일을 찾아낸다.

bash
# fdupes 설치
sudo apt install fdupes

# ── 중복 파일 탐지 (미디어 폴더) ────────────────────────────
# -r: 재귀 탐색, -S: 파일 크기 표시, -n: 빈 파일 제외
fdupes -rSn /mnt/external/Movies
fdupes -rSn /mnt/external/"TV Shows"

# 출력 예시:
# 1073741824 bytes each:
# /mnt/external/Movies/Inception (2010)/Inception (2010).mkv
# /mnt/external/inbox/inception.2010.1080p.BluRay.mkv      ← 중복!

# ── 중복 파일 대화형 삭제 ─────────────────────────────────
# -d: 대화형 모드 (각 그룹에서 유지할 파일 선택)
fdupes -rSnd /mnt/external/Movies

# ── 자동 삭제 (첫 번째 파일만 유지) — 주의: 검토 후 실행 ────
# --delete: 각 중복 그룹에서 첫 파일 외 모두 삭제
fdupes -rn --delete /mnt/external/inbox    # inbox만 (안전)

# inbox와 Movies 비교 — inbox의 중복만 삭제 (권장 워크플로)
fdupes -rn /mnt/external/Movies /mnt/external/inbox \
  | grep "^/mnt/external/inbox" \
  | while read dup; do
      echo "Removing duplicate from inbox: $dup"
      rm "$dup"
    done

# ── jdupes (fdupes보다 빠른 대안) ──────────────────────────
sudo apt install jdupes
jdupes -rS /mnt/external/Media    # -S: 크기 표시

Antigravity 정리 후 inbox 자동 비우기 스크립트

Antigravity --move 옵션을 쓰면 처리된 파일은 자동 삭제되지만, 인식하지 못한 파일(SKIP)은 inbox에 남는다. 이 잔여 파일들을 어떻게 처리할지 자동화 스크립트로 관리한다.

bash
# ── inbox 정리 완전 자동화 스크립트 ─────────────────────────
cat > /usr/local/bin/inbox-cleanup.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

INBOX="/mnt/external/inbox"
UNSORTED="/mnt/external/Unsorted"
LOG="/var/log/inbox-cleanup.log"
PLEX_TOKEN="XXXXXXXXXXXXXXXX"
PLEX_HOST="http://localhost:32400"

echo "=== $(date '+%Y-%m-%d %H:%M:%S') ===" | tee -a "$LOG"

# 1. Antigravity 실행 (dry-run 없이 — 이미 검토됐다고 가정)
echo "[1/4] Running Antigravity..." | tee -a "$LOG"
antigravity \
  --source  "$INBOX" \
  --movies  /mnt/external/Movies \
  --tv      /mnt/external/"TV Shows" \
  --music   /mnt/external/Music \
  --lang    ko,en \
  --sanitize \
  --move 2>&1 | tee -a "$LOG"

# 2. 인식 못한 파일 → Unsorted로 이동 (나중에 수동 처리)
mkdir -p "$UNSORTED"
UNRECOGNIZED=$(find "$INBOX" -type f \
  \( -name "*.mkv" -o -name "*.mp4" -o -name "*.avi" \
     -o -name "*.mov" -o -name "*.m4v" \) 2>/dev/null)

if [ -n "$UNRECOGNIZED" ]; then
  echo "[2/4] Moving unrecognized files to Unsorted..." | tee -a "$LOG"
  echo "$UNRECOGNIZED" | while read f; do
    mv "$f" "$UNSORTED/"
    echo "  → Unsorted: $(basename "$f")" | tee -a "$LOG"
  done
else
  echo "[2/4] No unrecognized files." | tee -a "$LOG"
fi

# 3. inbox의 빈 폴더 삭제
echo "[3/4] Removing empty directories..." | tee -a "$LOG"
find "$INBOX" -type d -empty -delete
echo "  Done." | tee -a "$LOG"

# 4. Plex 라이브러리 스캔 트리거
echo "[4/4] Triggering Plex library scan..." | tee -a "$LOG"
for SECTION_ID in 1 2 3; do
  curl -s -X GET \
    "${PLEX_HOST}/library/sections/${SECTION_ID}/refresh" \
    -H "X-Plex-Token: ${PLEX_TOKEN}" > /dev/null && \
    echo "  Scanned section ${SECTION_ID}" | tee -a "$LOG"
done

echo "=== Cleanup complete ===" | tee -a "$LOG"
SCRIPT
chmod +x /usr/local/bin/inbox-cleanup.sh

Docker 볼륨 및 이미지 정리

Docker는 오래된 이미지, 사용되지 않는 볼륨, 중단된 컨테이너를 자동으로 삭제하지 않는다. 시간이 지나면 수 GB가 /var/lib/docker에 쌓인다.

bash
# ── Docker 사용 현황 확인 ─────────────────────────────────
docker system df
# 출력 예시:
# TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
# Images          8         3         12.4GB    8.2GB (66%)
# Containers      5         2         124MB     98MB (79%)
# Local Volumes   12        4         3.8GB     2.1GB (55%)
# Build Cache     0         0         0B        0B

# ── 중단된 컨테이너 + 미사용 이미지 + 캐시 정리 ────────────
# -f: 확인 없이 바로 실행
docker system prune -f
# 더 공격적으로: 현재 사용 중이 아닌 이미지도 삭제
docker system prune -af

# ── 미사용 볼륨만 정리 ──────────────────────────────────────
docker volume prune -f

# ── 특정 이미지만 삭제 (plex 구버전 태그 등) ─────────────────
docker image ls | grep plex
docker image rm lscr.io/linuxserver/plex:1.32.x.xxxx

# ── Docker 루트 디렉토리 용량 확인 ─────────────────────────
du -sh /var/lib/docker/
du -sh /var/lib/docker/overlay2/  # 이미지 레이어

# ── 정기 Docker 정리 cron ───────────────────────────────────
# 매주 일요일 새벽 2시에 미사용 리소스 정리
0 2 * * 0 docker system prune -f >> /var/log/docker-prune.log 2>&1

로그 파일 자동 Rotation (logrotate 설정)

미디어 임포트 스크립트, Antigravity, Docker 로그가 무제한 쌓이면 SSD 수명을 단축시킨다. logrotate로 오래된 로그를 자동 압축·삭제한다.

bash
# ── logrotate 설정 파일 생성 ────────────────────────────────
sudo tee /etc/logrotate.d/media-server << 'EOF'
/var/log/media-import.log
/var/log/inbox-cleanup.log
/var/log/docker-prune.log
{
    # 30일치 보관
    rotate 30
    # 매일 순환
    daily
    # 파일이 없어도 오류 없음
    missingok
    # 빈 파일은 순환 안 함
    notifempty
    # gzip 압축
    compress
    # 다음 날 압축 (현재 로그는 즉시 읽을 수 있게)
    delaycompress
    # 날짜를 확장자로 사용 (media-import.log.2026-05-19)
    dateext
    # 10MB 초과 시 즉시 순환 (날짜와 무관)
    size 10M
}
EOF

# ── Docker 컨테이너 로그 크기 제한 ─────────────────────────
# docker-compose.yml에 logging 옵션 추가:
# services:
#   plex:
#     ...
#     logging:
#       driver: "json-file"
#       options:
#         max-size: "50m"    # 최대 50MB
#         max-file: "3"      # 최대 3개 파일 보관

# 현재 Plex 컨테이너 로그 크기 확인
du -sh $(docker inspect plex \
  | python3 -c "import sys,json;d=json.load(sys.stdin);print(d[0]['LogPath'])")

# logrotate 테스트 실행
sudo logrotate -d /etc/logrotate.d/media-server   # dry-run
sudo logrotate -f /etc/logrotate.d/media-server   # 강제 실행

오류 발생 시 대응 — 실전 트러블슈팅

직접 장기 운영하면서 반복적으로 마주친 오류 패턴과 해결법을 정리했다. 각 오류마다 즉시 적용 가능한 명령어와 Claude Code를 활용한 심화 디버깅 방법을 함께 제시한다.

오류 1: exFAT 마운트 실패 — exfat-fuse 미설치

bash
# 증상:
# sudo mount -a 실행 시:
# mount: /mnt/external: wrong fs type, bad option, bad superblock on /dev/sdb1

# 원인 확인
dmesg | tail -20
# → "No filesystem could mount root, tried: exfat"
# → exFAT 드라이버가 커널에 없거나 userspace 도구 미설치

# ── 해결 방법 (Ubuntu 22.04+) ────────────────────────────────
# 최신 커널(5.7+)에서는 exfatprogs 사용
sudo apt update
sudo apt install exfatprogs

# 구버전 커널 또는 FUSE 방식 사용 시
sudo apt install exfat-fuse exfat-utils

# 재마운트
sudo mount -a
mount | grep /mnt/external

# ── 커널 버전 확인 ────────────────────────────────────────────
uname -r
# 5.7 이상: 커널 내장 exFAT 드라이버 사용 (exfatprogs)
# 5.7 미만: FUSE 드라이버 (exfat-fuse)

# ── exFAT 마운트 옵션 최적화 (fstab) ─────────────────────────
# 기존:   UUID=A1B2  /mnt/external  exfat  defaults,nofail  0 0
# 최적화: iocharset=utf8 추가로 한글 파일명 깨짐 방지
# UUID=A1B2  /mnt/external  exfat  defaults,nofail,uid=1000,gid=1000,iocharset=utf8  0 0

오류 2: Plex가 /dev/dri 접근 권한 없음 — 하드웨어 트랜스코딩 실패

bash
# 증상:
# Plex 대시보드에서 스트리밍 시 CPU 100% 사용, "hw" 아이콘 없음
# docker logs plex | grep -i "dri\|vaapi\|qsv\|transcode"
# → "[transcode] Hardware accelerated decoding unavailable"
# → "failed to open device /dev/dri/renderD128: Permission denied"

# ── 원인 확인 ────────────────────────────────────────────────
# /dev/dri 장치 존재 여부 확인
ls -la /dev/dri/
# → crw-rw---- 1 root render 226, 128 /dev/dri/renderD128

# render 그룹 GID 확인
getent group render
# → render:x:109:  (GID = 109)

# 현재 plex 컨테이너의 그룹 확인
docker exec plex id
# → uid=1000(abc) gid=1000(abc) groups=1000(abc)
# render(109)가 없음 → 접근 불가

# ── 해결 방법 ─────────────────────────────────────────────────
# docker-compose.yml에 group_add 추가
# services:
#   plex:
#     ...
#     group_add:
#       - "render"   # /dev/dri 접근용
#       - "video"    # 일부 시스템에서 추가 필요

# 컨테이너 재시작
docker compose down && docker compose up -d

# 확인
docker exec plex id
# → uid=1000(abc) gid=1000(abc) groups=1000(abc),109(render),44(video)

# Intel GPU 드라이버 설치 확인
sudo apt install intel-media-va-driver vainfo
vainfo
# → VAEntrypointEncSlice, VAEntrypointVLD 항목이 나오면 정상

오류 3: Antigravity가 파일을 인식하지 못함 — 코덱·컨테이너 형식 문제

bash
# 증상:
# antigravity --dry-run 출력에서 SKIP이 많음
# [SKIP] unknown_movie_2023.avi  (confidence: 0.12)
# [SKIP] 영화파일.rmvb            (unsupported container)

# ── 원인 1: 파일명에서 제목 추론 실패 ────────────────────────
# 대응: 파일명을 좀 더 표준적으로 바꿔서 재시도
# 예: "알수없는영화_2023.mkv" → "알수없는영화 2023.mkv" (언더스코어 → 공백)

# 한꺼번에 언더스코어를 공백으로 치환
for f in /mnt/external/inbox/*.mkv; do
  mv "$f" "${f//_/ }"
done

# ── 원인 2: 구형 컨테이너 (rmvb, avi, divx) ─────────────────
# Antigravity는 파일을 이동만 하므로 컨테이너 형식은 관계없음
# 실제 문제는 Plex가 .rmvb를 재생 못하는 것 → 리먹싱 필요

# ffprobe로 파일 정보 확인
ffprobe -v quiet -print_format json -show_format -show_streams \
  /mnt/external/inbox/oldmovie.avi | python3 -m json.tool | head -40

# avi → mkv 리먹싱 (재인코딩 없이 컨테이너만 변경, 빠름)
ffmpeg -i "oldmovie.avi" -c copy "oldmovie.mkv"
rm "oldmovie.avi"

# ── 원인 3: TMDb 매칭 실패 (영화 정보 DB에 없는 마이너 작품) ──
# 대응: --dest 플래그로 미인식 파일 별도 폴더로 분리 후 수동 처리
antigravity \
  --source /mnt/external/inbox \
  --movies /mnt/external/Movies \
  --tv     /mnt/external/"TV Shows" \
  --dest   /mnt/external/Unsorted \    # 인식 실패 파일 여기로
  --move

# Unsorted 폴더 파일은 Plex에서 수동으로 메타데이터 매칭

# ── Claude Code로 Antigravity 오류 심화 분석 ──────────────────
antigravity --verbose --source /mnt/external/inbox \
            --movies /mnt/external/Movies --dry-run \
            2>&1 | grep -i 'error\|warn\|skip\|fail' \
  | claude "antigravity 실행 오류 로그 분석해줘. 각 오류의 원인과 해결 방법을 알려줘"

오류 4: Docker 컨테이너 일반 오류 — Claude Code 심화 분석

bash
# ── Plex 컨테이너 오류 분석 ──────────────────────────────────
# 최근 100줄 로그 확인
docker logs plex --tail 100

# 오류 레벨 로그만 필터링
docker logs plex 2>&1 | grep -iE 'error|crit|fatal|warn' | tail -30

# 타임스탬프 포함 로그
docker logs plex --timestamps --tail 50

# ── Claude Code에 로그 분석 요청 ────────────────────────────
# 방법 1: 파이프로 직접 전달
docker logs plex 2>&1 | tail -200 \
  | claude "docker logs plex 오류 분석해줘. 오류 원인과 해결법을 단계별로 설명해줘"

# 방법 2: 파일로 저장 후 전달
docker logs plex 2>&1 > /tmp/plex-logs.txt
claude "이 Plex 컨테이너 로그에서 문제가 뭔지 분석해줘" < /tmp/plex-logs.txt

# 방법 3: 특정 오류 집중 분석
docker logs plex 2>&1 | grep -i "database\|corrupt\|error" \
  | claude "Plex 데이터베이스 오류처럼 보이는데 원인과 해결법을 알려줘"

# ── Immich 오류 분석 ────────────────────────────────────────
docker logs immich-server 2>&1 | tail -100 \
  | claude "immich 서버 로그 오류 분석해줘"

# ── 컨테이너 상태 전체 점검 ────────────────────────────────
docker ps -a
# STATUS가 "Exited" 또는 "Restarting"이면 오류

# 종료된 컨테이너의 exit code 확인
docker inspect plex | python3 -c \
  "import sys,json;d=json.load(sys.stdin);print('Exit code:', d[0]['State']['ExitCode'])"
# Exit code 137: OOM(메모리 부족) 강제 종료
# Exit code 1:   일반 오류 (로그 확인 필요)
Claude Code 오류 분석 팁

로그를 그냥 붙여넣는 것보다 어떤 작업을 했을 때 오류가 났는지를 함께 설명하면 훨씬 정확한 답변이 나온다. 예: claude "Antigravity로 파일 100개 정리 후 Plex 스캔을 실행했더니 이 오류가 났어. 원인이 뭐야?" — 컨텍스트가 있으면 Claude Code가 전후 인과관계를 파악하고 더 정확한 해결책을 제시한다.

자주 발생하는 오류 빠른 참조표

오류 메시지원인즉시 해결
wrong fs type, bad option exfat-fuse / exfatprogs 미설치 sudo apt install exfatprogs
Permission denied /dev/dri Docker 컨테이너에 render 그룹 없음 docker-compose에 group_add: [render] 추가
confidence: 0.xx (too low) 파일명에서 제목 추론 실패 --dest /Unsorted 추가 후 수동 처리
no space left on device 디스크 포화 (트랜스코딩 캐시 등) docker system prune -f + 캐시 삭제
database disk image is malformed Plex DB 손상 (비정상 종료) sqlite3 plex.db "PRAGMA integrity_check;"
Plex 원격 접속 안 됨 Tailscale 미연결 또는 Relay 차단 tailscale status 확인, Plex 수동 서버 주소 입력
Immich 업로드 실패 /mnt/external 마운트 해제됨 mount | grep /mnt/external 확인 후 재마운트

Immich로 스마트폰 사진 자동 백업

Immich는 Google 포토와 동일한 UX의 셀프 호스팅 사진 서버다. 얼굴 인식, 지도 보기, AI 검색("작년 여름 바다 사진") 기능까지 갖췄다. 앱 설치 후 Wi-Fi 연결 시 사진·동영상이 외장하드로 자동 백업된다. Google 포토 2TB 플랜 월 3,900원 절약 효과.

yaml
# immich docker-compose.yml (핵심 서비스)
# 전체 공식 파일: immich.app/docs/install/docker-compose
services:
  immich-server:
    image: ghcr.io/immich-app/immich-server:release
    container_name: immich-server
    ports:
      - 2283:2283
    volumes:
      - /mnt/external/Immich:/usr/src/app/upload   # 사진 저장 경로
      - /etc/localtime:/etc/localtime:ro
    env_file: .env
    depends_on:
      - redis
      - database
    restart: unless-stopped

  # .env 파일 필수 내용:
  # DB_PASSWORD=강력한패스워드
  # DB_USERNAME=postgres
  # DB_DATABASE_NAME=immich
  # UPLOAD_LOCATION=/usr/src/app/upload
  # IMMICH_VERSION=release

  # redis, database, machine-learning 서비스는 공식 compose 파일 참조
bash
# Immich 시작 후 접속
docker compose up -d
# 브라우저: http://192.168.1.xxx:2283
# 첫 접속 시 관리자 계정 생성

# Immich 앱 설정 (스마트폰)
# 서버 주소: http://192.168.1.xxx:2283 (집 Wi-Fi)
# 또는 Tailscale 사용: http://100.x.x.x:2283 (어디서든 접속)

# 자동 백업 설정:
# Immich 앱 → Settings → Backup → Auto Backup: ON
# Background backup: ON (화면 꺼진 상태에서도 백업)
# Wi-Fi only: ON (데이터 절약)

# Immich 스토리지 사용량 확인
du -sh /mnt/external/Immich/
docker exec immich-server df -h /usr/src/app/upload
"이 모든 서비스를 6개월째 운영하면서 딱 두 가지가 가장 중요하다는 걸 배웠다. Antigravity dry-run을 건너뛰지 말 것, 그리고 cron으로 캐시 정리를 자동화할 것. 나머지는 다 오류가 나도 복구할 수 있다."