self-hosted N8N1에서 N8N2 변경점 및 도커 이미지 커스텀 팁

서론
최근 가장 핫한 자동화 앱 중에 개발 속도가 빠르기로 유명한 N8N에 관한 내용입니다. N8N2는 N8N1에 비해 훨씬 보안이 강화되었고, 성능을 위한 변화도 생겼습니다.
이 글은 self-hosted N8N을 사용하는 분들 중 N8N1에서 N8N2으로 전환을 고려하는 분들이 참고할만한 내용입니다. N8N이 뭔지 모르거나 N8N을 처음 설치하거나 N8N을 유료로 이용하는 분들에게는 별로 도움 될만한 내용이 없습니다.
이 글은 개인적인 메모 용도로 작성하고 있으므로 설명이 다소 불친절할 수 있습니다.
이 글을 작성하는 현재 N8N1의 최신 버전은 1.123.12, N8N2의 최신 버전은 2.4.0입니다. 워낙 변화가 빠른 앱이라 이 글의 내용과 앱의 상태가 다를 수 있습니다.
사전 준비
- 현재 환경 백업 (데이터베이스, 워크플로, 파일)
- 지원되는 데이터베이스 확인 (PostgreSQL 권장)
- Docker / npm 설치 버전 점검
N8N1 → N8N2 핵심 변경 사항 요약
- Start 노드 제거 및 대체 트리거 노드 구체화 : 기존 Start노드는 더 이상 지원되지 않고 Manual Trigger등 구체적인 트리거 노드만 사용됩니다.
- 미지원 노드 삭제 : Spontit, crowd.dev node, Kitemaker 등 더 이상 지원되지 않는 외부 서비스 노드가 삭제 되었습니다.
- Code 노드 Task Runner 기반 전환 : 코드 노드의 기본 실행방식이 Task Runner방식으로 바뀌었습니다. N8N 외부에서 js나 python을 코드를 실행한 후 결과를 N8N에 전달하는 방식입니다.
- ExecuteCommand / LocalFileTrigger 기본 비활성화 : 보안을 위해 Excute Command, LocalFileTrigger 노드가 기본적으로 비활성화되었습니다. 활성화하려면 N8N 환경변수 값을 변경해 함.
- ReadWriteFile from Disk 접근 가능한 폴더 제한 : 보안을 위해 미리 정의된 경로만 접근할 수 있음.
- 환경 변수 접근 차단 (
N8N_BLOCK_ENV_ACCESS_IN_NODE) : 보안을 위해 N8N의 환경 변수에 접근할 수 없는 것이 기본값 - Binary Data 저장 방식 변경 (filesystem / database / s3) : 바이너리를 처리할 때 기존 기본값이 었던 in-memory방식을 제거함.
N8N_DEFAULT_BINARY_DATA_MODE로 정의하며 일반적인 유저의 기본값은filesystem이 됨. - Docker 이미지 변경 (
n8nio/runners별도 사용) : n8n-runners를 별도의 도커 이미지로 지원.
데이터베이스 변경
- MySQL/MariaDB → PostgreSQL 전환 : N8N자체의 정보를 저장하는 DB가 기존에는 MySQL계열과 Postgres 모두 지원했지만 N8N2부터는 Postgres만 지원함. MySQL 노드는 여전히 지원.
워크플로우 마이그레이션 주의
- Start 노드 교체 (Manual Trigger / Execute Workflow Trigger)
- Python Code 노드 재작성 및 외부 Runner 연결
- ExecuteCommand / LocalFileTrigger 사용 시 보안 설정
- 서브 워크플로 동작 변경 (입력 → 출력 전달)
- 호환성 체크 : Settings → Migration Report에서 확인

