ci: Docker move build stage outside container (no-changelog) (#16009)

This commit is contained in:
shortstacked
2025-06-25 12:52:16 +01:00
committed by GitHub
parent 3f6eef1706
commit 909b65d266
14 changed files with 949 additions and 350 deletions

View File

@@ -12,7 +12,6 @@ packages/**/*.test.*
.github
!.github/scripts
*.tsbuildinfo
packages/cli/dist/**/e2e.*
docker/compose
docker/**/Dockerfile
.vscode

332
.github/workflows/docker-build-push.yml vendored Normal file
View File

@@ -0,0 +1,332 @@
# This workflow is used to build and push the Docker image for n8n
# - build-and-push-docker: This builds on both an ARM64 and AMD64 runner so the builds are native to the platform. Uses blacksmith native runners and build-push-action
# - create_multi_arch_manifest: This creates the multi-arch manifest for the Docker image. Needed to recombine the images from the build-and-push-docker job since they are separate runners.
# - security-scan: This scans the Docker image for security vulnerabilities using Trivy.
name: 'Docker: Build and Push'
on:
schedule:
- cron: '0 0 * * *'
workflow_call:
inputs:
n8n_version:
description: 'N8N version to build'
required: true
type: string
release_type:
description: 'Release type (stable, nightly, dev)'
required: true
type: string
default: 'dev'
push_enabled:
description: 'Whether to push the built images'
required: false
type: boolean
default: true
workflow_dispatch:
inputs:
release_type:
description: 'Release type'
required: true
type: choice
options:
- nightly
- dev
- stable
- branch
default: 'dev'
push_to_registry:
description: 'Push image to registry'
required: false
type: boolean
default: true
pull_request:
types:
- opened
- ready_for_review
paths:
- '.github/workflows/docker-build-push.yml'
- 'docker/images/n8n/Dockerfile'
jobs:
build-and-push-docker:
strategy:
matrix:
platform: [amd64, arm64]
include:
- platform: amd64
runner: blacksmith-4vcpu-ubuntu-2204
docker_platform: linux/amd64
- platform: arm64
runner: blacksmith-4vcpu-ubuntu-2204-arm
docker_platform: linux/arm64
name: Build App, then Build and Push Docker Image (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
outputs:
image_ref: ${{ steps.determine-tags.outputs.primary_tag }}
primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.primary_ghcr_manifest_tag }}
push_enabled_status: ${{ steps.context.outputs.push_enabled }}
release_type: ${{ steps.context.outputs.release_type }}
n8n_version: ${{ steps.context.outputs.n8n_version }}
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
run_install: false
- name: Setup Node.js
uses: useblacksmith/setup-node@65c6ca86fdeb0ab3d85e78f57e4f6a7e4780b391 # v5.0.4
with:
node-version: 22.x
- name: Install dependencies
run: pnpm install --frozen-lockfile
shell: bash
- name: Configure Turborepo Cache
uses: useblacksmith/caching-for-turbo@bafb57e7ebdbf1185762286ec94d24648cd3938a # v1
- name: Build n8n for Docker
run: pnpm build:n8n
shell: bash
- name: Determine build context values
id: context
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "release_type=nightly" >> $GITHUB_OUTPUT
echo "n8n_version=snapshot" >> $GITHUB_OUTPUT
echo "push_enabled=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "workflow_call" ]]; then
echo "release_type=${{ inputs.release_type }}" >> $GITHUB_OUTPUT
echo "n8n_version=${{ inputs.n8n_version }}" >> $GITHUB_OUTPUT
echo "push_enabled=${{ inputs.push_enabled }}" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
if [[ "${{ inputs.release_type }}" == "branch" ]]; then
BRANCH_NAME="${{ github.ref_name }}"
SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-' | tr -cd '[:alnum:]-_')
echo "release_type=branch" >> $GITHUB_OUTPUT
echo "n8n_version=branch-${SAFE_BRANCH_NAME}" >> $GITHUB_OUTPUT
echo "push_enabled=${{ inputs.push_to_registry }}" >> $GITHUB_OUTPUT
else
echo "release_type=${{ inputs.release_type }}" >> $GITHUB_OUTPUT
echo "n8n_version=snapshot" >> $GITHUB_OUTPUT
echo "push_enabled=true" >> $GITHUB_OUTPUT
fi
elif [[ "${{ github.event_name }}" == "push" ]]; then
echo "release_type=dev" >> $GITHUB_OUTPUT
echo "n8n_version=snapshot" >> $GITHUB_OUTPUT
echo "push_enabled=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "release_type=dev" >> $GITHUB_OUTPUT
echo "n8n_version=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
echo "push_enabled=false" >> $GITHUB_OUTPUT
else
echo "release_type=dev" >> $GITHUB_OUTPUT
echo "n8n_version=snapshot" >> $GITHUB_OUTPUT
echo "push_enabled=false" >> $GITHUB_OUTPUT
fi
- name: Determine Docker tags
id: determine-tags
run: |
RELEASE_TYPE="${{ steps.context.outputs.release_type }}"
N8N_VERSION_TAG="${{ steps.context.outputs.n8n_version }}"
GHCR_BASE="ghcr.io/${{ github.repository_owner }}/n8n"
DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}/n8n"
PLATFORM="${{ matrix.platform }}"
GHCR_TAGS_FOR_PUSH=""
DOCKER_TAGS_FOR_PUSH=""
PRIMARY_GHCR_MANIFEST_TAG_VALUE=""
PRIMARY_DOCKER_MANIFEST_TAG_VALUE=""
if [[ "$RELEASE_TYPE" == "stable" && -z "$N8N_VERSION_TAG" ]]; then
echo "Error: N8N_VERSION_TAG is empty for a stable release."
exit 1
fi
case "$RELEASE_TYPE" in
"stable")
PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}"
PRIMARY_DOCKER_MANIFEST_TAG_VALUE="${DOCKER_BASE}:${N8N_VERSION_TAG}"
GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}"
DOCKER_TAGS_FOR_PUSH="${PRIMARY_DOCKER_MANIFEST_TAG_VALUE}-${PLATFORM}"
;;
"nightly")
PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:nightly"
PRIMARY_DOCKER_MANIFEST_TAG_VALUE="${DOCKER_BASE}:nightly"
GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}"
DOCKER_TAGS_FOR_PUSH="${PRIMARY_DOCKER_MANIFEST_TAG_VALUE}-${PLATFORM}"
;;
"branch")
PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}"
GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}"
PRIMARY_DOCKER_MANIFEST_TAG_VALUE=""
DOCKER_TAGS_FOR_PUSH=""
;;
"dev"|*)
if [[ "$N8N_VERSION_TAG" == pr-* ]]; then
PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}"
GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}"
PRIMARY_DOCKER_MANIFEST_TAG_VALUE=""
DOCKER_TAGS_FOR_PUSH=""
else
PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:dev"
PRIMARY_DOCKER_MANIFEST_TAG_VALUE="${DOCKER_BASE}:dev"
GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}"
DOCKER_TAGS_FOR_PUSH="${PRIMARY_DOCKER_MANIFEST_TAG_VALUE}-${PLATFORM}"
fi
;;
esac
ALL_TAGS="${GHCR_TAGS_FOR_PUSH}"
if [[ -n "$DOCKER_TAGS_FOR_PUSH" ]]; then
ALL_TAGS="${ALL_TAGS}\n${DOCKER_TAGS_FOR_PUSH}"
fi
echo "Generated Tags for push: $ALL_TAGS"
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo -e "$ALL_TAGS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "ghcr_platform_tag=${GHCR_TAGS_FOR_PUSH}" >> $GITHUB_OUTPUT
echo "dockerhub_platform_tag=${DOCKER_TAGS_FOR_PUSH}" >> $GITHUB_OUTPUT
echo "primary_ghcr_manifest_tag=${PRIMARY_GHCR_MANIFEST_TAG_VALUE}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to GitHub Container Registry
if: steps.context.outputs.push_enabled == 'true'
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
if: steps.context.outputs.push_enabled == 'true'
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: useblacksmith/build-push-action@6fe3b1c3665ca911656e8249f6195103b7dc9782 # v1.2
with:
context: .
file: ./docker/images/n8n/Dockerfile
build-args: |
NODE_VERSION=22
N8N_VERSION=${{ steps.context.outputs.n8n_version }}
N8N_RELEASE_TYPE=${{ steps.context.outputs.release_type }}
platforms: ${{ matrix.docker_platform }}
provenance: false
push: ${{ steps.context.outputs.push_enabled }}
tags: ${{ steps.determine-tags.outputs.tags }}
create_multi_arch_manifest:
name: Create Multi-Arch Manifest
needs: build-and-push-docker
runs-on: ubuntu-latest
if: |
needs.build-and-push-docker.result == 'success' &&
needs.build-and-push-docker.outputs.push_enabled_status == 'true'
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Reconstruct Docker Hub Primary Tag
id: reconstruct_dockerhub_tag
run: |
RELEASE_TYPE="${{ needs.build-and-push-docker.outputs.release_type }}"
N8N_VERSION="${{ needs.build-and-push-docker.outputs.n8n_version }}"
DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}/n8n"
PRIMARY_DOCKER_MANIFEST_TAG=""
case "$RELEASE_TYPE" in
"stable")
PRIMARY_DOCKER_MANIFEST_TAG="${DOCKER_BASE}:${N8N_VERSION}"
;;
"nightly")
PRIMARY_DOCKER_MANIFEST_TAG="${DOCKER_BASE}:nightly"
;;
"dev")
if [[ "$N8N_VERSION" != pr-* ]]; then
PRIMARY_DOCKER_MANIFEST_TAG="${DOCKER_BASE}:dev"
fi
;;
esac
if [[ -n "$PRIMARY_DOCKER_MANIFEST_TAG" ]]; then
echo "PRIMARY_DOCKER_MANIFEST_TAG=$PRIMARY_DOCKER_MANIFEST_TAG" >> "$GITHUB_ENV"
else
echo "::notice::No Docker Hub primary tag to reconstruct for release type '$RELEASE_TYPE' and version '$N8N_VERSION'. Skipping Docker Hub manifest creation."
fi
- name: Create GHCR multi-arch manifest
if: needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag != ''
run: |
MANIFEST_TAG="${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}"
echo "Creating GHCR manifest: $MANIFEST_TAG"
# Create and push the multi-arch manifest using buildx
docker buildx imagetools create \
--tag $MANIFEST_TAG \
${MANIFEST_TAG}-amd64 \
${MANIFEST_TAG}-arm64
# Create Docker Hub multi-arch manifest
- name: Create Docker Hub multi-arch manifest
if: env.PRIMARY_DOCKER_MANIFEST_TAG != ''
run: |
MANIFEST_TAG="${{ env.PRIMARY_DOCKER_MANIFEST_TAG }}"
echo "Creating Docker Hub manifest: $MANIFEST_TAG"
# Create and push the multi-arch manifest using buildx
docker buildx imagetools create \
--tag $MANIFEST_TAG \
${MANIFEST_TAG}-amd64 \
${MANIFEST_TAG}-arm64
security-scan:
name: Security Scan
needs: [build-and-push-docker]
if: |
success() &&
(github.event_name == 'schedule' ||
(github.event_name == 'workflow_call' && inputs.release_type == 'stable'))
uses: ./.github/workflows/security-trivy-scan-callable.yml
with:
image_ref: ${{ needs.build-and-push-docker.outputs.image_ref }}
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@@ -1,83 +0,0 @@
name: Docker Custom Image CI
run-name: Build ${{ inputs.branch }} - ${{ inputs.user }}
on:
workflow_dispatch:
inputs:
branch:
description: 'GitHub branch to create image off.'
required: true
tag:
description: 'Name of the docker tag to create.'
required: true
merge-master:
description: 'Merge with master.'
type: boolean
required: true
default: false
user:
description: ''
required: false
default: 'none'
start-url:
description: 'URL to call after workflow is kicked off.'
required: false
default: ''
success-url:
description: 'URL to call after Docker Image got built successfully.'
required: false
default: ''
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Call Start URL - optionally
if: ${{ github.event.inputs.start-url != '' }}
run: curl -v -X POST -d 'url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' ${{github.event.inputs.start-url}} || echo ""
shell: bash
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: Merge Master - optionally
if: github.event.inputs.merge-master
run: git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo ""
shell: bash
- name: Set up QEMU
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
- name: Login to GHCR
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image to GHCR
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0
env:
DOCKER_BUILD_SUMMARY: false
with:
context: .
file: ./docker/images/n8n/Dockerfile
build-args: |
N8N_RELEASE_TYPE=development
platforms: linux/amd64
provenance: false
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ghcr.io/${{ github.repository_owner }}/n8n:${{ inputs.tag }}
- name: Call Success URL - optionally
if: ${{ github.event.inputs.success-url != '' }}
run: curl -v ${{github.event.inputs.success-url}} || echo ""
shell: bash

