본문 바로가기
  • AI 시대에 적응하는 현대인을 위한 지식 공간
  • AI를 위한 데이터를 과학으로 역어본다
AI 활용

n8n에서 Python Code노드 실행하기 (2/2) - External Mode

by 피크나인 2026. 1. 12.

n8n 2.0 Python Task Runner 완벽 가이드 -  External Mode로 프로덕션 환경 구축하기

n8n 2.0 버전부터 Python Code 노드 실행 방식이 크게 변경되었습니다. 이전에는 n8n이 설치된 환경에 Python만 있으면 바로 사용할 수 있었지만, 이제는 Task Runner라는 별도의 실행 환경이 필요합니다. 이 글에서는 왜 이런 변화가 생겼는지, 그리고 프로덕션 환경에서 권장되는 External Mode를 어떻게 설정하는지 상세히 알아보겠습니다. 특히 Docker Compose를 사용하는 Queue Mode 환경에서의 설정 방법을 중심으로 설명드리겠습니다.

n8n 2.0의 Task Runner 아키텍처 - Main 인스턴스와 분리된 Python 실행 환경
n8n 2.0의 Task Runner 아키텍처 - Main 인스턴스와 분리된 Python 실행 환경



1. Task Runner란 무엇인가

Task Runner는 n8n 2.0에서 도입된 코드 실행 격리 시스템입니다. Code 노드(JavaScript, Python)에서 작성한 코드가 직접 n8n 프로세스 내부에서 실행되지 않고, 별도의 격리된 환경에서 실행되도록 설계되었습니다. 이러한 구조 변경은 보안과 안정성을 크게 향상시키며, 악의적인 코드가 n8n 시스템 전체에 영향을 미치는 것을 방지합니다.

 

n8n 2.0 이전에는 Code 노드의 코드가 n8n 메인 프로세스 내에서 직접 실행되었습니다. 이는 구현이 단순하다는 장점이 있었지만, 보안 취약점이 될 수 있었습니다. 예를 들어, 악의적인 코드가 파일 시스템에 접근하거나, 환경 변수를 읽거나, 심지어 n8n 프로세스 자체를 조작할 수 있는 가능성이 있었습니다. Task Runner는 이러한 위험을 원천적으로 차단합니다.

 

Task Runner의 핵심 개념은 Task Broker와 Runner의 분리입니다. Task Broker는 n8n 메인 프로세스 내에서 동작하며 코드 실행 요청을 관리합니다. Runner는 실제로 코드를 실행하는 별도의 프로세스 또는 컨테이너입니다. 이 둘은 내부 네트워크를 통해 통신하며, 보안 토큰으로 인증됩니다.

 

현재 n8n은 JavaScript Task Runner와 Python Task Runner 두 가지를 지원합니다. JavaScript Runner는 n8n에 기본 내장되어 있어 별도 설정 없이 사용 가능합니다. 하지만 Python Runner는 별도로 설정해야 하며, 이것이 많은 분들이 "Python runner unavailable" 에러를 만나는 이유입니다.

sequenceDiagram
    participant TM as Task Requester
    participant TB as Task Broker
    participant L as Launcher
    participant TR as Task Runner

    %% Initial launcher connection
    L->>+TB: WS connect
    TB-->>L: broker:info-request
    L->>TB: runner:info
    TB-->>L: broker:runner-registered

    %% Task request flow
    L->>TB: runner:task-offer
    TM->>+TB: requester:task-request
    TB-->>L: broker:task-offer-accept
    L->>TB: runner:task-deferred

    %% Launcher executes and transitions to runner
    Note over L: exec.Command()
    %% New runner connection
    TR->>+TB: WS connect
    TB-->>TR: broker:info-request
    TR->>TB: runner:info
    TB-->>TR: broker:runner-registered
    TR->>TB: runner:task-offer
    TB-->>TR: broker:task-offer-accept
    TB-->>TM: broker:task-ready

    %% Task settings flow
    TM->>+TB: requester:task-settings
    TB-->>TR: broker:task-settings
    TR->>TB: runner:task-done
    TB-->>TM: broker:task-done
    TR->>TR: exit if idle
Task Runner는 Code 노드에서 Javascript와 Python코드를 실행하기 위한 최소한의 자원을 활용하는 구조입니다.

2. 왜 Task Runner가 필요해졌는가

n8n 2.0에서 Task Runner 시스템이 도입된 배경에는 여러 가지 중요한 이유가 있습니다. 가장 핵심적인 이유는 보안 강화입니다. 워크플로우 자동화 플랫폼은 다양한 사용자가 코드를 작성하고 실행하는 환경이므로, 코드 실행의 격리는 필수적인 보안 요구사항이 되었습니다.

  • 첫 번째 이유는 프로세스 격리를 통한 보안 강화입니다. Code 노드에서 실행되는 코드가 n8n 메인 프로세스와 분리되면, 악의적인 코드가 시스템에 미칠 수 있는 영향이 제한됩니다. 예를 들어, 무한 루프나 메모리 누수가 발생해도 Runner만 영향을 받고 n8n 전체가 다운되지 않습니다. 파일 시스템 접근, 네트워크 접근, 환경 변수 접근 등도 Runner 컨테이너 수준에서 제어할 수 있습니다.
  • 두 번째 이유는 리소스 관리의 효율성입니다. Runner를 별도 컨테이너로 분리하면 CPU, 메모리 등의 리소스를 독립적으로 할당하고 제한할 수 있습니다. 이는 특히 무거운 Python 연산을 수행할 때 유용하며, n8n 메인 인스턴스의 응답성을 유지하는 데 도움이 됩니다. Docker의 리소스 제한 기능을 활용하여 Runner가 사용할 수 있는 최대 리소스를 명시적으로 설정할 수 있습니다.
  • 세 번째 이유는 확장성 향상입니다. Runner를 별도 서비스로 분리하면 필요에 따라 여러 Runner 인스턴스를 띄워 부하를 분산할 수 있습니다. 이는 Queue Mode 환경에서 특히 유용하며, 대규모 워크플로우 실행 시 병렬 처리 능력을 크게 향상시킵니다. 각 Runner 인스턴스는 독립적으로 스케일 아웃이 가능합니다.
  • 네 번째 이유는 의존성 관리의 단순화입니다. Python 환경의 의존성 관리는 복잡할 수 있습니다. Runner를 별도 이미지로 관리하면 필요한 Python 패키지를 미리 설치하고 버전을 고정할 수 있습니다. n8n 업그레이드와 Python 환경 업그레이드를 독립적으로 수행할 수 있어 운영이 편리해집니다.

3. Internal Mode vs External Mode

n8n Task Runner는 두 가지 모드로 운영할 수 있습니다. 각 모드의 특성을 이해하면 환경에 맞는 적절한 선택을 할 수 있습니다. 공식 문서에서는 프로덕션 환경에서 External Mode 사용을 강력히 권장하고 있습니다.

Internal Mode

Internal Mode는 n8n 프로세스 내부에서 Runner를 실행하는 방식입니다. 설정이 간단하다는 장점이 있지만, 프로덕션 환경에서는 권장되지 않습니다. 이 모드를 사용하려면 n8n이 설치된 환경에 Python과 가상환경(venv)이 특정 경로에 존재해야 합니다.

 

