n8n YouTube 자막(Transcript) 추출

n8n YouTube 자막(Transcript) 추출 – 성공 구성 정리

환경: ① Dockge + Docker Compose, ② Home Assistant OS(HAOS) 로컬 애드온(Apps) — 둘 다 linux/arm64 (musl/Alpine) 기반 사용 노드: n8n-nodes-youtube-dl (yt-dlp 기반) 최종 확인일: 2026-08-04


문제 요약

n8n 공식 이미지(n8nio/n8n:next)가 v2.x부터 distroless(패키지 매니저 없음) 로 바뀌면서, YouTube 자막 추출에 필요한 의존성(Python, yt-dlp, JS 런타임)을 설치하기 위해 커스텀 Dockerfile이 필요했다. 추가로 아래 4개 문제가 순차적으로 겹쳐 있었다.

  1. apk 부재 — n8n v2.x 이미지에 패키지 매니저 자체가 없음
  2. musl/glibc 비호환 — yt-dlp의 PyInstaller 바이너리가 Alpine(musl)에서 실행 불가
  3. YouTube bot 차단 — 서버 IP에서의 요청을 봇으로 의심 (“Sign in to confirm you’re not a bot”)
  4. YouTube SABR/EJS 챌린지 — 2026년 YouTube가 도입한 JS 챌린지 인증, JS 런타임(deno) + 챌린지 solver 스크립트 필요

최종 Dockerfile

FROM alpine:latest AS alpine

FROM docker.n8n.io/n8nio/n8n:next
USER root

# n8n v2.x 이미지에는 apk가 없으므로 Alpine 이미지에서 바이너리를 복사해온다
COPY --from=alpine /sbin/apk /sbin/apk
COPY --from=alpine /usr/lib/libapk.so* /usr/lib/
COPY --from=alpine /etc/apk /etc/apk
COPY --from=alpine /lib/apk /lib/apk

RUN apk update && \
    # yt-dlp는 PyInstaller 바이너리 대신 pip 순수 Python 버전을 사용 (musl 호환 문제 회피)
    apk add --no-cache python3 py3-pip && \
    pip install --break-system-packages yt-dlp && \
    # deno는 apk edge 저장소의 musl 네이티브 빌드를 사용 (curl 설치 스크립트는 glibc 바이너리라 실패함)
    apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/edge/community deno

USER node

왜 이렇게 구성했는가

구성 요소방식이유
apkAlpine 이미지에서 바이너리 복사n8n v2.x 이미지는 패키지 매니저가 완전히 제거된 distroless 구조
yt-dlppip install (PyInstaller 바이너리 X)standalone 바이너리는 glibc 기반이라 musl(Alpine)에서 dladdr1: symbol not found 등으로 실행 실패. gcompat으로도 완전히 해결 안 됨
denoapk add --repository .../edge/community denocurl -fsSL https://deno.land/install.sh 스크립트로 받는 바이너리는 glibc용이라 Alpine에서 실행 불가 (__res_init: symbol not found). Alpine edge 저장소의 musl 네이티브 빌드를 써야 함

최종 compose.yaml

services:
  n8n:
    build:
      context: .
      dockerfile: Dockerfile
    restart: always
    ports:
      - 5678:5678
    environment:
      - N8N_HOST=n8n-w.duckdns.org
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - N8N_SECURE_COOKIE=false
      - N8N_DEFAULT_LOCALE=ko
      - N8N_BASIC_AUTH_PROXY=true
      - N8N_ENABLED_MODULES=chat-hub
      - NODE_ENV=production
      - WEBHOOK_URL=https://n8n-w.duckdns.org/
      - GENERIC_TIMEZONE=Asia/Seoul
      - N8N_ENCRYPTION_KEY=tthGpOU6HiiJAaCc13D5VNmi31yC1BLM
      - YT_DLP_PATH=/usr/bin/yt-dlp
    volumes:
      - /opt/n8n/node:/home/node
      - /opt/n8n/files:/files
      - /opt/n8n/cookies:/home/node/cookies   # CLI 디버깅용 (노드 자체는 아래 Credential을 사용)
networks: {}

/opt/n8n/cookies 볼륨은 노드 동작에는 필수가 아니며, CLI(docker exec)로 직접 yt-dlp를 테스트/디버깅할 때 사용한다. 실제 워크플로우는 아래 YouTube Cookies Credential을 사용한다.


HAOS 로컬 애드온(Apps) 버전 구성

Dockge/Compose와 별개로, Home Assistant OS의 로컬 애드온으로도 동일한 n8n(+ YouTube 자막 기능)을 운영 중이라면 아래처럼 구성한다. HAOS 애드온은 config.yaml이 compose.yaml 역할을, Supervisor가 docker-compose/Dockge 역할을 대신한다.

