N8N 从源码构建自定义镜像实战指南
N8N 从源码构建自定义镜像实战指南
在使用 N8N 进行自动化流程开发时,官方提供的标准 Docker 镜像可能无法满足所有定制化需求(例如:预装特定的社区节点、修改底层依赖版本、或进行深度代码修改)。本文将详细介绍如何从 N8N 源码开始,一步步编译并构建自定义的容器镜像。
一、配置编译环境
N8N 是基于 Node.js 的项目,构建过程对环境有一定要求。为了保证编译出的产物与容器环境兼容,建议在与目标容器相同的 Linux 环境下进行编译。
使用环境: CentOS
必要软件安装:
# 1. 更新系统包
sudo dnf update
# 2. 安装 Node.js (推荐 v20 版本,与 N8N 官方镜像保持一致)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo dnf install -y nodejs
# 3. 安装编译工具 (编译原生模块如 sqlite3, isolated-vm 必需)
sudo dnf install -y python3 make gcc-c++ "Development Tools"
# 4. 启用 Corepack (Node.js 自带的包管理器管理器)
# 这一步非常关键,用于自动激活项目锁定的 pnpm 版本
sudo corepack enable
二、源码编译
这一步的核心目标是生成 ./compiled 目录,该目录包含了 N8N 运行所需的所有代码、前端资源以及 node_modules 依赖树。
1. 克隆源码
从 GitHub 拉取 N8N 仓库并切换到您需要的稳定版本标签。
git clone https://github.com/n8n-io/n8n.git
cd n8n
# 使用版本 2.27.5 版本
git checkout tags/n8n@2.27.5
2. 安装依赖
使用 pnpm 安装项目依赖。这一步会根据 pnpm-lock.yaml 下载所有依赖包。
# 确保使用项目锁定的 pnpm 版本 (由 package.json 中的 packageManager 字段指定)
corepack prepare pnpm@10.32.1 --activate
# 安装依赖 --frozen-lockfile 确保严格按照 lockfile 安装,避免自动升级版本
pnpm install --frozen-lockfile
3. 生产构建
这是最关键的一步。请勿尝试使用 npm pack 解压,必须使用官方构建脚本。
# 执行构建命令
pnpm build:n8n
该命令会自动完成以下工作:
- 编译前端 UI (editor-ui)。
- 编译后端代码。
- 使用
pnpm deploy机制,将运行时所需的文件提取并整理到项目根目录下的compiled文件夹中。
执行成功后,您将看到根目录下生成了 compiled 文件夹,其中包含完整的 node_modules 目录。
三、N8N自定义镜像构造文件 Podmanfile
拥有 compiled 目录后,我们可以编写 Podmanfile(语法与 Dockerfile 完全兼容)来构建镜像。
Podmanfile 完整示例
在 N8N 源码根目录下创建 Podmanfile-N8N 文件:
添加扩展 音视频处理 ffmpeg 、视频下载 yt-dlp、Emoji渲染 font-noto-emoji 等
重构镜像
ARG NODE_VERSION=24.16.0
ARG N8N_VERSION=2.27.5
FROM node:24.16.0-alpine3.24 AS builder
COPY ./compiled /usr/local/lib/node_modules/n8n
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
apk add --no-cache python3 make g++ && \
cd /usr/local/lib/node_modules/n8n && \
npm rebuild sqlite3 && \
rm -rf node_modules/isolated-vm/prebuilds && \
cd node_modules/isolated-vm && \
node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild --release -j max --dist-url=https://registry.npmmirror.com/-/binary/node/ && \
rm -rf /var/cache/apk/*
FROM node:24.16.0-alpine3.24
# Install all dependencies in a single layer to minimize image size
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
apk update && apk upgrade --no-cache && \
# Install fonts
apk --no-cache add --virtual .build-deps-fonts msttcorefonts-installer fontconfig && \
update-ms-fonts && \
fc-cache -f && \
apk del .build-deps-fonts && \
find /usr/share/fonts/truetype/msttcorefonts/ -type l -exec unlink {} \; && \
apk add --no-cache \
openssh \
graphicsmagick \
tini \
tzdata \
ca-certificates \
libc6-compat \
# Media processing
ffmpeg \
# Browser automation (Chromium + Puppeteer/Playwright)
chromium \
chromium-chromedriver \
nss \
freetype \
harfbuzz \
cairo \
libxkbcommon \
libdrm \
mesa-gbm \
pango \
at-spi2-core \
# Emoji fonts
font-noto-emoji \
# Python/yt-dlp
python3 \
py3-pip \
py3-setuptools \
py3-wheel && \
# Install yt-dlp globally
pip3 install --no-cache-dir --break-system-packages yt-dlp && \
ln -sf /usr/bin/yt-dlp /usr/local/bin/yt-dlp && \
# Cleanup
rm -rf /tmp/* /root/.npm /root/.cache/node /opt/yarn* && \
rm -rf /var/cache/apk/*
# apk del apk-tools && \
# mkdir -p /usr/local/bin && ln -sf /usr/bin/node /usr/local/bin/node
ARG N8N_VERSION
ARG N8N_RELEASE_TYPE=dev
ENV NODE_ENV=production
ENV N8N_RELEASE_TYPE=${N8N_RELEASE_TYPE}
ENV SHELL=/bin/sh
ENV NODE_PATH=/usr/local/lib/node_modules
# Puppeteer/Playwright configuration
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser \
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
PLAYWRIGHT_BROWSERS_PATH=0 \
CHROME_BIN=/usr/bin/chromium-browser \
CHROME_PATH=/usr/bin/chromium-browser
WORKDIR /home/node
COPY --from=builder /usr/local/lib/node_modules/n8n /usr/local/lib/node_modules/n8n
COPY ./docker-entrypoint.sh /
# Setup n8n binary symlink, home directory, and permissions
RUN ln -s /usr/local/lib/node_modules/n8n/bin/n8n /usr/local/bin/n8n && \
mkdir -p /home/node/.n8n && \
chown -R node:node /home/node && \
rm -rf /root/.npm /tmp/* && \
node --version && \
n8n --version
EXPOSE 5678/tcp
USER node
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
LABEL org.opencontainers.image.title="n8n" \
org.opencontainers.image.description="Workflow Automation Tool" \
org.opencontainers.image.source="https://github.com/n8n-io/n8n" \
org.opencontainers.image.url="https://n8n.io" \
org.opencontainers.image.version=${N8N_VERSION}
指令详解
- 基础镜像选择:选择
node:20-alpine系列镜像,体积小且与编译环境一致。 - 重新编译原生模块:这是构建过程中最易出错的一步。
sqlite3和isolated-vm包含 C++ 原生代码,必须在与容器基础镜像相同的环境(Alpine)下重新编译,否则容器启动时会报错。 - 社区节点预装:通过环境变量
N8N_COMMUNITY_PACKAGES,N8N 启动时会自动下载指定的社区节点,无需手动复制文件。
四、task-runner自定义镜像构造文件 Podmanfile
拥有 compiled 目录后,我们可以编写 Podmanfile(语法与 Dockerfile 完全兼容)来构建镜像。
Podmanfile 完整示例
在 N8N 源码根目录下创建 Podmanfile-N8N-task-runner 文件:
添加扩展 音视频处理 ffmpeg 、视频下载 yt-dlp、Emoji渲染 font-noto-emoji 等
重构镜像
ARG NODE_VERSION=24.16.0
ARG PYTHON_VERSION=3.13
# ==============================================================================
# STAGE 1: JavaScript runner (@n8n/task-runner) artifact from CI
# ==============================================================================
FROM node:${NODE_VERSION}-alpine3.24 AS javascript-runner-builder
COPY ./dist/task-runner-javascript /app/task-runner-javascript
WORKDIR /app/task-runner-javascript
RUN corepack enable pnpm
# Remove `catalog` and `workspace` references from package.json to allow `pnpm add` in extended images
RUN node -e "const pkg = require('./package.json'); \
Object.keys(pkg.dependencies || {}).forEach(k => { \
const val = pkg.dependencies[k]; \
if (val === 'catalog:' || val.startsWith('catalog:') || val.startsWith('workspace:')) \
delete pkg.dependencies[k]; \
}); \
Object.keys(pkg.devDependencies || {}).forEach(k => { \
const val = pkg.devDependencies[k]; \
if (val === 'catalog:' || val.startsWith('catalog:') || val.startsWith('workspace:')) \
delete pkg.devDependencies[k]; \
}); \
delete pkg.devDependencies; \
require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));"
# Install moment (special case for backwards compatibility)
RUN rm -f node_modules/.modules.yaml && \
pnpm add moment@2.30.1 --prod --no-lockfile
# ==============================================================================
# STAGE 2: Python runner build (@n8n/task-runner-python) with uv
# Produces a relocatable venv tied to the python version used
# ==============================================================================
FROM python:${PYTHON_VERSION}-alpine AS python-runner-builder
ARG TARGETPLATFORM
ARG UV_VERSION=0.11.26
ARG UV_ARCH=x86_64-unknown-linux-musl
COPY ./uv-${UV_ARCH}.tar.gz ./
RUN tar -xzf "uv-${UV_ARCH}.tar.gz"; \
install -m 0755 "uv-${UV_ARCH}/uv" /usr/local/bin/uv; \
cd / && rm -rf /tmp/uv
WORKDIR /app/task-runner-python
COPY packages/@n8n/task-runner-python/pyproject.toml \
packages/@n8n/task-runner-python/uv.lock** \
packages/@n8n/task-runner-python/.python-version** \
./
RUN uv venv
RUN uv sync \
--frozen \
--no-editable \
--no-install-project \
--no-dev \
--all-extras
COPY packages/@n8n/task-runner-python/ ./
RUN uv sync \
--frozen \
--no-dev \
--all-extras \
--no-editable
# Install the python runner package itself into site packages. We can remove the src directory then
RUN uv pip install . && rm -rf /app/task-runner-python/src
# ==============================================================================
# STAGE 3: Task Runner Launcher download
# ==============================================================================
FROM alpine:3.24 AS launcher-downloader
ARG TARGETPLATFORM
ARG LAUNCHER_VERSION=task-runner-launcher-1.4.7-linux-arm64
COPY ./${LAUNCHER_VERSION}.tar.gz ./
RUN mkdir -p /launcher-bin; \
tar xzf ${LAUNCHER_VERSION}.tar.gz -C /launcher-bin; \
cd / && rm -rf /launcher-temp
# ==============================================================================
# STAGE 4: Node alpine base for JS task runner
# ==============================================================================
FROM node:${NODE_VERSION}-alpine3.24 AS node-alpine
# ==============================================================================
# STAGE 5: Runtime
# ==============================================================================
FROM python:${PYTHON_VERSION}-alpine AS runtime
ARG N8N_VERSION=2.27.5
ARG N8N_RELEASE_TYPE=dev
ENV NODE_ENV=production \
N8N_RELEASE_TYPE=${N8N_RELEASE_TYPE} \
SHELL=/bin/sh
# Bring `uv` over from python-runner-builder, to make the image easier to extend
COPY --from=python-runner-builder /usr/local/bin/uv /usr/local/bin/uv
# Bring node over from node-alpine
COPY --from=node-alpine /usr/local/bin/node /usr/local/bin/node
# libstdc++ is required by Node
# libc6-compat is required by task-runner-launcher
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
apk update && apk upgrade --no-cache && \
apk add --no-cache ca-certificates tini libstdc++ libc6-compat && \
ffmpeg chromium nss freetype harfbuzz ca-certificates ttf-freefont font-noto-cjk font-noto-emoji python3 py3-pip git && \
pip3 install --no-cache-dir --break-system-packages --upgrade pip yt-dlp && \
fc-cache -f -v && \
rm -rf /var/cache/apk/* && \
apk del apk-tools
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser \
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
PLAYWRIGHT_BROWSERS_PATH=0 \
CHROME_BIN=/usr/bin/chromium-browser \
CHROME_PATH=/usr/bin/chromium-browser
# Bring corepack and pnpm over, to make the image easier to extend
COPY --from=node-alpine /usr/local/lib/node_modules/corepack /usr/local/lib/node_modules/corepack
RUN ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack && \
ln -s ../lib/node_modules/corepack/dist/pnpm.js /usr/local/bin/pnpm
RUN addgroup -g 1000 -S runner \
&& adduser -u 1000 -S -G runner -h /home/runner -D runner
WORKDIR /home/runner
COPY --from=javascript-runner-builder --chown=root:root /app/task-runner-javascript /opt/runners/task-runner-javascript
COPY --from=python-runner-builder --chown=root:root /app/task-runner-python /opt/runners/task-runner-python
COPY --from=launcher-downloader /launcher-bin/* /usr/local/bin/
COPY --chown=root:root docker/images/runners/n8n-task-runners.json /etc/n8n-task-runners.json
USER runner
EXPOSE 5680/tcp
ENTRYPOINT ["tini", "--", "/usr/local/bin/task-runner-launcher"]
CMD ["javascript", "python"]
LABEL org.opencontainers.image.title="n8n task runners" \
org.opencontainers.image.description="Sidecar image providing n8n task runners for JavaScript and Python code execution" \
org.opencontainers.image.source="https://github.com/n8n-io/n8n" \
org.opencontainers.image.url="https://n8n.io" \
org.opencontainers.image.version="${N8N_VERSION}"
- COPY 命令需要的文件在源码中查找;其它文件需自行下载
- uv_x86_64-unknown-linux-musl.tar.gz
- task-runner-launcher-1.4.7-linux-amd64.tar.gz
五、生成镜像
1. 执行构建命令
确保当前目录包含 compiled 文件夹和 Podmanfile,执行以下命令构建镜像:
# 使用 Podman 构建 N8N 镜像
podman build -t n8n-custom:2.27.5 -f Podmanfile-N8N
# 使用 Podman 构建 task-runner 镜像
podman build -t n8n-task-runner-custom:2.27.5 -f Podmanfile-N8N-task-runner
2. 运行与验证
构建完成后,使用以下命令启动容器进行验证:
podman run -d --name n8n-test \
-p 5678:5678 \
-v ~/.n8n:/home/node/.n8n \
n8n-custom:2.27.5
访问 http://localhost:5678,如果能看到 N8N 的初始化向导,说明镜像构建成功。
文章首发于博客园,转载请注明出处。如有疑问,欢迎留言讨论。

浙公网安备 33010602011771号