Internal Mode의 가장 큰 문제는 n8n 공식 Docker 이미지에 Python이 포함되어 있지 않다는 것입니다. n8n은 2024년 후반부터 Docker Hardened Images를 사용하기 시작했는데, 이 이미지는 보안을 위해 패키지 매니저(apk, apt 등)가 제거되어 있습니다. 따라서 공식 이미지를 그대로 사용하면서 Python을 추가하는 것이 불가능합니다.

 

Internal Mode를 사용하려면 커스텀 Docker 이미지를 빌드해야 합니다. 이 과정에서 Node.js 버전 호환성, Python 가상환경 경로 설정, 환경 변수 구성 등 여러 복잡한 요소를 고려해야 합니다. 단순히 Python을 설치하는 것만으로는 부족하며, n8n이 기대하는 특정 경로에 가상환경을 생성해야 합니다.

External Mode

External Mode는 Runner를 별도의 컨테이너(또는 프로세스)로 실행하는 방식입니다. n8n 공식에서 제공하는 n8nio/runners 이미지를 사용하며, n8n 메인 인스턴스와는 네트워크를 통해 통신합니다. 이 방식은 설정이 조금 더 복잡하지만, 운영 측면에서 여러 장점이 있습니다.

  • External Mode의 첫 번째 장점은 공식 지원입니다. Anthropic에서 제공하는 공식 이미지를 사용하므로 호환성과 안정성이 보장됩니다. n8n 버전 업그레이드 시에도 Runner 이미지만 함께 업그레이드하면 되므로 관리가 편리합니다. 커뮤니티에서 발생하는 이슈도 공식적으로 지원받을 수 있습니다.
  • 두 번째 장점은 완벽한 격리입니다. Runner가 별도 컨테이너에서 실행되므로 보안과 안정성 측면에서 가장 이상적인 구성입니다. 컨테이너 수준에서 네트워크 접근, 볼륨 마운트, 리소스 제한 등을 세밀하게 제어할 수 있습니다. 문제가 발생해도 Runner 컨테이너만 재시작하면 됩니다.
  • 세 번째 장점은 독립적인 스케일링입니다. Runner 컨테이너를 필요에 따라 여러 개 띄울 수 있어 코드 실행 부하를 분산할 수 있습니다. Queue Mode 환경에서 Worker와 함께 스케일 아웃하여 대규모 워크플로우 처리가 가능합니다.
비교항목 Internal Mode External Mode
설정 복잡도 높음 (커스텀 이미지 필요) 중간 (컨테이너 추가)
보안 수준 중간 높음 (완벽한 격리)
공식 지원 제한적 완전 지원
스케일링 불가 가능
리소스 제어 제한적 세밀한 제어 가능
프로덕션 권장 아니오

4. External Mode 설정하기

External Mode를 설정하려면 n8n 인스턴스의 환경 변수 설정과 별도의 Runner 컨테이너 실행이 필요합니다. 먼저 단일 인스턴스(Main only) 환경에서의 기본 설정을 살펴본 후, Queue Mode 환경으로 확장하겠습니다.

프로젝트 디렉토리 구조

프로젝트를 체계적으로 관리하기 위해 다음과 같은 디렉토리 구조를 권장합니다. 이 구조는 설정 파일과 커스텀 이미지 빌드 파일을 분리하여 유지보수를 용이하게 합니다.

n8n-stack/
  +-- docker-compose.yml           # 전체 서비스 정의
  +-- .env                         # 환경 변수
  +-- runners-config/
  |     +-- n8n-task-runners.json  # Runner 보안 설정
  +-- runners-custom/
  |     +-- Dockerfile             # 커스텀 Runner 이미지 (crawl4ai등 3rd Party SDK 포함)
  +-- data/
        +-- n8n/                   # n8n 데이터 볼륨

환경 변수 파일 (.env)

먼저 .env 파일에 필요한 환경 변수들을 정의합니다. 보안 토큰은 충분히 긴 랜덤 문자열을 사용해야 합니다. 이 토큰은 n8n과 Runner 간의 인증에 사용되므로 외부에 노출되지 않도록 주의해야 합니다.

openssl rand -base64 32
# .env

# === 기본 설정 ===
TZ=Asia/Seoul
N8N_HOST=n8n.example.com
WEBHOOK_URL=https://n8n.example.com/

# === 데이터베이스 설정 ===
DB_HOST=your-db-host
DB_PORT=5432
DB_NAME=n8n
DB_USER=n8n
DB_PASSWORD=your-secure-password

# === Redis 설정 (Queue Mode 필수) ===
REDIS_HOST=your-redis-host
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password

# === 암호화 키 ===
N8N_ENCRYPTION_KEY=your-encryption-key-min-24-chars

# === Task Runner 설정 ===
# 보안 토큰: openssl rand -hex 32 명령으로 생성 권장
N8N_RUNNERS_AUTH_TOKEN=your-very-long-random-token-at-least-32-chars

단일 인스턴스 docker-compose.yml

가장 기본적인 External Mode 설정입니다. n8n 메인 인스턴스와 Runner 컨테이너를 함께 실행합니다. 이 구성은 단일 서버에서 n8n을 운영할 때 적합합니다.

# docker-compose.yml (단일 인스턴스)

version: '3.8'

