o2o-negosium-original/lps/Dockerfile.worker
민헌 9380760869 fix(lps): 네이버 컨테이너 차단 — 계측 CDP 토글 + 지문 판정 + 폰트 보강
운영 실측으로 범위가 좁혀졌다: 같은 컨테이너에서 **쿠팡은 60건 정상, 네이버만** 매 IP
첫 요청부터 wtm_captcha. kr_host 도 정상이다(ensure_ports 에 kr.decodo.com, 마커가 해외 IP
하드차단 '비정상적인 접근'(2.6KB)이 아니라 wtm_captcha(43~57KB)). IP·게이트웨이가 아니라
브라우저 지문이 남은 후보다.

**먼저 확인한 것 — 지문 위장은 이 스택에서 불가능하다(중요)**
WebGL 렌더러(컨테이너=SwiftShader)를 실기기 이름으로 덮으려 했으나 주입 경로가 둘 다 막혔다:
  context.add_init_script()  무반응. 단순 마커(window.__M__) 주입조차 undefined 다.
                             patchright 가 무력화한다 — 이 API 는 CDP
                             addScriptToEvaluateOnNewDocument 로 구현되고 그 흔적이 탐지
                             신호라, 걷어내는 게 patchright 의 존재 이유다.
  확장(MV3, world:MAIN, document_start)  로드는 되지만 값이 바뀌지 않았다.
→ 동작하지 않는 코드를 남기지 않는다. 스푸핑은 넣지 않고, 대신 **판정**만 한다.
   재시도 방지를 위해 이 실측을 fingerprint.py docstring 에 남겼다.

**넣은 것**
- services/search/fingerprint.py: 지문 판정(항목·정상여부·관측값·왜 문제인가). 진단이 곧
  '무엇을 고칠까'로 이어지도록 이유를 함께 낸다.
- net_meter 토글: 네이버에 계측용 CDP(Network.enable)를 붙이지 않는다(기본 off).
  네이버는 '요청 가로채기 자체가 탐지 신호'라 리소스 차단을 끈 이력이 있는데 계측 CDP 는
  그대로 붙고 있었다 — 같은 계열이다. [WorkerConfig].naver_net_meter 로 재빌드 없이 A/B
  (config 는 마운트). 끄면 대역폭이 DOM 크기 근사로 떨어질 뿐 크롤엔 영향 없다.
  쿠팡은 잘 통과하므로 기본값을 바꾸지 않는다.
- Dockerfile.worker: fonts-noto-cjk·fonts-liberation 추가(나눔 하나뿐이면 폰트 지문이 부자연스럽다).
- diag_naver.py 가 같은 판정 모듈을 쓰도록 정리.

통과 환경 기준값(맥 실측): 의심 항목 0개 · HTTP 200 · 6건 · 1.26MB.
테스트 6건 추가(통과 지문을 문제로 몰지 않는지·컨테이너 지문을 놓치지 않는지), 전체 262 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:22:01 +09:00

56 lines
3.8 KiB
Docker

# LPS 워커 이미지 — 크롤용 **헤드풀 Chromium + 가상 디스플레이(Xvfb)**.
# headless Chrome 은 Akamai(쿠팡)·Cloudflare Turnstile(G마켓/옥션)에 탐지돼 통과 못 함(실측 확인).
# → 컨테이너에선 Xvfb(가상 프레임버퍼)로 headful Chromium 을 실행한다.
#
# 빌드/실행:
# docker compose build lps-worker && docker compose up -d lps-worker
# 스텔스 참고: 로컬 Mac 은 실제 Chrome(channel=chrome), 컨테이너도 실제 google-chrome-stable(executable_path).
# 오픈소스 chromium 은 Akamai(쿠팡)가 지문으로 차단(실측: errors.edgesuite.net, IP 첫 요청부터) → 실제 Chrome 필수.
FROM python:3.14-slim
# 실제 Chrome(구글 apt 저장소, amd64 전용) + Xvfb + 한글 폰트
# 폰트를 넉넉히 깐다: 네이버는 한국 IP·ko-KR 로케일로 접근하는데 브라우저에 한글 폰트가
# 나눔 하나뿐이면 폰트 열거 지문이 부자연스럽다. noto-cjk(한중일)·liberation(서구권 표준)까지
# 넣어 실사용 데스크톱에 가깝게 맞춘다(이미지 ~100MB 증가, 크롤 성공률과 맞바꿀 가치가 있다).
RUN apt-get update && apt-get install -y --no-install-recommends wget gnupg \
&& wget -qO- https://dl.google.com/linux/linux_signing_key.pub \
| gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" \
> /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update && apt-get install -y --no-install-recommends \
google-chrome-stable xvfb xauth ca-certificates \
fonts-nanum fonts-noto-cjk fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# 시크릿 든 config.local.toml 은 .dockerignore 로 제외됨 → example(플레이스홀더)로 대체.
# 실값은 compose env 로 주입: DB_*, OPENAI_API_KEY, DECODO_*(포트 포함), NAVER_KEYS.
RUN cp config/config.local.toml.example config/config.local.toml
# Chrome 경로는 [WorkerConfig].chrome_executable(설정 파일)이 유일한 소스다.
# 예전에 LPS_CHROME_EXECUTABLE env 를 뒀지만 읽는 코드가 없어 '설정한 줄 알았는데 아니었다'는
# 오진을 만들었다(2026-07-28 배포서버 크롤 조사) — 죽은 env 는 두지 않는다.
# 컨테이너에서는 config 에 "/opt/google/chrome/chrome" 을 넣어야 --no-sandbox 가 함께 붙는다.
ENV APP_ENV=local \
PYTHONUNBUFFERED=1 \
DISPLAY=:99 \
LPS_HEARTBEAT_FILE=/tmp/lps_worker_heartbeat
# 하트비트 신선도(<120s)로 행/좀비 워커 감지. start-period 는 웜업(브라우저 기동) 여유.
HEALTHCHECK --interval=30s --timeout=8s --start-period=120s --retries=3 \
CMD python -c "import os,time,sys; p=os.environ['LPS_HEARTBEAT_FILE']; sys.exit(0 if os.path.exists(p) and time.time()-os.path.getmtime(p)<120 else 1)"
# Xvfb(가상 디스플레이)를 백그라운드로 띄우고 python 을 exec 로 승계 실행.
# → 헤드풀 Chromium 이 :99 에 뜨고, 워커 로그는 그대로 docker logs 로 나온다(xvfb-run 은 로그를 삼킴).
#
# stale lock 제거가 먼저다(2026-08-04): `docker compose start`(재시작)는 컨테이너 파일시스템을
# 그대로 재사용하므로 지난 실행의 /tmp/.X99-lock·/tmp/.X11-unix/X99 가 남는다. Xvfb 는 이걸
# '이미 켜진 디스플레이'로 보고 죽고 → DISPLAY 없이 headful Chrome 이 못 떠 **모든 크롤이 조용히 실패**한다
# (증상: 워커는 살아있는데 launch_persistent_context 가 'Missing X server or $DISPLAY' 로 실패).
CMD ["bash", "-c", "rm -f /tmp/.X99-lock /tmp/.X11-unix/X99; Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp >/dev/null 2>&1 & sleep 1; exec python worker_main.py"]