반응형

시놀로지 NAS(DSM)의 Docker에 Stash를 설치하고, 전용 Cloudflare WARP 프록시와 R18.dev PostgreSQL DB를 붙여 R18dev_SQL 스크레이퍼로 메타데이터를 가져오는 구성이다.

Claude를 통해 설치 절차를 도움받았고, 중간중간 트러블슈팅 과정도 함께 작성하였다.

 

* 본 문서의 Docker는 NAS(Synology) 환경에서 구동되고 있으며 Docker 디렉토리가 Volume2에 설치되어 있다.


1. 구성 개요

            stash-net (bridge)
 ┌─────────────────────────────────────────────┐
 │                                             │
 │  stash ──HTTP(S)──▶ stash-socks ──▶ 인터넷 │
 │    │                 (WARP)                 │
 │    └──psql──▶ r18-postgres                  │
 │                    ▲                        │
 │  r18-tools ────────┘ (덤프 갱신/스크레이퍼 설치, 1회성) │
 └─────────────────────────────────────────────┘

 

 

서비스 이미지 역할
stash-socks ghcr.io/mon-ius/docker-warp-socks:latest Stash 전용 WARP 프록시 (9091, HTTP/SOCKS5 겸용). 다른 용도의 warp-socks 컨테이너와 구분하기 위해 이름을 따로 씀
r18-postgres postgres:17-alpine R18.dev 덤프를 적재한 DB (r18)
stash stash-r18:latest (로컬 빌드) stashapp/stash:latest + py3-psycopg2, py3-pysocks
r18-tools postgres:17-alpine profiles: [tools] — up 으로는 안 뜨고 run 으로만 실행

 

파일 구조

/volume2/docker/stash/
├── docker-compose.yml
├── r18-update-cron.sh        # 작업 스케줄러용 정기 갱신 스크립트
├── stash/
│   └── Dockerfile            # Stash 이미지 확장
├── r18-tools/
│   └── r18.sh                # update / install-scraper
│
│   ── 아래는 실행 중 생성/사용되는 데이터 ──
├── config/                   # Stash 설정 (/root/.stash)
│   └── scrapers/R18dev_SQL/  # 스크레이퍼 설치 위치
├── metadata/  cache/  blobs/  generated/
├── r18-db/                   # PostgreSQL 데이터
├── r18-dumps/                # 받은 덤프 (r18-latest.sql.gz)
└── logs/                     # r18-update.log

미디어(e.g. aaa.mp4)는 /volume4/media → 컨테이너 /data (읽기 전용). 

 

2. 사전 준비

2-1. 권한

DSM에서는 일반 계정이 /var/run/docker.sock 에 접근할 수 없다. 모든 docker 명령은 sudo 로 실행한다.

permission denied while trying to connect to the Docker daemon socket

→ sudo 를 빼먹은 것.

 

2-2. 파일 배치

stash-r18.zip 을 /volume2/docker/stash/ 에 올리고 압축 해제. zip 안에 stash-r18/ 상위 폴더가 있으므로 내용물만 꺼낸다. (DSM에 unzip 이 없을 수 있어 7z 사용)

(7z사용이 어렵다면 winSCP와 같은 툴로 접근해서 ftp처럼 경로에 올리면 된다.)

cd /volume2/docker/stash
sudo 7z x stash-r18.zip -o/tmp/stash-r18 -y
sudo cp -r /tmp/stash-r18/stash-r18/. .
sudo rm -rf /tmp/stash-r18

 

docker-compose.yml, stash/Dockerfile, r18-tools/r18.sh, r18-update-cron.sh 가 모두 있는지 확인.

 

unable to prepare context: path "./stash" not found

→ stash/ 폴더(Dockerfile)를 빼먹은 것.

 

2-3. bind mount 폴더 생성

시놀로지 Docker는 없는 bind mount 경로를 자동 생성하지 않는다.

cd /volume2/docker/stash
sudo mkdir -p config/scrapers metadata cache blobs generated r18-db r18-dumps logs
Bind mount failed: '/volume2/docker/stash/r18-db' does not exist

→ 이 단계를 빼먹은 것.

 

2-4. 기존 stash 컨테이너가 있을 때

이름 충돌을 피하려면 기존 컨테이너가 어떻게 만들어졌는지 먼저 확인한다.

sudo docker inspect stash --format '{{ index .Config.Labels "com.docker.compose.project" }} / {{ index .Config.Labels "com.docker.compose.project.working_dir" }}'

 