services:
  # ===========================================
  # n8n Main Instance
  # ===========================================
  n8n:
    image: n8nio/n8n:latest
    container_name: n8n-main
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      # 기본 설정
      - TZ=${TZ}
      - GENERIC_TIMEZONE=${TZ}
      - N8N_HOST=${N8N_HOST}
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=${WEBHOOK_URL}
      
      # 데이터베이스 설정
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=${DB_HOST}
      - DB_POSTGRESDB_PORT=${DB_PORT}
      - DB_POSTGRESDB_DATABASE=${DB_NAME}
      - DB_POSTGRESDB_USER=${DB_USER}
      - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}
      
      # 암호화 키
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      
      # ===========================================
      # Task Runner 설정 (External Mode)
      # ===========================================
      - N8N_RUNNERS_MODE=external
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
      - N8N_RUNNERS_ENABLED=true
      
    volumes:
      - ./data/n8n:/home/node/.n8n
    networks:
      - n8n-network
    healthcheck:
      test: ["CMD-SHELL", "wget --spider -q http://localhost:5678/healthz || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

  # ===========================================
  # Task Runner (Python + JavaScript)
  # ===========================================
  n8n-runners:
    image: n8nio/runners:latest
    container_name: n8n-runners
    restart: unless-stopped
    environment:
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
      - N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
    volumes:
      - ./runners-config/n8n-task-runners.json:/etc/n8n-task-runners.json:ro
    networks:
      - n8n-network
    depends_on:
      n8n:
        condition: service_healthy

networks:
  n8n-network:
    driver: bridge

 

핵심 환경 변수에 대해 설명드리겠습니다.

  • N8N_RUNNERS_MODE=external은 External Mode를 활성화합니다.
  • N8N_RUNNERS_AUTH_TOKEN은 n8n과 Runner 간 인증에 사용되며, 양쪽에 동일한 값을 설정해야 합니다.
  • N8N_RUNNERS_TASK_BROKER_URI는 Runner가 n8n의 Task Broker에 연결할 주소입니다. n8n의 Task Broker는 기본적으로 포트 5679에서 동작합니다.

5. Queue Mode 환경에서의 설정

대규모 워크플로우를 처리하거나 고가용성이 필요한 환경에서는 Queue Mode를 사용합니다. Queue Mode에서는 Main 인스턴스가 UI와 API를 담당하고, Worker 인스턴스가 실제 워크플로우를 실행합니다. 이 구성에서는 Main과 Worker 각각에 연결된 Runner가 필요합니다.

Queue Mode 아키텍처 이해

Queue Mode에서 코드 노드의 실행 흐름을 이해하는 것이 중요합니다. 워크플로우 실행 요청이 들어오면 Main 인스턴스가 이를 Redis 큐에 등록합니다. Worker 인스턴스가 큐에서 작업을 가져와 실행합니다. 워크플로우에 Python Code 노드가 있다면, Worker에 연결된 Runner가 해당 코드를 실행합니다.

따라서 Main에서 수동 테스트 실행 시에는 Main용 Runner가, Worker에서 실제 워크플로우 실행 시에는 Worker용 Runner가 각각 동작합니다. 이를 위해 두 개의 Runner 컨테이너가 필요하며, 각각 다른 Task Broker URI를 바라봐야 합니다.

커스텀 Runner 이미지 사전 빌드

Queue Mode에서는 커스텀 Runner 이미지를 미리 빌드해 두는 것이 효율적입니다. docker-compose.yml에서 빌드 컨텍스트를 지정하면 서비스 시작 시마다 빌드가 수행될 수 있어 시간이 오래 걸립니다. 대신 이미지를 미리 빌드하고 태그를 지정하여 사용합니다.

먼저 기본 Runner 이미지를 위한 Dockerfile을 작성합니다. 이 이미지는 기본 n8nio/runners 이미지와 동일하지만, 필요에 따라 추가 패키지를 설치할 수 있습니다.

# runners-custom/Dockerfile.basic
# 기본 Runner 이미지 (추가 패키지 없음)
FROM n8nio/runners:latest

# 필요한 경우 기본 Python 패키지 추가
# RUN pip install --no-cache-dir pandas requests

# 기본 설정 유지
USER node

 

이미지를 빌드하고 태그를 지정합니다.

# 기본 Runner 이미지 빌드
cd n8n-stack

# 간단하게 이미지 빌드 : 현재 폴더의 Dockerfile을 참조하여 n8n-runner-custom:latest이미지 생성
docker built -t n8n-runner-custom:latest .

# 이미지 빌드 및 태그 지정, 이미지가 변경되는 경우는 --no-cache를 이용해 새로 생성하는것이 좋음
docker build -t n8n-runners-custom:latest -f runners-custom/Dockerfile.basic runners-custom/

# 빌드된 이미지 확인
docker images | grep n8n-runners-custom

Queue Mode docker-compose.yml

다음은 Queue Mode 환경의 전체 docker-compose.yml입니다. Main 인스턴스, Worker 인스턴스, 그리고 각각에 연결된 Runner 컨테이너를 정의합니다. 사전 빌드된 커스텀 이미지를 지정하여 사용합니다.

# docker-compose.yml (Queue Mode)

version: '3.8'

services:
  # ===========================================
  # n8n Main Instance
  # 역할: UI 제공, API 처리, Task Broker 운영
  # ===========================================
  n8n-main:
    image: n8nio/n8n:latest
    container_name: n8n-main
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      # 기본 설정
      - TZ=${TZ}
      - GENERIC_TIMEZONE=${TZ}
      - N8N_HOST=${N8N_HOST}
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=${WEBHOOK_URL}
      
      # 데이터베이스 설정
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=${DB_HOST}
      - DB_POSTGRESDB_PORT=${DB_PORT}
      - DB_POSTGRESDB_DATABASE=${DB_NAME}
      - DB_POSTGRESDB_USER=${DB_USER}
      - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}
      
      # Queue Mode 설정
      - EXECUTIONS_MODE=queue
      - QUEUE_BULL_REDIS_HOST=${REDIS_HOST}
      - QUEUE_BULL_REDIS_PORT=${REDIS_PORT}
      - QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD}
      
      # 암호화 키
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      
      # Task Runner 설정 (External Mode)
      - N8N_RUNNERS_MODE=external
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
      - N8N_RUNNERS_ENABLED=true
      
      # 수동 실행을 Worker로 오프로드
      - OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
      
    volumes:
      - ./data/n8n:/home/node/.n8n
    networks:
      - n8n-internal-network
    healthcheck:
      test: ["CMD-SHELL", "wget --spider -q http://localhost:5678/healthz || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

  # ===========================================
  # n8n Worker Instance
  # 역할: 워크플로우 실제 실행
  # ===========================================
  n8n-worker:
    image: n8nio/n8n:latest
    container_name: n8n-worker
    restart: unless-stopped
    command: worker --concurrency=10
    environment:
      # 기본 설정
      - TZ=${TZ}
      - GENERIC_TIMEZONE=${TZ}
      
      # 데이터베이스 설정
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=${DB_HOST}
      - DB_POSTGRESDB_PORT=${DB_PORT}
      - DB_POSTGRESDB_DATABASE=${DB_NAME}
      - DB_POSTGRESDB_USER=${DB_USER}
      - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}
      
      # Queue Mode 설정
      - EXECUTIONS_MODE=queue
      - QUEUE_BULL_REDIS_HOST=${REDIS_HOST}
      - QUEUE_BULL_REDIS_PORT=${REDIS_PORT}
      - QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD}
      
      # 암호화 키
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      
      # Task Runner 설정 (External Mode)
      - N8N_RUNNERS_MODE=external
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
      - N8N_RUNNERS_ENABLED=true
            
    volumes:
      - ./data/n8n:/home/node/.n8n
    networks:
      - n8n-internal-network
    depends_on:
      n8n-main:
        condition: service_healthy

  # ===========================================
  # Task Runner for Main
  # Main 인스턴스의 Task Broker에 연결
  # ===========================================
  n8n-runners:
    image: n8n-runners-custom:latest
    container_name: n8n-runners
    restart: unless-stopped
    environment:
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
      - N8N_RUNNERS_TASK_BROKER_URI=http://n8n-main:5679
    volumes:
      - ./runners-config/n8n-task-runners.json:/etc/n8n-task-runners.json:ro
    networks:
      - n8n-internal-network
    depends_on:
      n8n-main:
        condition: service_healthy

  # ===========================================
  # Task Runner for Worker
  # Worker 인스턴스의 Task Broker에 연결
  # ===========================================
  n8n-runners-worker:
    image: n8n-runners-custom:latest
    container_name: n8n-runners-worker
    restart: unless-stopped
    environment:
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
      - N8N_RUNNERS_TASK_BROKER_URI=http://n8n-worker:5679
    volumes:
      - ./runners-config/n8n-task-runners.json:/etc/n8n-task-runners.json:ro
    networks:
      - n8n-internal-network
    depends_on:
      - n8n-worker

networks:
  n8n-internal-network:
    driver: bridge

 

Queue Mode 설정에서 주의할 점을 설명드리겠습니다. OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true 설정을 사용하면 UI에서 수동으로 실행한 워크플로우도 Worker에서 처리됩니다. 이 경우 n8n-runners-worker가 코드를 실행합니다. 이 설정을 사용하지 않으면 수동 실행은 Main에서 처리되어 n8n-runners가 동작합니다.

 

각 Runner는 서로 다른 Task Broker URI를 가리킵니다. n8n-runners는 http://n8n-main:5679를, n8n-runners-worker는 http://n8n-worker:5679를 바라봅니다. 이렇게 해야 각 n8n 인스턴스가 자신에게 연결된 Runner를 올바르게 인식합니다.