N8N2 커스텀 도커 이미지 관련 변경 내용
기본값으로 설치해서 사용하시는 분들은 별로 골치 앓을 부분이 없습니다. 하지만 rclone, ffmpeg, yt-dlp등을 추가해서 도커 이미지를 커스텀하시는 분들은 변경사항이 많아서 변경이 쉽지 않을 수 있습니다. 저도 3개의 서버 중 테스트 서버는 이전을 완료했지만 가장 활발하게 사용하는 2개는 아직 N8N2로 이전하지 않고 있습니다. 도커 이미지 관련 기본적인 변경점은 다음과 같습니다.
- N8N의 도커는 n8n, n8n-runners 두 개의 이미지를 쓰는 방식으로 바뀌었습니다.
- n8n 도커 이미지는 distroless 방식으로 바뀌었습니다. (apk없음)
- n8n-runners 도커 이미지는 distroless 방식, non-distroless 방식 모두 제공됩니다.
N8N2 도커 이미지 커스텀 팁
N8N 이미지 생성 팁
- n8n 도커 이미지는 distroless 방식으로 변경되었기 때문에 apk 자체가 없음
- 도커 이미지 생성시 alpine linux 버전에 맞는 apk를 수동으로 추가해야 커스텀 설치 가능
- n8n 도커 이미지 내에 어떤 alpine linux가 사용되는지는 다음 파일 참고 : https://github.com/n8n-io/n8n/blob/master/docker/images/n8n-base/Dockerfile (3.22로 확인됨)
- apk파일 수동 구성을 포함한 커스텀 Dockerfile
FROM n8nio/n8n:2.4.0
USER root
# Copy apk and its deps from Alpine 3.22
COPY --from=alpine:3.22 /sbin/apk /sbin/apk
COPY --from=alpine:3.22 /usr/lib/libapk.so* /usr/lib/
RUN apk add --no-cache \
curl \
ffmpeg \
rclone \
yt-dlp \
sqlite \
perl \
imagemagick \
ghostscript \
jq \
coreutils \
libxml2-utils
USER node
N8N-runner 이미지 생성 팁
- n8n-runners 도커 이미지는 distroless 방식이 아닌 non-distroless 방식의 이미지를 시작이미로 설정해야 커스텀이 쉬워짐
- 그렇지만 호환성이 떨어지는 파이썬 3.13을 사용해야 한다는 치명적인 단점이 있음(다른 버전의 파이썬을 사용하는 방식은 이 글에서 다루지 않음)
- 추가 패키지를 설치하는 커스텀 Dockerfile (node.js의 moment uuid exceljs pdf-lib를 추가 설치, 파이썬의 numpy pandas pefile를 추가 설치 )
FROM n8nio/runners:2.4.0
USER root
RUN cd /opt/runners/task-runner-javascript && pnpm add moment uuid exceljs pdf-lib
RUN cd /opt/runners/task-runner-python && uv pip install numpy pandas pefile
COPY n8n-task-runners.json /etc/n8n-task-runners.json
USER runner
- 위 dockerfile을 빌드하기 전에 Dockerfile과 같은 경로에
n8n-task-runners.json도 있어야 함. n8n-task-runners.json파일 최신 버전 받기
curl -o n8n-task-runners.json \
https://raw.githubusercontent.com/n8n-io/n8n/master/docker/images/runners/n8n-task-runners.json
- n8n-task-runners.json 수정 예(N8N_RUNNERS_STDLIB_ALLOW: 기본 패키지 모두 제한 없이 사용 / NODE_FUNCTION_ALLOW_EXTERNAL: node.js의 moment uuid exceljs pdf-lib 추가 / N8N_RUNNERS_EXTERNAL_ALLOW: 파이썬의 numpy pandas pefile 추가)
{
"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": "crypto",
"NODE_FUNCTION_ALLOW_EXTERNAL": "moment,uuid,exceljs,pdf-lib",
"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": "numpy,pandas,pefile"
}
}
]
}
N8N2의 도커 컴포즈 예제
- 제가 사용하는 값과 다른 부분이 많고 각자 상황에 맞게 .env, flag를 수정해서 써야 합니다.
.env
POSTGRES_USER=myname
POSTGRES_PASSWORD=mypw
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=myname
POSTGRES_NON_ROOT_PASSWORD=mypw
N8N_RUNNERS_AUTH_TOKEN=my_auth_token
docker-compose.yml
services:
postgres:
image: postgres:16
restart: always
environment:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
- POSTGRES_NON_ROOT_USER
- POSTGRES_NON_ROOT_PASSWORD
volumes:
- ./db_storage:/var/lib/postgresql/data
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
n8n:
image: n8nio/n8n:2.4.0
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
- N8N_RUNNERS_ENABLED=true
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
- N8N_NATIVE_PYTHON_RUNNER=true
- NODES_EXCLUDE="[]" #기존 노드 전부 사용
ports:
- "5678:5678"
volumes:
- ./n8n_storage:/home/node/.n8n
task-runners:
image: n8nio/runners:2.4.0
environment:
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
depends_on:
- n8n