결과 조치
stash / /volume2/docker/stash 같은 프로젝트. 그대로 up 하면 교체됨
비어 있음 / 다른 프로젝트 sudo docker stop stash && sudo docker rm stash 후 진행 (설정·DB는 bind mount라 유지됨). Container Manager의 다른 프로젝트였다면 그쪽도 정리

3. 설치

3-1. 기동

cd /volume2/docker/stash
sudo docker compose up -d --build --remove-orphans
sudo docker compose ps

stash-socks, r18-postgres 가 healthy 가 된 뒤 stash 가 뜬다. Stash UI: http://<NAS IP>:9999

 

WARP 동작 확인 (warp=on 이 보이면 정상):

sudo docker exec stash-socks curl -sx socks5h://127.0.0.1:9091 https://www.cloudflare.com/cdn-cgi/trace | grep warp

 

3-2. R18.dev DB 적재

덤프가 수 GB라 오래 걸린다. SSH가 끊겨도 계속되도록 스크립트로 실행하는 것을 권장.

cd /volume2/docker/stash
sudo -v                                   # 비밀번호 먼저 입력 (백그라운드에서 프롬프트에 막히지 않게)
sudo nohup bash r18-update-cron.sh >/dev/null 2>&1 &
tail -f logs/r18-update.log               # Ctrl+C 로 보기만 종료, 작업은 계속됨

 

정상 로그:

[r18] 덤프 확인: https://r18.dev/dumps/latest
[r18] 새 덤프 다운로드 완료 (x.xG)
[r18] 임시 DB(r18_new)에 적재 중…
[r18] r18 DB 교체
[r18] 완료 — derived_video N건

 

확인:

sudo docker exec r18-postgres psql -U postgres -d r18 -c "SELECT count(*) FROM derived_video;"

(2026-10 기준 약 191만 건)

 

3-3. 스크레이퍼 설치

sudo docker compose run --rm r18-tools install-scraper

 

install-scraper 가 하는 일:

  1. javstash/R18dev_SQL 저장소의 scrapers/R18dev_SQL/ 을 config/scrapers/R18dev_SQL/ 에 복사
  2. R18dev_SQL.py 수정
    • host="localhost" → host="r18-postgres"
    • password → R18_DB_PASSWORD (기본 postgres)
    • LANG='EN' (일본어: -e SCRAPER_LANG=JA 로 실행)
  3. 안전 실행 래퍼 R18dev_SQL_wrap.py 생성 + yml 이 래퍼를 호출하도록 수정
    • 원본은 결과가 없으면 아무것도 출력하지 않고 죽어서 Stash 가 could not unmarshal json from script output: EOF 를 냄
    • 래퍼는 이 경우 null / [] 을 출력해 '결과 없음' 으로 처리하고, 원인을 Stash 로그에 남김

설치 후 Stash: 설정 › Metadata Providers › Scrapers › Reload scrapers

스크레이퍼를 Stash UI(Available Scrapers)에서 다시 받으면 위 수정이 사라진다. 그럴 땐 install-scraper 를 다시 실행.

3-4. 스크레이퍼 동작 확인

sudo docker exec -i stash sh -c 'cd /root/.stash/scrapers/R18dev_SQL && echo "{\"code\":\"SSIS-001\"}" | python3 R18dev_SQL_wrap.py sceneByFragment' 2>/dev/null

 