View File

@@ -1,58 +0,0 @@
name: Docker Nightly Image CI
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up QEMU
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
- name: Login to GHCR
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push image to GHCR and DockerHub
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0
env:
DOCKER_BUILD_SUMMARY: false
with:
context: .
file: ./docker/images/n8n/Dockerfile
build-args: |
N8N_RELEASE_TYPE=nightly
platforms: linux/amd64,linux/arm64
provenance: false
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
ghcr.io/${{ github.repository_owner }}/n8n:nightly
${{ secrets.DOCKER_USERNAME }}/n8n:nightly
security-scan:
needs: build
uses: ./.github/workflows/security-trivy-scan-callable.yml
with:
image_ref: ghcr.io/${{ github.repository_owner }}/n8n:nightly
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@@ -72,62 +72,11 @@ jobs:
publish-to-docker-hub:
name: Publish to DockerHub
needs: [publish-to-npm]
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0
env:
DOCKER_BUILD_SUMMARY: false
with:
context: .
file: docker/images/n8n/Dockerfile
build-args: |
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
N8N_RELEASE_TYPE=stable
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
provenance: false
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.publish-to-npm.outputs.release }}
ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.publish-to-npm.outputs.release }}
security-scan:
name: Security Scan Release Image
needs: [publish-to-npm, publish-to-docker-hub]
uses: ./.github/workflows/security-trivy-scan-callable.yml
uses: ./.github/workflows/docker-build-push.yml
with:
image_ref: ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.publish-to-npm.outputs.release }}
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
n8n_version: ${{ needs.publish-to-npm.outputs.release }}
release_type: stable
secrets: inherit
create-github-release:
name: Create a GitHub Release