config.yaml

name: "n8n"
version: "2.34.0"
slug: "n8n"
description: "Workflow automation tool (n8n) running as a Home Assistant App (with YouTube transcript support)"
url: "https://github.com/n8n-io/n8n"
arch:
  - amd64
  - aarch64

# image 키를 쓰지 않는다 -> Supervisor가 같은 폴더의 Dockerfile로 직접 빌드하도록 강제
# (image 키가 있으면 Supervisor는 사전 빌드 이미지를 그냥 pull만 하고 Dockerfile은 무시함)

startup: services
boot: auto

webui: "http://[HOST]:[PORT:5678]/"

ports:
  5678/tcp: 5678
ports_description:
  5678/tcp: "n8n web UI / webhook 포트"

map:
  - type: share
    read_only: false
    path: /files
  - type: addon_config
    read_only: false
    path: /home/node
    # 쿠키 파일이 필요하면 이 볼륨 안에 cookies/cookies.txt 로 두면 됨
    # (호스트 경로: addon_configs/<slug>/cookies/cookies.txt)

environment:
  N8N_HOST: "w-n8n.duckdns.org"
  N8N_PORT: "5678"
  N8N_PROTOCOL: "https"
  N8N_PROXY_HOPS: "1"
  N8N_SECURE_COOKIE: "false"
  N8N_DEFAULT_LOCALE: "ko"
  N8N_BASIC_AUTH_PROXY: "true"
  N8N_ENABLED_MODULES: "chat-hub"
  NODE_ENV: "production"
  WEBHOOK_URL: "https://w-n8n.duckdns.org/"
  GENERIC_TIMEZONE: "Asia/Seoul"
  N8N_ENCRYPTION_KEY: "tthGpOU6HiiJAaCc13D5VNmi31yC1BLM"
  YT_DLP_PATH: "/usr/bin/yt-dlp"

options: {}
schema: {}

Dockerfile (config.yaml과 같은 폴더에 위치)

ARG BUILD_VERSION=latest
FROM alpine:latest AS alpine

# 주의: docker.n8n.io가 아닌 Docker Hub(docker.io)를 사용한다 (아래 트러블슈팅 참고)
FROM docker.io/n8nio/n8n:${BUILD_VERSION}
USER root

COPY --from=alpine /sbin/apk /sbin/apk
COPY --from=alpine /usr/lib/libapk.so* /usr/lib/
COPY --from=alpine /etc/apk /etc/apk
COPY --from=alpine /lib/apk /lib/apk