동일한 설정 파일(n8n-task-runners.json)을 양쪽 Runner에 마운트합니다. 이렇게 하면 설정을 한 곳에서 관리할 수 있어 유지보수가 편리합니다.


6. Python 보안 설정 (stdlib/external 허용)

n8n의 Python Task Runner는 기본적으로 보안을 위해 Python 표준 라이브러리(stdlib)와 외부 패키지(external) 사용을 제한합니다. 이로 인해 import sys, import os, import requests 등을 사용하면 다음과 같은 에러가 발생합니다.

Security violations detected
Line 1: Import of standard library module 'sys' is disallowed. Allowed stdlib modules: none

환경 변수 vs 설정 파일 우선순위

많은 분들이 환경 변수로 이 제한을 해제하려고 시도합니다. 하지만 이 방법은 동작하지 않습니다. n8nio/runners 이미지에는 /etc/n8n-task-runners.json 설정 파일이 내장되어 있으며, 이 파일의 env-overrides 섹션이 환경 변수를 덮어씁니다.

# 이 방법은 동작하지 않습니다!
environment:
  - N8N_RUNNERS_STDLIB_ALLOW=*
  - N8N_RUNNERS_EXTERNAL_ALLOW=*

 

n8nio/runners 이미지에 내장된 기본 설정을 확인하면, N8N_RUNNERS_STDLIB_ALLOW와 N8N_RUNNERS_EXTERNAL_ALLOW가 빈 문자열로 강제 설정되어 있습니다. 환경 변수를 아무리 설정해도 이 파일이 덮어쓰기 때문에 적용되지 않습니다.

커스텀 설정 파일 생성

따라서 환경 변수 대신 커스텀 설정 파일을 마운트해야 합니다. runners-config/n8n-task-runners.json 파일을 다음과 같이 작성합니다. 이 설정 파일은 모든 표준 라이브러리와 외부 패키지 사용을 허용합니다.

{
  "task-runners": [
    {
      "runner-type": "javascript",
      "workdir": "/home/runner",
      "command": "/usr/local/bin/node",
      "args": [
        "--disallow-code-generation-from-strings",
        "--disable-proto=delete",
        "/opt/runners/task-runner-javascript/dist/start.js"
      ],
      "health-check-server-port": "5681",
      "allowed-env": [
        "PATH",
        "GENERIC_TIMEZONE",
        "NODE_OPTIONS",
        "N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT",
        "N8N_RUNNERS_TASK_TIMEOUT",
        "N8N_RUNNERS_MAX_CONCURRENCY",
        "N8N_SENTRY_DSN",
        "N8N_VERSION",
        "ENVIRONMENT",
        "DEPLOYMENT_NAME",
        "HOME"
      ],
      "env-overrides": {
        "NODE_FUNCTION_ALLOW_BUILTIN": "*",
        "NODE_FUNCTION_ALLOW_EXTERNAL": "*",
        "N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST": "0.0.0.0"
      }
    },
    {
      "runner-type": "python",
      "workdir": "/home/runner",
      "command": "/opt/runners/task-runner-python/.venv/bin/python",
      "args": ["-m", "src.main"],
      "health-check-server-port": "5682",
      "allowed-env": [
        "PATH",
        "N8N_RUNNERS_LAUNCHER_LOG_LEVEL",
        "N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT",
        "N8N_RUNNERS_TASK_TIMEOUT",
        "N8N_RUNNERS_MAX_CONCURRENCY",
        "N8N_SENTRY_DSN",
        "N8N_VERSION",
        "ENVIRONMENT",
        "DEPLOYMENT_NAME"
      ],
      "env-overrides": {
        "PYTHONPATH": "/opt/runners/task-runner-python",
        "N8N_RUNNERS_STDLIB_ALLOW": "*",
        "N8N_RUNNERS_EXTERNAL_ALLOW": "*"
      }
    }
  ]
}

 

설정 값 설명

각 설정 값의 의미를 상세히 설명드리겠습니다. 이 설정들을 이해하면 필요에 따라 보안 수준을 조절할 수 있습니다.

  • N8N_RUNNERS_STDLIB_ALLOW는 Python 표준 라이브러리 허용 범위를 결정합니다. *로 설정하면 모든 표준 라이브러리를 허용합니다. sys,os,json처럼 쉼표로 구분하면 특정 모듈만 허용할 수 있습니다. 빈 문자열("")로 설정하면 모든 표준 라이브러리를 차단합니다.
  • N8N_RUNNERS_EXTERNAL_ALLOW는 pip로 설치한 외부 패키지 허용 범위를 결정합니다. *로 설정하면 모든 외부 패키지를 허용합니다. requests,pandas처럼 특정 패키지만 허용할 수도 있습니다. 보안이 중요한 환경에서는 필요한 패키지만 명시적으로 허용하는 것이 좋습니다.

docker-compose.yml에 볼륨 마운트

생성한 설정 파일을 Runner 컨테이너에 마운트합니다. :ro 플래그는 read-only로 마운트하여 컨테이너에서 실수로 수정되는 것을 방지합니다. 앞서 제시한 docker-compose.yml에는 이미 이 설정이 포함되어 있습니다.

volumes:
  - ./runners-config/n8n-task-runners.json:/etc/n8n-task-runners.json:ro

설정 적용 확인

컨테이너를 재생성한 후 설정이 올바르게 적용되었는지 확인합니다. 다음 명령어로 설정 파일 내용을 검증할 수 있습니다.

# 컨테이너 재생성
docker compose up -d --force-recreate n8n-runners n8n-runners-worker

# 설정 파일 확인
docker exec n8n-runners cat /etc/n8n-task-runners.json | grep -i stdlib

# 예상 출력:
#   "N8N_RUNNERS_STDLIB_ALLOW": "*",

보안 고려사항

모든 라이브러리를 허용(*)하는 것은 편리하지만, 보안 위험이 있을 수 있습니다. 프로덕션 환경에서는 실제로 사용하는 모듈만 명시적으로 허용하는 것을 고려해 보시기 바랍니다. 다음은 일반적인 데이터 처리 워크플로우에서 필요한 모듈만 허용하는 예시입니다.

"env-overrides": {
  "N8N_RUNNERS_STDLIB_ALLOW": "json,datetime,re,math,collections,itertools",
  "N8N_RUNNERS_EXTERNAL_ALLOW": "requests,pandas,numpy"
}

7. 설정 검증 및 테스트

모든 설정이 완료되었다면, 실제로 Python Code 노드가 정상적으로 동작하는지 테스트해야 합니다. 이 섹션에서는 단계별 검증 방법과 테스트 코드를 제공합니다.

컨테이너 상태 확인

먼저 모든 컨테이너가 정상적으로 실행 중인지 확인합니다. 특히 Runner 컨테이너가 n8n의 Task Broker에 성공적으로 연결되었는지 로그를 통해 확인해야 합니다.

# 전체 컨테이너 상태 확인
dockere ps

# 특정 컨테이너 그룹 상태 확인
cd n8n-stack
docker compose ps

# 예상 출력 (모든 서비스가 Up 상태여야 함):
# NAME               STATUS
# n8n-main           Up (healthy)
# n8n-worker         Up
# n8n-runners        Up
# n8n-runners-worker Up

# Runner 로그 확인 (성공 시)
docker logs n8n-runners 2>&1 | tail -10