4
.gitignore vendored
View File

@@ -26,3 +26,7 @@ build-storybook.log
junit.xml
test-results.json
*.0x
compiled_app_output
trivy_report*
compiled
jest.config.js

View File

@@ -1,37 +1,34 @@
ARG NODE_VERSION=22
# 1. Use a builder step to download various dependencies
# ==============================================================================
# STAGE 1: Builder for Base Dependencies
# ==============================================================================
FROM node:${NODE_VERSION}-alpine AS builder
# Install fonts
RUN \
apk --no-cache add --virtual fonts msttcorefonts-installer fontconfig && \
update-ms-fonts && \
fc-cache -f && \
apk del fonts && \
find /usr/share/fonts/truetype/msttcorefonts/ -type l -exec unlink {} \;
RUN \
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 {} \;
# Install git and other OS dependencies
RUN apk add --update git openssh graphicsmagick tini tzdata ca-certificates libc6-compat jq
# Install essential OS dependencies
RUN apk add --no-cache git openssh graphicsmagick tini tzdata ca-certificates libc6-compat jq
# Update npm and install full-uci
COPY .npmrc /usr/local/etc/npmrc
RUN npm install -g corepack@0.33 full-icu@1.5.0
# Update npm, install full-icu and npm@11.4.2 to fix brace-expansion vulnerability
# Remove npm update after vulnerability is fixed in in node image
RUN npm install -g full-icu@1.5.0 npm@11.4.2
# Activate corepack, and install pnpm
WORKDIR /tmp
COPY package.json ./
RUN corepack enable && corepack prepare --activate
RUN apk del apk-tools && \
rm -rf /tmp/* /root/.npm /root/.cache/node /opt/yarn* /var/cache/apk/* /lib/apk/db
# Cleanup
RUN rm -rf /lib/apk/db /var/cache/apk/ /tmp/* /root/.npm /root/.cache/node /opt/yarn*
# 2. Start with a new clean image and copy over the added files into a single layer
# ==============================================================================
# STAGE 2: Final Base Runtime Image
# ==============================================================================
FROM node:${NODE_VERSION}-alpine
COPY --from=builder / /
# Delete this folder to make the base image backward compatible to be able to build older version images
RUN rm -rf /tmp/v8-compile-cache*
COPY --from=builder / /
WORKDIR /home/node
ENV NODE_ICU_DATA=/usr/local/lib/node_modules/full-icu

View File

@@ -1,81 +1,75 @@
ARG NODE_VERSION=22
# 1. Create an image to build n8n
FROM --platform=linux/amd64 n8nio/base:${NODE_VERSION} AS builder
# Build the application from source
WORKDIR /src
COPY . /src
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store --mount=type=cache,id=pnpm-metadata,target=/root/.cache/pnpm/metadata DOCKER_BUILD=true pnpm install --frozen-lockfile
RUN pnpm build
# Delete all dev dependencies
RUN node .github/scripts/trim-fe-packageJson.js
# We don't want to remove all patches because we want them still to be applied
# in `pnpm deploy`. However, we need to remove FE patches because we trim the FE
# package.json files and `pnpm deploy` will fail otherwise. element-plus is the
# only FE patch that we need to remove.
RUN jq '.pnpm.patchedDependencies |= with_entries(select(.key | startswith("pdfjs-dist") or startswith("pkce-challenge")))' package.json > package.json.tmp; mv package.json.tmp package.json
# Delete any source code or typings
RUN find . -type f -name "*.ts" -o -name "*.vue" -o -name "tsconfig.json" -o -name "*.tsbuildinfo" | xargs rm -rf
# Deploy the `n8n` package into /compiled
RUN mkdir /compiled
RUN NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n --prod --no-optional --legacy deploy /compiled
# 2. Start with a new clean image with just the code that is needed to run n8n
FROM n8nio/base:${NODE_VERSION}
ENV NODE_ENV=production
ARG N8N_VERSION=snapshot
ARG N8N_RELEASE_TYPE=dev
ENV N8N_RELEASE_TYPE=${N8N_RELEASE_TYPE}
ARG LAUNCHER_VERSION=1.1.3
ARG TARGETPLATFORM
LABEL org.opencontainers.image.title="n8n"
LABEL org.opencontainers.image.description="Workflow Automation Tool"
LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n"
LABEL org.opencontainers.image.url="https://n8n.io"
LABEL org.opencontainers.image.version=${N8N_VERSION}
# ==============================================================================
# STAGE 1: System Dependencies & Base Setup
# ==============================================================================
FROM n8nio/base:${NODE_VERSION} AS system-deps
# ==============================================================================
# STAGE 2: Application Artifact Processor
# ==============================================================================
FROM alpine:3.22.0 AS app-artifact-processor
COPY ./compiled /app/
# ==============================================================================
# STAGE 3: Task Runner Launcher
# ==============================================================================
FROM alpine:3.22.0 AS launcher-downloader
ARG TARGETPLATFORM
ARG LAUNCHER_VERSION
RUN set -e; \
case "$TARGETPLATFORM" in \
"linux/amd64") ARCH_NAME="amd64" ;; \
"linux/arm64") ARCH_NAME="arm64" ;; \
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
esac; \
mkdir /launcher-temp && cd /launcher-temp; \
wget -q "https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz"; \
wget -q "https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256"; \
echo "$(cat task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256) task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz" > checksum.sha256; \
sha256sum -c checksum.sha256; \
mkdir -p /launcher-bin; \
tar xzf task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz -C /launcher-bin; \
cd / && rm -rf /launcher-temp
# ==============================================================================
# STAGE 4: Final Runtime Image
# ==============================================================================
FROM system-deps AS runtime
ARG N8N_VERSION
ARG N8N_RELEASE_TYPE=dev
ENV NODE_ENV=production
ENV N8N_RELEASE_TYPE=${N8N_RELEASE_TYPE}
ENV NODE_ICU_DATA=/usr/local/lib/node_modules/full-icu
ENV SHELL=/bin/sh
WORKDIR /home/node
COPY --from=builder /compiled /usr/local/lib/node_modules/n8n
COPY --from=app-artifact-processor /app /usr/local/lib/node_modules/n8n
COPY --from=launcher-downloader /launcher-bin/* /usr/local/bin/
COPY docker/images/n8n/docker-entrypoint.sh /
# Setup the Task Runner Launcher
ARG TARGETPLATFORM
ARG LAUNCHER_VERSION=1.1.3
COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json
# Download, verify, then extract the launcher binary
RUN \
if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="amd64"; \
elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="arm64"; fi; \
mkdir /launcher-temp && \
cd /launcher-temp && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz && \
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256 && \
# The .sha256 does not contain the filename --> Form the correct checksum file
echo "$(cat task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256) task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz" > checksum.sha256 && \
sha256sum -c checksum.sha256 && \
tar xvf task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz --directory=/usr/local/bin && \
cd - && \
rm -r /launcher-temp
RUN \
cd /usr/local/lib/node_modules/n8n && \
npm rebuild sqlite3 && \
cd - && \
ln -s /usr/local/lib/node_modules/n8n/bin/n8n /usr/local/bin/n8n && \
mkdir .n8n && \
chown node:node .n8n
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
# pdfjs-dist has an optional dependency on @napi-rs/canvas, which is required
# for it to work.
# Install npm@11.4.2 to fix brace-expansion vulnerability, remove after vulnerability is fixed in node image
RUN npm install -g npm@11.4.2
RUN cd /usr/local/lib/node_modules/n8n/node_modules/pdfjs-dist && npm install @napi-rs/canvas
# Install npm 11.4.1 to fix the vulnerable cross-spawn dependency
RUN npm install -g npm@11.4.1
ENV SHELL /bin/sh
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}

View File

@@ -14,6 +14,9 @@
"build:backend": "turbo run build:backend",
"build:frontend": "turbo run build:frontend",
"build:nodes": "turbo run build:nodes",
"build:n8n": "node scripts/build-n8n.mjs",
"build:docker": "node scripts/build-n8n.mjs && node scripts/dockerize-n8n.mjs",
"build:docker:scan": "node scripts/build-n8n.mjs && node scripts/dockerize-n8n.mjs && node scripts/scan-n8n-image.mjs",
"typecheck": "turbo typecheck",
"dev": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner",
"dev:be": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui",
@@ -95,7 +98,9 @@
"vue-tsc": "^2.2.8",
"google-gax": "^4.3.7",
"ws": ">=8.17.1",
"zod": "3.25.67"
"zod": "3.25.67",
"brace-expansion@1": "1.1.12",
"brace-expansion@2": "2.0.2"
},
"patchedDependencies": {
"bull@4.16.4": "patches/bull@4.16.4.patch",

View File

@@ -49,8 +49,7 @@
"files": [
"bin",
"templates",
"dist",
"!dist/**/e2e.*"
"dist"
],
"devDependencies": {
"@n8n/typescript-config": "workspace:*",

118
pnpm-lock.yaml generated
View File

@@ -187,6 +187,8 @@ overrides:
google-gax: ^4.3.7
ws: '>=8.17.1'
zod: 3.25.67
brace-expansion@1: 1.1.12
brace-expansion@2: 2.0.2
patchedDependencies:
'@types/express-serve-static-core@5.0.6':
@@ -5305,73 +5307,73 @@ packages:
'@n8n_io/riot-tmpl@4.0.1':
resolution: {integrity: sha512-/zdRbEfTFjsm1NqnpPQHgZTkTdbp5v3VUxGeMA9098sps8jRCTraQkc3AQstJgHUm7ylBXJcIVhnVeLUMWAfwQ==}
'@napi-rs/canvas-android-arm64@0.1.70':
resolution: {integrity: sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==}
'@napi-rs/canvas-android-arm64@0.1.71':
resolution: {integrity: sha512-cxi3VCotIOS9kNFQI7dcysbVJi106pxryVY1Hi85pX+ZeqahRyeqc/NsLaZ998Ae99+F3HI5X/39G1Y/Byrf0A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.70':
resolution: {integrity: sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==}
'@napi-rs/canvas-darwin-arm64@0.1.71':
resolution: {integrity: sha512-7Y4D/6vIuMLYsVNtRM/w2j0+fB1GyqeOxc7I0BTx8eLP1S6BZE2Rj6zJfdG+zmLEOW0IlHa+VQq1q2MUAjW84w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.70':
resolution: {integrity: sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==}
'@napi-rs/canvas-darwin-x64@0.1.71':
resolution: {integrity: sha512-Z0IUqxclrYdfVt/SK9nKCzUHTOXKTWiygtO71YCzs0OtxKdNI7GJRJdYG48wXZEDQ/pqTF4F7Ifgtidfc2tYpg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.70':
resolution: {integrity: sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==}
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.71':
resolution: {integrity: sha512-KlpqqCASak5ruY+UIolJgmhMZ9Pa2o1QyaNu648L8sz4WNBbNa+aOT60XCLCL1VIKLv11B3MlNgiOHoYNmDhXQ==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.70':
resolution: {integrity: sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==}
'@napi-rs/canvas-linux-arm64-gnu@0.1.71':
resolution: {integrity: sha512-bdGZCGu8YQNAiu3nkIVVUp6nIn6fPd36IuZsLXTG027E52KyIuZ3obCxehSwjDIUNkFWvmff5D6JYfWwAoioEw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.70':
resolution: {integrity: sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==}
'@napi-rs/canvas-linux-arm64-musl@0.1.71':
resolution: {integrity: sha512-1R5sMWe9ur8uM+hAeylBwG0b6UHDR+iWQNgzXmF9vbBYRooQvmDWqpcgytKLJAC0vnWhIkKwqd7yExn7cwczmg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.70':
resolution: {integrity: sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==}
'@napi-rs/canvas-linux-riscv64-gnu@0.1.71':
resolution: {integrity: sha512-xjjKsipueuG+LdKIk6/uAlqdo+rzGcmNpTZPXdakIT1sHX4NNSnQTzjRaj9Gh96Czjd9G89UWR0KIlE7fwOgFA==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.70':
resolution: {integrity: sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==}
'@napi-rs/canvas-linux-x64-gnu@0.1.71':
resolution: {integrity: sha512-3s6YpklXDB4OeeULG1XTRyKrKAOo7c3HHEqM9A6N4STSjMaJtzmpp7tB/JTvAFeOeFte6gWN8IwC+7AjGJ6MpQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.70':
resolution: {integrity: sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==}
'@napi-rs/canvas-linux-x64-musl@0.1.71':
resolution: {integrity: sha512-5v9aCLzCXw7u10ray5juQMdl7TykZSn1X5AIGYwBvTAcKSgrqaR9QkRxp1Lqk3njQmFekOW1SFN9bZ/i/6y6kA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-x64-msvc@0.1.70':
resolution: {integrity: sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==}
'@napi-rs/canvas-win32-x64-msvc@0.1.71':
resolution: {integrity: sha512-oJughk6xjsRIr0Rd9EqjmZmhIMkvcPuXgr3MNn2QexTqn+YFOizrwHS5ha0BDfFl7TEGRvwaDUXBQtu8JKXb8A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.70':
resolution: {integrity: sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==}
'@napi-rs/canvas@0.1.71':
resolution: {integrity: sha512-92ybDocKl6JM48ZpYbj+A7Qt45IaTABDk0y3sDecEQfgdhfNzJtEityqNHoCZ4Vty2dldPkJhxgvOnbrQMXTTA==}
engines: {node: '>= 10'}
'@ngneat/falso@7.4.0':
@@ -7822,11 +7824,11 @@ packages:
bowser@2.11.0:
resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==}
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -18051,48 +18053,48 @@ snapshots:
dependencies:
eslint-config-riot: 1.0.0
'@napi-rs/canvas-android-arm64@0.1.70':
'@napi-rs/canvas-android-arm64@0.1.71':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.70':
'@napi-rs/canvas-darwin-arm64@0.1.71':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.70':
'@napi-rs/canvas-darwin-x64@0.1.71':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.70':
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.71':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.70':
'@napi-rs/canvas-linux-arm64-gnu@0.1.71':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.70':
'@napi-rs/canvas-linux-arm64-musl@0.1.71':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.70':
'@napi-rs/canvas-linux-riscv64-gnu@0.1.71':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.70':
'@napi-rs/canvas-linux-x64-gnu@0.1.71':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.70':
'@napi-rs/canvas-linux-x64-musl@0.1.71':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.70':
'@napi-rs/canvas-win32-x64-msvc@0.1.71':
optional: true
'@napi-rs/canvas@0.1.70':
'@napi-rs/canvas@0.1.71':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.70
'@napi-rs/canvas-darwin-arm64': 0.1.70
'@napi-rs/canvas-darwin-x64': 0.1.70
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.70
'@napi-rs/canvas-linux-arm64-gnu': 0.1.70
'@napi-rs/canvas-linux-arm64-musl': 0.1.70
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.70
'@napi-rs/canvas-linux-x64-gnu': 0.1.70
'@napi-rs/canvas-linux-x64-musl': 0.1.70
'@napi-rs/canvas-win32-x64-msvc': 0.1.70
'@napi-rs/canvas-android-arm64': 0.1.71
'@napi-rs/canvas-darwin-arm64': 0.1.71
'@napi-rs/canvas-darwin-x64': 0.1.71
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.71
'@napi-rs/canvas-linux-arm64-gnu': 0.1.71
'@napi-rs/canvas-linux-arm64-musl': 0.1.71
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.71
'@napi-rs/canvas-linux-x64-gnu': 0.1.71
'@napi-rs/canvas-linux-x64-musl': 0.1.71
'@napi-rs/canvas-win32-x64-msvc': 0.1.71
optional: true
'@ngneat/falso@7.4.0':
@@ -21258,12 +21260,12 @@ snapshots:
bowser@2.11.0: {}
brace-expansion@1.1.11:
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.0.1:
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
@@ -22898,7 +22900,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.0
debug: 4.4.1(supports-color@8.1.1)
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.2.2
@@ -25435,31 +25437,31 @@ snapshots:
minimatch@10.0.1:
dependencies:
brace-expansion: 2.0.1
brace-expansion: 2.0.2
minimatch@3.0.8:
dependencies:
brace-expansion: 1.1.11
brace-expansion: 1.1.12
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.11
brace-expansion: 1.1.12
minimatch@5.1.5:
dependencies:
brace-expansion: 2.0.1
brace-expansion: 2.0.2
minimatch@9.0.1:
dependencies:
brace-expansion: 2.0.1
brace-expansion: 2.0.2
minimatch@9.0.3:
dependencies:
brace-expansion: 2.0.1
brace-expansion: 2.0.2
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
brace-expansion: 2.0.2
minimist@1.2.8: {}
@@ -26554,7 +26556,7 @@ snapshots:
pdfjs-dist@5.3.31(patch_hash=421253c8e411cdaef58ba96d2bb44ae0784e1b3e446f5caca50710daa1fa5dcd):
optionalDependencies:
'@napi-rs/canvas': 0.1.70
'@napi-rs/canvas': 0.1.71
peberminta@0.9.0: {}

219
scripts/build-n8n.mjs Executable file
View File

@@ -0,0 +1,219 @@
#!/usr/bin/env node
/**
* This script is used to build the n8n application for production.
* It will:
* 1. Clean the previous build output
* 2. Run pnpm install and build
* 3. Prepare for deployment - clean package.json files
* 4. Create a pruned production deployment in 'compiled'
*/
import { $, echo, fs, chalk } from 'zx';
import path from 'path';
// Check if running in a CI environment
const isCI = process.env.CI === 'true';
// Disable verbose output and force color only if not in CI
$.verbose = !isCI;
process.env.FORCE_COLOR = isCI ? '0' : '1';
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
const isInScriptsDir = path.basename(scriptDir) === 'scripts';
const rootDir = isInScriptsDir ? path.join(scriptDir, '..') : scriptDir;
// --- Configuration ---
const config = {
compiledAppDir: process.env.BUILD_OUTPUT_DIR || path.join(rootDir, 'compiled'),
rootDir: rootDir,
};
// Define backend patches to keep during deployment
const PATCHES_TO_KEEP = ['pdfjs-dist', 'pkce-challenge', 'bull'];
// --- Helper Functions ---
const timers = new Map();
function startTimer(name) {
timers.set(name, Date.now());
}
function getElapsedTime(name) {
const start = timers.get(name);
if (!start) return 0;
return Math.floor((Date.now() - start) / 1000);
}
function formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) return `${hours}h ${minutes}m ${secs}s`;
if (minutes > 0) return `${minutes}m ${secs}s`;
return `${secs}s`;
}
function printHeader(title) {
echo('');
echo(chalk.blue.bold(`===== ${title} =====`));
}
function printDivider() {
echo(chalk.gray('-----------------------------------------------'));
}
// --- Main Build Process ---
printHeader('n8n Build & Production Preparation');
echo(`INFO: Output Directory: ${config.compiledAppDir}`);
printDivider();
startTimer('total_build');
// 0. Clean Previous Build Output
echo(chalk.yellow(`INFO: Cleaning previous output directory: ${config.compiledAppDir}...`));
await fs.remove(config.compiledAppDir);
printDivider();
// 1. Local Application Pre-build
echo(chalk.yellow('INFO: Starting local application pre-build...'));
startTimer('package_build');
echo(chalk.yellow('INFO: Running pnpm install and build...'));
await $`cd ${config.rootDir} && pnpm install --frozen-lockfile`;
await $`cd ${config.rootDir} && pnpm build`;
echo(chalk.green('✅ pnpm install and build completed'));
const packageBuildTime = getElapsedTime('package_build');
echo(chalk.green(`✅ Package build completed in ${formatDuration(packageBuildTime)}`));
printDivider();
// 2. Prepare for deployment - clean package.json files
echo(chalk.yellow('INFO: Performing pre-deploy cleanup on package.json files...'));
// Find and backup package.json files
const packageJsonFiles = await $`cd ${config.rootDir} && find . -name "package.json" \
-not -path "./node_modules/*" \
-not -path "*/node_modules/*" \
-not -path "./compiled/*" \
-type f`.lines();
// Backup all package.json files
// This is only needed locally, not in CI
if (process.env.CI !== 'true') {
for (const file of packageJsonFiles) {
if (file) {
const fullPath = path.join(config.rootDir, file);
await fs.copy(fullPath, `${fullPath}.bak`);
}
}
}
// Run FE trim script
await $`cd ${config.rootDir} && node .github/scripts/trim-fe-packageJson.js`;
echo(chalk.yellow('INFO: Performing selective patch cleanup...'));
const packageJsonPath = path.join(config.rootDir, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
try {
// 1. Read the package.json file
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
let packageJson = JSON.parse(packageJsonContent);
// 2. Modify the patchedDependencies directly in JavaScript
if (packageJson.pnpm && packageJson.pnpm.patchedDependencies) {
const filteredPatches = {};
for (const [key, value] of Object.entries(packageJson.pnpm.patchedDependencies)) {
// Check if the key (patch name) starts with any of the allowed patches
const shouldKeep = PATCHES_TO_KEEP.some((patchPrefix) => key.startsWith(patchPrefix));
if (shouldKeep) {
filteredPatches[key] = value;
}
}
packageJson.pnpm.patchedDependencies = filteredPatches;
}
// 3. Write the modified package.json back
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
echo(chalk.green('✅ Kept backend patches: ' + PATCHES_TO_KEEP.join(', ')));
echo(
chalk.gray(
`Removed FE/dev patches that are not in the list of backend patches to keep: ${PATCHES_TO_KEEP.join(', ')}`,
),
);
} catch (error) {
echo(chalk.red(`ERROR: Failed to cleanup patches in package.json: ${error.message}`));
process.exit(1);
}
}
echo(chalk.yellow(`INFO: Creating pruned production deployment in '${config.compiledAppDir}'...`));
startTimer('package_deploy');
await fs.ensureDir(config.compiledAppDir);
await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n --prod --legacy deploy --no-optional ./compiled`;
const packageDeployTime = getElapsedTime('package_deploy');
// Restore package.json files
// This is only needed locally, not in CI
if (process.env.CI !== 'true') {
for (const file of packageJsonFiles) {
if (file) {
const fullPath = path.join(config.rootDir, file);
const backupPath = `${fullPath}.bak`;
if (await fs.pathExists(backupPath)) {
await fs.move(backupPath, fullPath, { overwrite: true });
}
}
}
}
// Calculate output size
const compiledAppOutputSize = (await $`du -sh ${config.compiledAppDir} | cut -f1`).stdout.trim();
// Generate build manifest
const buildManifest = {
buildTime: new Date().toISOString(),
artifactSize: compiledAppOutputSize,
buildDuration: {
packageBuild: packageBuildTime,
packageDeploy: packageDeployTime,
total: getElapsedTime('total_build'),
},
};
await fs.writeJson(path.join(config.compiledAppDir, 'build-manifest.json'), buildManifest, {
spaces: 2,
});
echo(chalk.green(`✅ Package deployment completed in ${formatDuration(packageDeployTime)}`));
echo(`INFO: Size of ${config.compiledAppDir}: ${compiledAppOutputSize}`);
printDivider();
// Calculate total time
const totalBuildTime = getElapsedTime('total_build');
// --- Final Output ---
echo('');
echo(chalk.green.bold('================ BUILD SUMMARY ================'));
echo(chalk.green(`✅ n8n built successfully!`));
echo('');
echo(chalk.blue('📦 Build Output:'));
echo(` Directory: ${path.resolve(config.compiledAppDir)}`);
echo(` Size: ${compiledAppOutputSize}`);
echo('');
echo(chalk.blue('⏱️ Build Times:'));
echo(` Package Build: ${formatDuration(packageBuildTime)}`);
echo(` Package Deploy: ${formatDuration(packageDeployTime)}`);
echo(chalk.gray(' -----------------------------'));
echo(chalk.bold(` Total Time: ${formatDuration(totalBuildTime)}`));
echo('');
echo(chalk.blue('📋 Build Manifest:'));
echo(` ${path.resolve(config.compiledAppDir)}/build-manifest.json`);
echo(chalk.green.bold('=============================================='));
// Exit with success
process.exit(0);

88
scripts/dockerize-n8n.mjs Executable file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env node
/**
* This script is used to build the n8n docker image locally.
* It simulates how we build for CI and should allow for local testing.
* By default it outputs the tag 'dev' and the image name 'n8n-local:dev'.
* It can be overridden by setting the IMAGE_BASE_NAME and IMAGE_TAG environment variables.
*/
import { $, echo, fs, chalk } from 'zx';
import path from 'path';
// Disable verbose mode for cleaner output
$.verbose = false;
process.env.FORCE_COLOR = '1';
// --- Determine script location ---
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
const isInScriptsDir = path.basename(scriptDir) === 'scripts';
const rootDir = isInScriptsDir ? path.join(scriptDir, '..') : scriptDir;
// --- Configuration ---
const config = {
dockerfilePath: path.join(rootDir, 'docker/images/n8n/Dockerfile'),
imageBaseName: process.env.IMAGE_BASE_NAME || 'n8n-local',
imageTag: process.env.IMAGE_TAG || 'dev',
buildContext: rootDir,
compiledAppDir: path.join(rootDir, 'compiled'),
};
config.fullImageName = `${config.imageBaseName}:${config.imageTag}`;
// --- Check Prerequisites ---
echo(chalk.blue.bold('===== Docker Build for n8n ====='));
echo(`INFO: Image: ${config.fullImageName}`);
echo(chalk.gray('-----------------------------------------------'));
// Check if compiled directory exists
if (!(await fs.pathExists(config.compiledAppDir))) {
echo(chalk.red(`Error: Compiled app directory not found at ${config.compiledAppDir}`));
echo(chalk.yellow('Please run build-n8n.mjs first!'));
process.exit(1);
}
// Check Docker
try {
await $`command -v docker`;
} catch {
echo(chalk.red('Error: Docker is not installed or not in PATH'));
process.exit(1);
}
// --- Build Docker Image ---
const startTime = Date.now();
echo(chalk.yellow('INFO: Building Docker image...'));
try {
const buildOutput = await $`docker build \
-t ${config.fullImageName} \
-f ${config.dockerfilePath} \
${config.buildContext}`;
echo(buildOutput.stdout);
} catch (error) {
echo(chalk.red(`ERROR: Docker build failed: ${error.stderr || error.message}`));
process.exit(1);
}
const buildTime = Math.floor((Date.now() - startTime) / 1000);
// Get image size
let imageSize = 'Unknown';
try {
const sizeOutput = await $`docker images ${config.fullImageName} --format "{{.Size}}"`;
imageSize = sizeOutput.stdout.trim();
} catch (error) {
echo(chalk.yellow('Warning: Could not get image size'));
}
// --- Summary ---
echo('');
echo(chalk.green.bold('================ DOCKER BUILD COMPLETE ================'));
echo(chalk.green(`✅ Image built: ${config.fullImageName}`));
echo(` Size: ${imageSize}`);
echo(` Build time: ${buildTime}s`);
echo(chalk.green.bold('===================================================='));
// Exit with success
process.exit(0);

152
scripts/scan-n8n-image.mjs Executable file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env node
/**
* This script is used to scan the n8n docker image for vulnerabilities.
* It uses Trivy to scan the image.
*/
import { $, echo, fs, chalk } from 'zx';
import path from 'path';
$.verbose = false;
process.env.FORCE_COLOR = '1';
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
const isInScriptsDir = path.basename(scriptDir) === 'scripts';
const rootDir = isInScriptsDir ? path.join(scriptDir, '..') : scriptDir;
// --- Configuration ---
const config = {
imageBaseName: process.env.IMAGE_BASE_NAME || 'n8n-local',
imageTag: process.env.IMAGE_TAG || 'dev',
trivyImage: process.env.TRIVY_IMAGE || 'aquasec/trivy:latest',
severity: process.env.TRIVY_SEVERITY || 'CRITICAL,HIGH,MEDIUM,LOW',
outputFormat: process.env.TRIVY_FORMAT || 'table',
outputFile: process.env.TRIVY_OUTPUT || null,
scanTimeout: process.env.TRIVY_TIMEOUT || '10m',
ignoreUnfixed: process.env.TRIVY_IGNORE_UNFIXED === 'true',
scanners: process.env.TRIVY_SCANNERS || 'vuln',
quiet: process.env.TRIVY_QUIET === 'true',
rootDir: rootDir,
};
config.fullImageName = `${config.imageBaseName}:${config.imageTag}`;
const printHeader = (title) =>
!config.quiet && echo(`\n${chalk.blue.bold(`===== ${title} =====`)}`);
const printSummary = (status, time, message) => {
if (config.quiet) return;
echo('\n' + chalk.blue.bold('===== Scan Summary ====='));
echo(status === 'success' ? chalk.green.bold(message) : chalk.yellow.bold(message));
echo(chalk[status === 'success' ? 'green' : 'yellow'](` Scan time: ${time}s`));
if (config.outputFile) {
const resolvedPath = path.isAbsolute(config.outputFile)
? config.outputFile
: path.join(config.rootDir, config.outputFile);
echo(chalk[status === 'success' ? 'green' : 'yellow'](` Report saved to: ${resolvedPath}`));
}
echo('\n' + chalk.gray('Scan Configuration:'));
echo(chalk.gray(` • Target Image: ${config.fullImageName}`));
echo(chalk.gray(` • Severity Levels: ${config.severity}`));
echo(chalk.gray(` • Scanners: ${config.scanners}`));
if (config.ignoreUnfixed) echo(chalk.gray(` • Ignored unfixed: yes`));
echo(chalk.blue.bold('========================'));
};
// --- Main Process ---
(async () => {
printHeader('Trivy Security Scan for n8n Image');
try {
await $`command -v docker`;
} catch {
echo(chalk.red('Error: Docker is not installed or not in PATH'));
process.exit(1);
}
try {
await $`docker image inspect ${config.fullImageName} > /dev/null 2>&1`;
} catch {
echo(chalk.red(`Error: Docker image '${config.fullImageName}' not found`));
echo(chalk.yellow('Please run dockerize-n8n.mjs first!'));
process.exit(1);
}
// Pull latest Trivy image silently
try {
await $`docker pull ${config.trivyImage} > /dev/null 2>&1`;
} catch {
// Silent fallback to cached version
}
// Build Trivy command
const trivyArgs = [
'run',
'--rm',
'-v',
'/var/run/docker.sock:/var/run/docker.sock',
config.trivyImage,
'image',
'--severity',
config.severity,
'--format',
config.outputFormat,
'--timeout',
config.scanTimeout,
'--scanners',
config.scanners,
'--no-progress',
];
if (config.ignoreUnfixed) trivyArgs.push('--ignore-unfixed');
if (config.quiet && config.outputFormat === 'table') trivyArgs.push('--quiet');
// Handle output file - resolve relative to root directory
if (config.outputFile) {
const outputPath = path.isAbsolute(config.outputFile)
? config.outputFile
: path.join(config.rootDir, config.outputFile);
await fs.ensureDir(path.dirname(outputPath));
trivyArgs.push('--output', '/tmp/trivy-output', '-v', `${outputPath}:/tmp/trivy-output`);
}
trivyArgs.push(config.fullImageName);
// Run the scan
const startTime = Date.now();
try {
const result = await $`docker ${trivyArgs}`;
// Print Trivy output first
if (!config.outputFile && result.stdout) {
echo(result.stdout);
}
// Then print our summary
const scanTime = Math.floor((Date.now() - startTime) / 1000);
printSummary('success', scanTime, '✅ Security scan completed successfully');
process.exit(0);
} catch (error) {
const scanTime = Math.floor((Date.now() - startTime) / 1000);
// Trivy returns exit code 1 when vulnerabilities are found
if (error.exitCode === 1) {
// Print Trivy output first
if (!config.outputFile && error.stdout) {
echo(error.stdout);
}
// Then print our summary
printSummary('warning', scanTime, '⚠️ Vulnerabilities found!');
process.exit(1);
} else {
echo(chalk.red(`❌ Scan failed: ${error.message}`));
process.exit(error.exitCode || 1);
}
}
})();