RUN apk update && \
    apk add --no-cache python3 py3-pip && \
    pip install --break-system-packages --no-cache-dir yt-dlp && \
    apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/edge/community deno && \
    rm -rf /var/cache/apk/* /tmp/* /root/.cache

USER node

핵심 포인트

항목설명
image: 키 제거있으면 Supervisor가 Dockerfile을 무시하고 사전 빌드 이미지를 그대로 pull함. 커스텀 빌드를 쓰려면 반드시 제거
ARG BUILD_VERSIONSupervisor가 빌드 시 config.yaml의 version 값을 --build-arg BUILD_VERSION=...으로 자동 주입. FROM ...${BUILD_VERSION}으로 받으면 config.yaml의 version만 올려도 자동 반영
ARG BUILD_VERSION=latest (기본값)기본값을 지정하지 않으면 InvalidDefaultArgInFrom 경고가 뜸 (치명적이진 않음). Supervisor가 항상 실제 값을 주입하므로 latest는 실사용되지 않는 안전한 placeholder
FROM docker.io/... (Docker Hub)docker.n8n.io(n8n 자체 레지스트리)가 아닌 Docker Hub를 사용. 아래 트러블슈팅 참고
설치+정리를 한 RUN 레이어에rm -rf ...를 별도 RUN으로 나누면 레이어가 분리되어 이미지 용량이 줄지 않음. 설치와 캐시 정리를 같은 레이어에서 함께 처리해야 실제로 용량 절감 효과가 있음

⚠️ 트러블슈팅: docker.n8n.io 429 Too Many Requests

증상

ERROR: unexpected status from HEAD request to https://docker.n8n.io/v2/n8nio/n8n/manifests/2.34.0: 429 Too Many Requests

원인: docker.n8n.io는 n8n이 자체 운영하는 이미지 배포처로 Docker Hub와는 별개 인프라다. 여러 버전(2.33.3, 2.34.0 등)으로 바꿔서 시도해도 동일하게 실패했다면, 특정 버전/최초 시도 여부와 무관하게 이 레지스트리(또는 앞단 CDN)가 해당 요청을 rate limit으로 막고 있는 상태로 판단할 수 있다. Supervisor가 빌드 시 --pull 옵션으로 매번 메타데이터를 새로 조회하는데, 이 조회 자체가 막혀 있었다.

해결: Dockerfile의 FROM 대상을 docker.n8n.io/n8nio/n8n:${BUILD_VERSION}docker.io/n8nio/n8n:${BUILD_VERSION}(Docker Hub)로 바꾸는 것만으로 즉시 해결됨. 같은 이미지의 다른 배포처를 쓰는 것이므로 다른 부작용은 없다.

참고: Docker Hub도 비로그인 pull은 IP당 시간당 100회 제한이 있으므로, 너무 잦은 rebuild는 이쪽에서도 429가 날 수 있다.

애드온 버전 업그레이드 절차

  1. config.yaml의 version 값만 새 버전으로 수정 (예: "2.34.0""2.35.0")
  2. Supervisor → 애드온 → Rebuild
  3. Dockerfile은 그대로 두면 됨 (${BUILD_VERSION}이 자동으로 새 버전을 참조)

n8n 노드 설정 (YouTube Downloader / Get Transcript)

Credential: YouTube Cookies

  1. 브라우저 확장 프로그램 EditThisCookie (V3) 설치
  2. YouTube에 로그인한 상태에서 youtube.com 쿠키를 JSON 형식으로 Export (클립보드 복사)
  3. n8n → Credentials → Add Credential → YouTube Cookies
  4. 복사한 JSON을 붙여넣고 저장

Custom yt-dlp Flags

노드 파라미터의 Custom yt-dlp Flags 필드에 아래 값을 입력:

--remote-components ejs:github
  • YouTube의 SABR 스트리밍 강제 전환에 대응하기 위해, yt-dlp가 JS 챌린지 solver 스크립트(EJS)를 GitHub에서 자동으로 받아오도록 지정하는 옵션
  • deno 런타임은 Dockerfile에서 이미 설치되어 있으므로, 이 플래그만 추가하면 챌린지가 자동으로 풀림

문제 해결 시 확인 순서 (체크리스트)

향후 n8n/yt-dlp 업데이트 후 자막 추출이 다시 안 될 경우, 아래 순서로 원인을 좁혀간다.

  1. yt-dlp 실행 자체가 되는가?


    docker exec -it n8n-n8n-1 yt-dlp --version

    버전이 안 뜨면 → Dockerfile의 pip 설치 단계 확인


  2. deno가 정상 실행되는가?


    docker exec -it n8n-n8n-1 deno --version

    Permission denied, symbol not found 등이 뜨면 → musl 네이티브 빌드가 아닌 바이너리가 설치된 것. apk edge 저장소 방식으로 재설치


  3. CLI에서 쿠키 포함 자막 추출이 되는가? (노드를 거치지 않고 직접 확인)

    docker exec -it n8n-n8n-1 yt-dlp \
    --skip-download --write-auto-sub --sub-lang en \
    --cookies /home/node/cookies/cookies.txt \
    --remote-components ejs:github \
    -o /tmp/test "https://www.youtube.com/watch?v=VIDEO_ID"
    • Sign in to confirm you're not a bot → 쿠키 만료. 재로그인 후 쿠키 재발급 필요
    • Requested format is not available → EJS/deno 관련 문제. --remote-components ejs:github 플래그 확인
    • 정상적으로 .vtt 파일이 다운로드되면 → yt-dlp/서버 쪽은 정상. 노드 쪽 파라미터(Custom Flags, Credential 연결 여부) 확인
  4. 위 3번이 성공하는데 n8n 노드 실행만 실패한다면

    • 노드의 Custom yt-dlp Flags--remote-components ejs:github가 들어있는지 확인
    • YouTube Cookies Credential이 워크플로우 노드에 실제로 선택(연결)되어 있는지 확인
    • Credential의 쿠키가 최신인지 확인 (재로그인 후 재발급)

쿠키 만료 시 재설정 절차 (요약)

증상: Sign in to confirm you're not a bot 에러 재발생

  1. 브라우저에서 YouTube 재로그인
  2. EditThisCookie (V3) → Export (JSON, 클립보드 복사)
  3. n8n → Credentials → YouTube Cookies → 기존 값 삭제 후 붙여넣기 → Save
  4. 워크플로우 재실행

참고: 구글 계정 보안상 세션 로그아웃을 하면 기존에 내보낸 모든 쿠키(파일 포함)가 즉시 무효화된다. 쿠키 파일이나 JSON은 채팅/외부 서비스에 절대 업로드하지 말고, 로컬 → 서버로 직접 전송(scp) 또는 n8n 화면에 직접 붙여넣기만 할 것.


참고 링크