# 예상 출력:
# [launcher:py] Waiting for task broker to be ready...
# [launcher:py] Waiting for launcher's task offer to be accepted...
# [launcher:py] Runner registered successfully

 

Runner 로그에서 "Runner registered successfully" 또는 "task offer to be accepted" 메시지가 보이면 연결이 성공한 것입니다. 에러 메시지가 있다면 인증 토큰이나 네트워크 설정을 다시 확인해야 합니다.

기본 Python 테스트

n8n 에디터에서 새 워크플로우를 생성하고 Python Code 노드를 추가합니다. 다음 테스트 코드를 입력하여 기본 동작을 확인합니다.

# 기본 동작 테스트
import sys
import json

result = {
    "python_version": sys.version,
    "message": "Python Task Runner is working!",
    "stdlib_test": "sys module imported successfully"
}

return result

 

이 코드가 정상적으로 실행되면 Python 버전 정보와 메시지가 출력됩니다. sys 모듈 import가 성공했다면 N8N_RUNNERS_STDLIB_ALLOW 설정이 올바르게 적용된 것입니다.

외부 패키지 테스트

Runner 이미지에 기본 설치된 외부 패키지가 동작하는지 확인합니다. 다음 코드로 requests 패키지를 테스트할 수 있습니다.

# 외부 패키지 테스트
import requests

response = requests.get("https://httpbin.org/ip")
data = response.json()

return {
    "status_code": response.status_code,
    "origin_ip": data.get("origin"),
    "external_package_test": "requests module working"
}

n8n Code노드를 이용해서 Python Native Script가 정상적으로 실행됩니다
n8n Code노드를 이용해서 Python Native Script가 정상적으로 실행됩니다

이 테스트가 성공하면 외부 패키지 import와 네트워크 요청이 모두 정상 동작하는 것입니다. 실패한다면 N8N_RUNNERS_EXTERNAL_ALLOW 설정이나 네트워크 연결을 확인해야 합니다.

Queue Mode 테스트

Queue Mode 환경에서는 Main과 Worker 양쪽에서 테스트해야 합니다. OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true 설정을 사용하는 경우, 에디터에서 수동 실행하면 Worker에서 처리됩니다. 이때 n8n-runners-worker가 코드를 실행합니다.

스케줄 트리거나 웹훅으로 실행되는 워크플로우는 항상 Worker에서 처리됩니다. 두 경로 모두에서 Python 코드가 정상 동작하는지 확인하시기 바랍니다.


8. crawl4ai로 웹 스크래핑 Runner 구축하기

Python Task Runner의 진정한 강점은 커스텀 패키지를 설치하여 고급 기능을 구현할 수 있다는 점입니다. 이 섹션에서는 최신 웹 스크래핑 라이브러리인 crawl4ai를 포함한 커스텀 Runner 이미지를 빌드하는 방법을 설명합니다. crawl4ai는 Playwright 기반의 고성능 웹 스크래핑 도구로, JavaScript 렌더링이 필요한 현대 웹사이트도 쉽게 크롤링할 수 있습니다.

crawl4ai란 무엇인가

crawl4ai는 AI 친화적인 웹 스크래핑 라이브러리입니다. Playwright를 기반으로 하여 JavaScript로 렌더링되는 SPA(Single Page Application)도 크롤링할 수 있습니다. 또한 Markdown 변환, 링크 추출, 스크린샷 캡처 등 다양한 기능을 제공하며, LLM과 함께 사용하기 좋은 형태로 데이터를 추출해 줍니다.

 

n8n의 HTTP Request 노드로는 JavaScript 렌더링이 필요한 페이지를 크롤링하기 어렵습니다. crawl4ai를 Python Code 노드에서 사용하면 동적 웹사이트도 쉽게 스크래핑할 수 있습니다. 이를 통해 데이터 수집, 가격 모니터링, 뉴스 수집 등 다양한 자동화 워크플로우를 구축할 수 있습니다.

3rd Party SDK 설치는 꽤나 복잡합니다  |  crawl4ai

crawl4ai를 Runner에 설치하는 것은 단순히 pip install crawl4ai로 끝나지 않습니다. 여러 가지 기술적 요구사항이 있어서 커스텀 Docker 이미지를 빌드해야 합니다. 이러한 복잡성을 이해하면 문제가 발생했을 때 더 효과적으로 대응할 수 있습니다.

  • 첫째, crawl4ai는 Playwright를 의존성으로 가지고 있습니다. Playwright는 Chromium, Firefox, WebKit 등의 실제 브라우저 바이너리가 필요합니다. 이 브라우저들은 용량이 크고(수백 MB), 별도의 설치 명령어(playwright install)로 다운로드해야 합니다. 브라우저 바이너리 없이는 crawl4ai가 정상적으로 동작하지 않습니다.
  • 둘째, Playwright 브라우저는 다양한 시스템 라이브러리에 의존합니다. Chromium을 실행하려면 libglib, libnss, libatk 등 수십 개의 시스템 패키지가 필요합니다. 기본 n8nio/runners 이미지에는 이러한 패키지가 포함되어 있지 않으므로 추가 설치가 필요합니다.
  • 셋째, n8n Task Runner의 보안 메커니즘이 환경 변수 접근을 차단합니다. crawl4ai와 Playwright는 HOME, PLAYWRIGHT_BROWSERS_PATH 등의 환경 변수를 읽어야 정상 동작하는데, 기본 설정에서는 이 접근이 차단되어 있습니다. 따라서 N8N_BLOCK_RUNNER_ENV_ACCESS=false 설정이 필수적입니다.

커스텀 Runner 이미지 빌드하기

crawl4ai를 사용하기 위한 커스텀 Runner 이미지를 빌드하는 Dockerfile을 작성합니다. 이 Dockerfile은 Multi-stage build를 사용하여 n8nio/runners에서 필요한 파일을 복사하고, Python 3.13 환경에서 crawl4ai와 Playwright를 설치합니다.

 

runners-custom/Dockerfile (for crawl4ai) 파일을 생성합니다.

# runners-custom/Dockerfile
# n8n Task Runner 커스텀 이미지 (crawl4ai + Playwright 포함)

# ============================================
# Stage 1: n8nio/runners에서 task-runner 파일 복사용
# ============================================
FROM n8nio/runners:latest AS source

# ============================================
# Stage 2: Debian용 Node.js 복사용
# n8nio/runners는 Alpine(musl)이라 Debian(glibc)에서 실행 불가
# ============================================
FROM node:22-bookworm-slim AS node-source

# ============================================
# Stage 3: Python 3.13 기반 Debian (최종 이미지)
# ============================================
FROM python:3.13-slim-bookworm

USER root