"title": "After Abstaining ...", "performers": [{"name": "Donald Trump", ... 처럼 영문 JSON 이 나오면 정상.

 

4. 정기 덤프 갱신 (DSM 작업 스케줄러)

r18-update-cron.sh 동작:

  • 서버 덤프가 바뀌지 않았으면 다운로드·적재 없이 수 초 만에 종료 → 매일 실행 권장
  • r18_new 에 적재 → derived_video 검증 → 통과 시에만 r18 교체 (실패해도 기존 DB 유지)
  • flock 으로 중복 실행 방지, PATH 보정, docker compose run -T (TTY 없음)
  • 로그: logs/r18-update.log (최근 2000줄 유지)

등록

제어판 › 작업 스케줄러 › 생성 › 예약된 작업 › 사용자 정의 스크립트

탭 설정
일반 작업: R18 덤프 갱신 / 사용자: root
스케줄 매일, 04:00 등 한가한 시간
작업 설정 bash /volume2/docker/stash/r18-update-cron.sh
작업 설정 (선택) 실행 세부 정보 메일 → "비정상 종료 시에만"

 

등록 후 목록에서 선택 → 실행 으로 테스트.

강제 재적재:

cd /volume2/docker/stash
sudo FORCE=1 docker compose run --rm r18-tools update
교체 순간에는 덤프 + r18 +r18_new 가 동시에 존재하므로 /volume2 에 DB 크기 2배 이상 여유 공간이 필요하다.

5. Stash 에서 사용

5-1. 한 장면씩

장면 편집 › Scrape with… › JAVStash-R18dev_SQL. 파일명에 품번(ABCD-001 등)만 있으면 된다. 제목이 비어 있으면 Stash 가 파일명을 제목으로 넘긴다.

5-2. 일괄 수집·저장 (Identify)

Identify 는 위치가 두 군데이고 둘 다 숨겨져 있다.

위치 보이는 조건
설정 › Tasks › Library › Identify… 설정 화면 상단 Advanced Mode 를 켜야 보임. 전체 또는 경로 지정
Scenes 목록 › 우측 상단 ⋯ › Identify… 장면을 1개 이상 선택해야 보임

권장 옵션:

항목 설정
Sources JAVStash-R18dev_SQL 만 남김
Title / Details / Date / Code / Director Overwrite (기존 값 보존하려면 Ignore)
Performers / Tags Merge + Create missing
Studio Overwrite + Create missing
Set cover images 켬
Skip matches that have more than one result 켬
Set organised flag 필요 시

처음엔 장면 10개 정도로 시험 후 전체 실행.

결과를 눈으로 보며 저장하려면 Scenes 보기 방식을 Tagger 로 바꿔 소스를 R18dev_SQL 로 지정.


6. 문제 해결

증상 원인 / 조치
permission denied ... docker.sock sudo 누락
path "./stash" not found stash/Dockerfile 누락 → 2-2
Bind mount failed ... does not exist 폴더 미생성 → 2-3
could not unmarshal json from script output: EOF ① r18 DB 없음 → 3-2 ② 래퍼 미설치 → 3-3 다시 실행
database "r18" does not exist 덤프 적재 전이거나 실패 → logs/r18-update.log 확인 후 3-2
스크랩은 되는데 '결과 없음' Stash 로그(Debug)에서 확인: DB 에서 해당 작품을 찾지 못함 = R18.dev 에 없음 / 품번을 추출하지 못함 = 파일명·제목에 품번 없음
배우 이름이 일본어로 표시 아래 6-1

 

6-1. 배우 이름이 일본어로 나올 때

  1. 스크레이퍼 출력 확인 (3-4). 출력이 일본어면 LANG 이 JA → 수정:
   sudo sed -i "s/^LANG=.*/LANG='EN' # JA or EN/" config/scrapers/R18dev_SQL/R18dev_SQL.py

 

  1. 출력은 영문인데 Stash 에 일본어로 보이면 기존 배우와 매칭된 것. 예전에 LANG='JA' 로 스크랩하면 배우가 name=ドナルド・トランプ, aliases=Donald Trump로 생성되므로, 영문 이름이 별칭으로 매칭되어 일본어 이름이 표시된다. → Performers 에서 Name 을 영문으로, 일본어 이름을 Aliases 로 옮긴다.
  2. DB 에 영문 이름 자체가 없는지 확인:
   sudo docker exec r18-postgres psql -U postgres -d r18 -c "
   SELECT A.id, A.name_kanji, A.name_romaji, MT.target_en
   FROM derived_video_actress VA
   LEFT JOIN derived_actress A ON VA.actress_id = A.id
   LEFT JOIN machine_translation MT ON A.name_kanji = MT.source_ja
   WHERE VA.content_id = 'ssis00001';"

name_romaji, target_en 둘 다 비어 있으면 R18.dev 데이터에 영문명이 없는 것 (스크레이퍼는 일본어로 대체).


7. 유지보수

작업 명령
Stash 업데이트 sudo docker compose build --pull stash && sudo docker compose up -d (커스텀 이미지라 pull 만으로는 안 됨)
프록시·DB 이미지 업데이트 sudo docker compose pull stash-socks r18-postgres && sudo docker compose up -d
스크레이퍼 재설치 sudo docker compose run --rm r18-tools install-scraper
덤프 갱신 로그 tail -n 50 /volume2/docker/stash/logs/r18-update.log
전체 상태 sudo docker compose ps
컨테이너 로그 sudo docker compose logs -f stash

 

설정 메모

  • 프록시는 http://stash-socks:9091 로 지정 (warp-socks v8 은 같은 포트에서 HTTP 도 받음). socks5h:// 로 쓰면 파이썬 requests 에 PySocks 가 필요 — 이미지에 py3-pysocks 를 넣어 두었으므로 바꿔도 동작은 함.
  • NO_PROXY 에 내부 컨테이너와 사설 대역이 들어 있어 DB·LAN 통신은 프록시를 타지 않음.
  • 다른 기기에서 이 프록시를 쓰려면 stash-socks 의 ports 주석을 해제 (9092:9091, 기존 warp-socks 의 9091 과 겹치지 않게).
  • DB 비밀번호를 바꾸려면 docker-compose.yml 옆에 .env 를 만들고 R18_DB_PASSWORD=... 지정. DB 최초 생성 전에 정해야 하며, 바꾼 뒤 install-scraper 재실행.

참고

더보기

docker-compose.yml:

# Stash + Cloudflare WARP 프록시 + R18.dev PostgreSQL
# 배치 위치 예: /volume2/docker/stash/  (이 파일, stash/, r18-tools/ 를 같은 폴더에)
#
#   최초 기동 :  docker compose up -d --build
#   DB 덤프   :  docker compose run --rm r18-tools update
#   스크레이퍼:  docker compose run --rm r18-tools install-scraper

x-proxy-env: &proxy-env
  # warp-socks v8 은 9091 한 포트에서 HTTP/SOCKS5 를 같이 제공.
  # http:// 로 지정해야 스크레이퍼(파이썬 requests)도 별도 의존성 없이 프록시를 탄다.
  HTTP_PROXY: http://stash-socks:9091
  HTTPS_PROXY: http://stash-socks:9091
  http_proxy: http://stash-socks:9091
  https_proxy: http://stash-socks:9091
  NO_PROXY: &no-proxy localhost,127.0.0.1,stash-socks,r18-postgres,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12
  no_proxy: *no-proxy

services:
  stash-socks:
    image: ghcr.io/mon-ius/docker-warp-socks:latest
    container_name: stash-socks
    restart: unless-stopped
    # 다른 기기에서도 이 프록시를 쓰려면 아래 주석 해제 (호스트 포트가 다른 warp-socks 컨테이너와 겹치지 않게 조정)
    # ports:
    #   - "9092:9091"   # 기존 warp-socks(9091)와 겹치지 않게
    healthcheck:
      test: ["CMD", "curl", "-x", "socks5h://127.0.0.1:9091", "-fsSL", "https://www.cloudflare.com/cdn-cgi/trace"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 15s
    networks:
      - stash-net

  r18-postgres:
    image: postgres:17-alpine
    container_name: r18-postgres
    restart: unless-stopped
    shm_size: 256mb            # 덤프 import 중 인덱스 생성 시 shared memory 부족 방지
    environment:
      TZ: Asia/Seoul
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${R18_DB_PASSWORD:-postgres}
      POSTGRES_DB: postgres
    volumes:
      - /volume2/docker/stash/r18-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - stash-net

  stash:
    build: ./stash                 # stashapp/stash:latest + psycopg2/pysocks
    image: stash-r18:latest
    container_name: stash
    restart: unless-stopped
    ports:
      - "9999:9999"
    environment:
      <<: *proxy-env
      TZ: Asia/Seoul
      STASH_STASH: /data
      STASH_GENERATED: /generated
      STASH_METADATA: /metadata
      STASH_CACHE: /cache
      STASH_BLOBS: /blobs
    volumes:
      - /volume2/docker/stash/config:/root/.stash
      - /volume2/docker/stash/metadata:/metadata
      - /volume2/docker/stash/cache:/cache
      - /volume2/docker/stash/blobs:/blobs
      - /volume2/docker/stash/generated:/generated
      - /volume2/MEDIA:/data:ro
    depends_on:
      stash-socks:
        condition: service_healthy
      r18-postgres:
        condition: service_healthy
    networks:
      - stash-net

  # 1회성 작업용 (up 으로는 뜨지 않음): docker compose run --rm r18-tools <update|install-scraper>
  r18-tools:
    image: postgres:17-alpine
    profiles: ["tools"]
    entrypoint: ["sh", "/r18.sh"]
    command: ["update"]
    environment:
      <<: *proxy-env
      PGHOST: r18-postgres
      PGUSER: postgres
      PGPASSWORD: ${R18_DB_PASSWORD:-postgres}
      FORCE: ${FORCE:-0}
    volumes:
      - ./r18-tools/r18.sh:/r18.sh:ro
      - /volume2/docker/stash/r18-dumps:/dumps
      - /volume2/docker/stash/config/scrapers:/scrapers
    depends_on:
      stash-socks:
        condition: service_healthy
      r18-postgres:
        condition: service_healthy
    networks:
      - stash-net

networks:
  stash-net:
    driver: bridge

+ Recent posts