畅想!!

馨园

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 :: 管理 ::

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 系列镜像,体积小且与编译环境一致。
  • 重新编译原生模块:这是构建过程中最易出错的一步。sqlite3isolated-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}"

五、生成镜像

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 的初始化向导,说明镜像构建成功。


文章首发于博客园,转载请注明出处。如有疑问,欢迎留言讨论。

posted on 2026-07-03 14:56  阿乐01  阅读(15)  评论(0)    收藏  举报