# tini 및 시스템 의존성 설치
# Playwright Chromium 실행에 필요한 라이브러리들
RUN apt-get update && apt-get install -y --no-install-recommends \
    tini \
    wget \
    gnupg \
    ca-certificates \
    fonts-liberation \
    libasound2 \
    libatk-bridge2.0-0 \
    libatk1.0-0 \
    libatspi2.0-0 \
    libcups2 \
    libdbus-1-3 \
    libdrm2 \
    libgbm1 \
    libgtk-3-0 \
    libnspr4 \
    libnss3 \
    libxcomposite1 \
    libxdamage1 \
    libxfixes3 \
    libxkbcommon0 \
    libxrandr2 \
    xdg-utils \
    && rm -rf /var/lib/apt/lists/*

# runner 유저 및 디렉토리 생성
RUN useradd -m -d /home/runner -s /bin/bash runner \
    && mkdir -p /home/runner/.crawl4ai \
    && chown -R runner:runner /home/runner

# Node.js: Debian용 공식 이미지에서 복사
COPY --from=node-source /usr/local/bin/node /usr/local/bin/node

# task-runner: n8nio/runners에서 복사 (순수 JS/Python 코드라 호환됨)
COPY --from=source /usr/local/bin/task-runner-launcher /usr/local/bin/task-runner-launcher
COPY --from=source /opt/runners/task-runner-javascript /opt/runners/task-runner-javascript
COPY --from=source /opt/runners/task-runner-python/src /opt/runners/task-runner-python/src
COPY --from=source /opt/runners/task-runner-python/pyproject.toml /opt/runners/task-runner-python/

# 빌드 시 환경변수 (설치용)
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
ENV HOME=/home/runner

# venv 생성 및 패키지 설치
RUN python -m venv /opt/runners/task-runner-python/.venv \
    && /opt/runners/task-runner-python/.venv/bin/pip install --upgrade pip \
    && /opt/runners/task-runner-python/.venv/bin/pip install --no-cache-dir \
    websockets>=15.0.1 \
    crawl4ai \
    pandas \
    requests \
    beautifulsoup4 \
    playwright

# Playwright 브라우저 설치
RUN mkdir -p /opt/playwright \
    && /opt/runners/task-runner-python/.venv/bin/playwright install chromium \
    && /opt/runners/task-runner-python/.venv/bin/playwright install-deps chromium \
    && chmod -R 755 /opt/playwright

# crawl4ai 초기화
RUN /opt/runners/task-runner-python/.venv/bin/crawl4ai-setup \
    && chown -R runner:runner /home/runner/.crawl4ai

# ============================================
# 핵심: sitecustomize.py로 환경변수 강제 설정
# Python 시작 시 무조건 실행됨
# ============================================
RUN echo 'import os\n\
os.environ["HOME"] = "/home/runner"\n\
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = "/opt/playwright"' \
    > /opt/runners/task-runner-python/.venv/lib/python3.13/site-packages/sitecustomize.py

WORKDIR /home/runner

USER runner

ENTRYPOINT ["tini", "--", "/usr/local/bin/task-runner-launcher"]
CMD ["javascript", "python"]

 

이 Dockerfile의 핵심 포인트를 설명드리겠습니다.

  • 첫째, Multi-stage build를 사용합니다. n8nio/runners 이미지에서 task-runner-launcher와 Python/JavaScript runner 코드를 복사합니다. 이렇게 하면 n8n의 Task Runner 프로토콜과 호환되는 실행 환경을 구성할 수 있습니다.
  • 둘째, Python 가상환경(venv)을 명시적으로 생성합니다. /opt/runners/task-runner-python/.venv 경로에 생성해야 task-runner-launcher가 올바르게 Python을 실행할 수 있습니다. 원본 이미지의 설정 파일에서 이 경로를 참조하기 때문입니다.
  • 셋째, sitecustomize.py를 사용하여 환경변수를 강제 설정합니다. 이것이 가장 중요한 부분입니다. n8n Task Runner는 보안을 위해 환경변수 접근을 차단할 수 있는데, sitecustomize.py는 Python 인터프리터가 시작될 때 무조건 실행됩니다. 따라서 n8n의 차단 메커니즘과 관계없이 HOME과 PLAYWRIGHT_BROWSERS_PATH가 설정됩니다.
  • 넷째, crawl4ai-setup을 실행하여 초기화합니다. crawl4ai는 최초 실행 시 추가 설정이 필요한데, 빌드 시점에 미리 실행해두면 런타임에 지연이 발생하지 않습니다.

crawl4ai용 보안 설정 파일

crawl4ai가 정상적으로 동작하려면 환경 변수 접근이 필요합니다. runners-config/n8n-task-runners.json 파일을 다음과 같이 작성합니다. 이 설정은 실제 테스트를 거쳐 검증된 버전입니다.

{
  "task-runners": [
    {
      "runner-type": "javascript",
      "workdir": "/home/runner",
      "command": "/usr/local/bin/node",
      "args": [
        "--disallow-code-generation-from-strings",
        "--disable-proto=delete",
        "/opt/runners/task-runner-javascript/dist/start.js"
      ],
      "health-check-server-port": "5681",
      "allowed-env": [
        "PATH",
        "HOME",
        "GENERIC_TIMEZONE",
        "NODE_OPTIONS",
        "N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT",
        "N8N_RUNNERS_TASK_TIMEOUT",
        "N8N_RUNNERS_MAX_CONCURRENCY",
        "N8N_SENTRY_DSN",
        "N8N_VERSION",
        "ENVIRONMENT",
        "DEPLOYMENT_NAME"
      ],
      "env-overrides": {
        "NODE_FUNCTION_ALLOW_BUILTIN": "*",
        "NODE_FUNCTION_ALLOW_EXTERNAL": "*",
        "N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST": "0.0.0.0"
      }
    },
    {
      "runner-type": "python",
      "workdir": "/home/runner",
      "command": "/opt/runners/task-runner-python/.venv/bin/python",
      "args": ["-m", "src.main"],
      "health-check-server-port": "5682",
      "allowed-env": [
        "PATH",
        "HOME",
        "PLAYWRIGHT_BROWSERS_PATH",
        "N8N_BLOCK_RUNNER_ENV_ACCESS",
        "N8N_RUNNERS_LAUNCHER_LOG_LEVEL",
        "N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT",
        "N8N_RUNNERS_TASK_TIMEOUT",
        "N8N_RUNNERS_MAX_CONCURRENCY",
        "N8N_SENTRY_DSN",
        "N8N_VERSION",
        "ENVIRONMENT",
        "DEPLOYMENT_NAME"
      ],
      "env-overrides": {
        "PYTHONPATH": "/opt/runners/task-runner-python",
        "N8N_RUNNERS_STDLIB_ALLOW": "*",
        "N8N_RUNNERS_EXTERNAL_ALLOW": "*",
        "N8N_BLOCK_RUNNER_ENV_ACCESS": "false",
        "HOME": "/home/runner",
        "PLAYWRIGHT_BROWSERS_PATH": "/opt/playwright"
      }
    }
  ]
}

 

핵심 설정을 설명드리겠습니다.

  • N8N_BLOCK_RUNNER_ENV_ACCESS: "false"는 Python 코드가 환경 변수에 접근할 수 있도록 허용합니다. 기본값이 true이면 os.environ이 비어 있어서 crawl4ai가 브라우저 경로를 찾지 못합니다. 하지만 Dockerfile의 sitecustomize.py가 이 문제를 우회하므로, 이 설정은 추가적인 안전장치 역할을 합니다.
  • PLAYWRIGHT_BROWSERS_PATH: "/opt/playwright"는 Playwright가 브라우저 바이너리를 찾는 경로입니다. Dockerfile에서 이 경로에 Chromium을 설치했으므로 동일하게 맞춰야 합니다.
  • HOME: "/home/runner"는 crawl4ai가 캐시 파일을 저장하는 홈 디렉토리입니다. Dockerfile에서 runner 유저의 홈 디렉토리로 설정한 값과 일치해야 합니다.

이미지 빌드 및 배포

커스텀 이미지를 빌드하고 docker-compose.yml에서 사용합니다. 이미지 빌드에는 Playwright 브라우저 다운로드가 포함되어 있어 시간이 다소 소요될 수 있습니다(약 5-10분).

# 디렉토리 이동
cd n8n-stack/runner-custom

# crawl4ai를 포함하는 간단한 빌드
docker build -t n8n-runner-custom:latest .

# crawl4ai 포함 커스텀 이미지 빌드 (날짜 태그 사용)
DATE_TAG=$(date +%Y%m%d)
docker build \
  -t n8n-runners-custom:${DATE_TAG} \
  -t n8n-runners-custom:latest \
  -f runners-custom/Dockerfile \

# 빌드 완료 확인
docker images | grep n8n-runners-custom

# 예상 출력 (crawl4ai + Playwright로 인해 용량이 큼): 
# Docker Desktop 또는 docker images로 확인
# n8n-runners-custom   20260113   def456abc   5 minutes ago   1.5GB
# n8n-runners-custom   latest     def456abc   5 minutes ago   1.5GB

 

docker-compose.yml에서 Runner 서비스를 다음과 같이 설정합니다. 이 설정은 실제 프로덕션 환경에서 검증된 버전입니다.

  # ---------------------------------------------------------------------------
  # n8n Task Runners (Python/JS Code 노드 실행용)
  # ---------------------------------------------------------------------------
  n8n-runners:
    # 사전 빌드한 커스텀 이미지 사용
    image: n8n-runners-crawl4ai:latest
    container_name: n8n-runners
    restart: unless-stopped
    # Playwright/Chrome이 공유 메모리를 많이 사용하므로 증가 필요
    shm_size: '2gb'
    environment:
      # Runner 인증 토큰
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
      # n8n Main의 Task Broker 연결 주소
      - N8N_RUNNERS_TASK_BROKER_URI=http://n8n-main:5679
      # STDLIB/EXTERNAL 환경변수는 설정 파일에서 처리하므로 생략
    volumes:
      # 보안 설정 파일 마운트 (읽기 전용)
      - ./runners-config/n8n-task-runners.json:/etc/n8n-task-runners.json:ro
    networks:
      - n8n-internal-network
      - n8n-shared-network
    depends_on:
      n8n-main:
        condition: service_healthy
    # 리소스 제한 설정 (선택사항이지만 권장)
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '1'
          memory: 1G

  # ---------------------------------------------------------------------------
  # n8n Task Runners for Worker
  # ---------------------------------------------------------------------------
  n8n-runners-worker:
    # Main용 Runner와 동일한 이미지 사용
    image: n8n-runners-crawl4ai:latest
    container_name: n8n-runners-worker
    restart: unless-stopped
    shm_size: '2gb'
    environment:
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
      # Worker의 Task Broker 연결 주소
      - N8N_RUNNERS_TASK_BROKER_URI=http://n8n-worker:5679
    volumes:
      - ./runners-config/n8n-task-runners.json:/etc/n8n-task-runners.json:ro
    networks:
      - n8n-internal-network
      - n8n-shared-network
    depends_on:
      - n8n-worker
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '1'
          memory: 1G

 

핵심 설정을 설명드리겠습니다.

  • shm_size: '2gb'는 Playwright와 Chromium이 공유 메모리를 많이 사용하므로 필수입니다. 기본값(64MB)으로는 브라우저가 크래시하거나 메모리 부족 에러가 발생합니다. 최소 1GB 이상, 권장 2GB입니다.
  • deploy.resources는 Runner 컨테이너의 리소스 사용량을 제한합니다. crawl4ai와 Playwright는 리소스를 많이 사용하므로, 다른 서비스에 영향을 주지 않도록 제한하는 것이 좋습니다. limits는 최대 사용량, reservations는 최소 보장량입니다.
  • 환경변수에서 N8N_RUNNERS_STDLIB_ALLOWN8N_RUNNERS_EXTERNAL_ALLOW를 설정하지 않은 이유는, 설정 파일(n8n-task-runners.json)의 env-overrides가 환경변수를 덮어쓰기 때문입니다. 설정 파일에서 한 번만 관리하는 것이 혼란을 줄입니다.

이제 서비스를 시작합니다.

# 전체 서비스 시작
docker compose up -d

# Runner 컨테이너만 재생성 (이미지 변경 후)
docker compose up -d --force-recreate n8n-runners n8n-runners-worker

# 실행 중인 이미지 버전 확인
docker compose ps --format "table {{.Name}}\t{{.Image}}"

# Runner 로그 확인 (정상 시작 확인)
docker logs n8n-runners 2>&1 | tail -20

# 예상 출력:
# [launcher:py] Starting launcher goroutine...
# [launcher:py] Waiting for task broker to be ready...
# [launcher:js] Starting launcher goroutine...

crawl4ai 테스트 코드

n8n의 Python Code 노드에서 crawl4ai가 정상적으로 동작하는지 테스트합니다. 다음 코드를 n8n 워크플로우 편집기 창에서  Code 노드 > Python (Native)를 지정한 다음 아래의 코드를  입력하고 실행합니다.

# crawl4ai 기본 테스트
import os
import asyncio
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig

async def main():
    browser_config = BrowserConfig(
        headless=True,
        verbose=False,
    )
    
    crawler_config = CrawlerRunConfig(
        wait_until="networkidle",
    )
    
    async with AsyncWebCrawler(config=browser_config) as crawler:
        result = await crawler.arun(
            url="https://example.com",
            config=crawler_config
        )
        
        return {
            "success": result.success,
            "title": result.metadata.get("title", "No title"),
            "markdown_length": len(result.markdown.raw_markdown) if result.markdown else 0,
            "links_count": len(result.links.get("internal", [])) + len(result.links.get("external", []))
        }

result = asyncio.run(main())
return result

 

이 테스트가 성공하면 crawl4ai가 정상적으로 동작하는 것입니다. success: true와 함께 페이지 제목, Markdown 길이, 링크 수가 출력됩니다.

실제 웹 스크래핑 예시

crawl4ai를 활용한 실제 웹 스크래핑 예시입니다. 뉴스 사이트에서 기사를 수집하는 워크플로우에 활용할 수 있습니다.

# 실제 웹 스크래핑 예시 - 뉴스 기사 수집
import asyncio
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig

async def scrape_news():
    browser_config = BrowserConfig(
        headless=True,
        verbose=False,
    )
    
    crawler_config = CrawlerRunConfig(
        wait_until="domcontentloaded",
        page_timeout=30000,  # 30초 타임아웃
    )
    
    # 입력 데이터에서 URL 가져오기 (n8n Python Code 노드 방식)
    # _input은 n8n이 제공하는 입력 데이터 객체입니다
    try:
        url = _input.first().json.get("url", "https://news.ycombinator.com")
    except:
        url = "https://news.ycombinator.com"
    
    async with AsyncWebCrawler(config=browser_config) as crawler:
        result = await crawler.arun(
            url=url,
            config=crawler_config
        )
        
        if not result.success:
            return {"error": "Crawling failed", "url": url}
        
        return {
            "url": url,
            "title": result.metadata.get("title", ""),
            "markdown": result.markdown.raw_markdown[:5000] if result.markdown else "",
            "internal_links": result.links.get("internal", [])[:20],
            "external_links": result.links.get("external", [])[:20],
            "screenshot_available": result.screenshot is not None
        }

result = asyncio.run(scrape_news())
return result
https://news.ycombinator.com 뉴스 사이트에 대한 실제 스크래핑 결과 마크다운 형태의 결과물을 출력하고 있습니다
https://news.ycombinator.com 뉴스 사이트에 대한 실제 스크래핑 결과 마크다운 형태의 결과물을 출력하고 있습니다
 

9. 트러블슈팅 가이드

Python Task Runner를 설정하다 보면 다양한 에러를 만날 수 있습니다. 이 섹션에서는 자주 발생하는 문제들과 해결 방법을 정리했습니다.

Python runner unavailable

가장 흔한 에러입니다. Python Code 노드를 실행하려고 할 때 "Python runner unavailable" 메시지가 나타납니다. 이 에러는 n8n이 Python Task Runner를 찾지 못했을 때 발생합니다.

원인 1: Runner 컨테이너가 실행되지 않음
해결: docker compose ps로 상태 확인 후 docker compose up -d

원인 2: 인증 토큰 불일치
해결: n8n과 Runner의 N8N_RUNNERS_AUTH_TOKEN이 동일한지 확인

원인 3: Task Broker URI 오류
해결: N8N_RUNNERS_TASK_BROKER_URI가 올바른지 확인
      (예: http://n8n-main:5679)

원인 4: 네트워크 연결 문제
해결: Runner와 n8n이 같은 Docker 네트워크에 있는지 확인

Security violations detected

Python 표준 라이브러리나 외부 패키지를 import할 때 발생합니다. 기본 설정에서는 보안을 위해 대부분의 import가 차단되어 있습니다.

에러 메시지 예시:
Security violations detected
Line 1: Import of standard library module 'sys' is disallowed

해결 방법:
1. runners-config/n8n-task-runners.json 파일 생성
2. N8N_RUNNERS_STDLIB_ALLOW와 N8N_RUNNERS_EXTERNAL_ALLOW를 * 로 설정
3. 설정 파일을 Runner 컨테이너에 볼륨 마운트
4. docker compose up -d --force-recreate로 재시작

NoneType object has no attribute startswith

crawl4ai 사용 시 자주 발생하는 에러입니다. Playwright가 브라우저 경로를 찾지 못할 때 나타납니다. 이는 환경 변수 접근이 차단되어 있기 때문입니다.

원인: N8N_BLOCK_RUNNER_ENV_ACCESS가 기본값(true)으로 설정됨
해결: n8n-task-runners.json에 다음 추가
      "N8N_BLOCK_RUNNER_ENV_ACCESS": "false"

추가 확인:
- PLAYWRIGHT_BROWSERS_PATH가 올바르게 설정되었는지 확인
- HOME 환경 변수가 설정되었는지 확인

No module named crawl4ai

커스텀 이미지가 제대로 적용되지 않았을 때 발생합니다. Runner 컨테이너가 기본 이미지를 사용하고 있을 수 있습니다.

확인 방법:
docker exec n8n-runners pip list | grep crawl4ai

해결 방법:
1. docker-compose.yml에서 이미지가 커스텀 이미지인지 확인
   image: n8n-runners-crawl4ai:latest
2. 이미지가 실제로 빌드되었는지 확인
   docker images | grep n8n-runners-crawl4ai
3. 컨테이너 재생성
   docker compose up -d --force-recreate n8n-runners

Lost connection to the server

Queue Mode에서 수동 실행 시 발생할 수 있습니다. 특히 OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true 설정 후에 나타날 수 있습니다.

원인: Worker용 Runner(n8n-runners-worker)가 정상 동작하지 않음

확인 방법:
docker logs n8n-runners-worker 2>&1 | tail -30

해결 방법:
1. n8n-runners-worker 컨테이너 상태 확인
2. N8N_RUNNERS_TASK_BROKER_URI가 http://n8n-worker:5679인지 확인
3. Worker 인스턴스의 N8N_RUNNERS_ENABLED=true 확인
4. 컨테이너 재시작

디버깅 명령어 모음

문제 해결에 유용한 명령어들을 정리했습니다. 상황에 따라 적절한 명령어를 사용하여 원인을 파악하시기 바랍니다.

# 전체 컨테이너 상태 확인
docker compose ps

# Runner 로그 확인
docker logs n8n-runners 2>&1 | tail -50
docker logs n8n-runners-worker 2>&1 | tail -50

# 설정 파일 확인
docker exec n8n-runners cat /etc/n8n-task-runners.json

# 설치된 Python 패키지 확인
docker exec n8n-runners pip list

# Playwright 브라우저 경로 확인
docker exec n8n-runners ls -la /root/.cache/ms-playwright/

# 환경 변수 확인
docker exec n8n-runners env | grep -E "PLAYWRIGHT|HOME|N8N"

# 네트워크 연결 테스트
docker exec n8n-runners wget -q -O- http://n8n-main:5679/healthz

10. 마무리 및 권장사항

이 가이드에서는 n8n 2.0의 Python Task Runner를 External Mode로 설정하는 방법을 상세히 다루었습니다. Task Runner의 개념부터 Queue Mode 환경에서의 설정, 보안 구성, 그리고 crawl4ai를 포함한 커스텀 이미지 빌드까지 실제 프로덕션에서 검증된 설정을 공유했습니다.

핵심 요약

n8n 2.0에서 Python Code 노드를 사용하려면 Task Runner가 필수입니다. Internal Mode는 개발 환경에서만, 프로덕션에서는 반드시 External Mode를 사용해야 합니다. Queue Mode 환경에서는 Main과 Worker 각각에 연결된 Runner가 필요합니다. Python 표준 라이브러리와 외부 패키지를 사용하려면 커스텀 설정 파일을 마운트해야 합니다.

권장 설정 체크리스트

프로덕션 환경에 배포하기 전에 다음 항목들을 확인하시기 바랍니다.

[v] N8N_RUNNERS_MODE=external 설정
[v] N8N_RUNNERS_AUTH_TOKEN - 충분히 긴 랜덤 토큰 사용
[v] n8n-task-runners.json 커스텀 설정 파일 마운트
[v] Queue Mode: Main용, Worker용 Runner 각각 구성
[v] 커스텀 이미지 사전 빌드 및 태그 지정
[v] 컨테이너 health check 설정
[v] 로그 모니터링 구성

보안 권장사항

모든 라이브러리를 허용(*)하는 것보다 실제 필요한 모듈만 명시적으로 허용하는 것이 좋습니다. 특히 외부에서 입력을 받는 워크플로우의 경우, 입력 값 검증을 철저히 하시기 바랍니다. crawl4ai 사용 시 N8N_BLOCK_RUNNER_ENV_ACCESS=false가 필수이지만, 이 설정은 보안 수준을 낮추므로 신뢰할 수 있는 코드만 실행해야 합니다.

다음 단계

이 가이드의 설정이 완료되었다면 다양한 Python 기반 자동화를 구현해 볼 수 있습니다. 데이터 분석 워크플로우에서는 pandas, numpy를 활용할 수 있습니다. 웹 스크래핑에서는 crawl4ai, requests, beautifulsoup4를 조합할 수 있습니다. AI/ML 연동에서는 openai, anthropic 등의 API 클라이언트를 사용할 수 있습니다. 필요한 패키지를 커스텀 Runner 이미지에 추가하여 n8n의 가능성을 확장해 보시기 바랍니다.


참고 자료