From 908a810e07163262b99c79560027aaafb344ff6a Mon Sep 17 00:00:00 2001 From: Declan Carroll Date: Mon, 9 Mar 2026 08:40:40 +0000 Subject: [PATCH] fix: Remove isolated-vm from Dockerfile npm rebuild (#26745) Co-authored-by: Claude Opus 4.6 --- .github/workflows/docker-build-smoke.yml | 48 ++++++++++++++++++++++++ docker/images/n8n/Dockerfile | 2 +- package.json | 1 + scripts/dockerize-n8n.mjs | 34 ++++++++++++++++- 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/docker-build-smoke.yml diff --git a/.github/workflows/docker-build-smoke.yml b/.github/workflows/docker-build-smoke.yml new file mode 100644 index 00000000000..5e130e3ebfe --- /dev/null +++ b/.github/workflows/docker-build-smoke.yml @@ -0,0 +1,48 @@ +name: 'Docker Build Smoke Test' + +# Verifies the full Docker build chain works without any caching. +# Catches native module compilation failures (e.g., isolated-vm, sqlite3) +# that layer caching can mask in the regular E2E pipeline. +# +# Full chain: pnpm install → pnpm build (no Turbo cache) → +# build base image (no Docker cache) → +# build n8n + runners images (no Docker cache) + +on: + pull_request: + paths: + - 'docker/images/n8n/**' + - 'docker/images/n8n-base/**' + - 'docker/images/runners/**' + - 'scripts/build-n8n.mjs' + - 'scripts/dockerize-n8n.mjs' + workflow_dispatch: + +jobs: + docker-smoke-test: + name: 'Docker Build (no cache)' + runs-on: blacksmith-4vcpu-ubuntu-2204 + if: ${{ !github.event.pull_request.head.repo.fork }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Login to DHI Registry (for base image) + uses: ./.github/actions/docker-registry-login + with: + login-ghcr: 'false' + login-dhi: 'true' + dockerhub-username: ${{ secrets.DOCKER_USERNAME }} + dockerhub-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build full chain (no cache) + uses: ./.github/actions/setup-nodejs + with: + build-command: 'pnpm build:docker:clean' + enable-docker-cache: true + + - name: Verify n8n image starts + run: | + docker run --rm -d --name n8n-smoke-test n8nio/n8n:local + sleep 5 + docker logs n8n-smoke-test 2>&1 | tail -20 + docker stop n8n-smoke-test diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index 39edf9dcb86..a1caea8f368 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -15,7 +15,7 @@ COPY ./compiled /usr/local/lib/node_modules/n8n COPY docker/images/n8n/docker-entrypoint.sh / RUN cd /usr/local/lib/node_modules/n8n && \ - npm rebuild sqlite3 isolated-vm && \ + npm rebuild sqlite3 && \ 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 && \ diff --git a/package.json b/package.json index c451e5de554..e2533f786b5 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "build:docker": "node scripts/build-n8n.mjs && node scripts/dockerize-n8n.mjs", "build:docker:coverage": "BUILD_WITH_COVERAGE=true 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", + "build:docker:clean": "TURBO_FORCE=true node scripts/build-n8n.mjs && DOCKER_BUILD_NO_CACHE=true DOCKER_BUILD_BASE_IMAGE=true node scripts/dockerize-n8n.mjs", "build:docker:test": "node scripts/build-n8n.mjs && node scripts/dockerize-n8n.mjs && turbo run test:container:standard --filter=n8n-playwright", "typecheck": "turbo typecheck", "dev": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner", diff --git a/scripts/dockerize-n8n.mjs b/scripts/dockerize-n8n.mjs index 9c83f9d39ad..1fe86079d52 100755 --- a/scripts/dockerize-n8n.mjs +++ b/scripts/dockerize-n8n.mjs @@ -119,7 +119,17 @@ const __dirname = path.dirname(__filename); const isInScriptsDir = path.basename(__dirname) === 'scripts'; const rootDir = isInScriptsDir ? path.join(__dirname, '..') : __dirname; +const noCache = process.env.DOCKER_BUILD_NO_CACHE === 'true'; +const withBaseImage = process.env.DOCKER_BUILD_BASE_IMAGE === 'true'; +const nodeVersion = process.env.NODE_VERSION || '24.13.1'; + const config = { + base: { + dockerfilePath: path.join(rootDir, 'docker/images/n8n-base/Dockerfile'), + get fullImageName() { + return `n8nio/base:${nodeVersion}`; + }, + }, n8n: { dockerfilePath: path.join(rootDir, 'docker/images/n8n/Dockerfile'), imageBaseName: process.env.IMAGE_BASE_NAME || 'n8nio/n8n', @@ -153,20 +163,35 @@ async function main() { echo(`INFO: n8n Image: ${config.n8n.fullImageName}`); echo(`INFO: Runners Image: ${config.runners.fullImageName}`); echo(`INFO: Platform: ${platform}`); + if (noCache) echo(chalk.yellow('INFO: Docker layer cache disabled (DOCKER_BUILD_NO_CACHE=true)')); + if (withBaseImage) echo(chalk.yellow('INFO: Building base image first (DOCKER_BUILD_BASE_IMAGE=true)')); echo(chalk.gray('-'.repeat(47))); await checkPrerequisites(); + if (withBaseImage) { + await buildDockerImage({ + name: 'base', + dockerfilePath: config.base.dockerfilePath, + fullImageName: config.base.fullImageName, + buildArgs: [`NODE_VERSION=${nodeVersion}`], + }); + } + + const nodeVersionArgs = withBaseImage ? [`NODE_VERSION=${nodeVersion}`] : []; + const n8nBuildTime = await buildDockerImage({ name: 'n8n', dockerfilePath: config.n8n.dockerfilePath, fullImageName: config.n8n.fullImageName, + buildArgs: nodeVersionArgs, }); const runnersBuildTime = await buildDockerImage({ name: 'runners', dockerfilePath: config.runners.dockerfilePath, fullImageName: config.runners.fullImageName, + buildArgs: nodeVersionArgs, }); // Get image details @@ -226,13 +251,18 @@ async function checkPrerequisites() { } } -async function buildDockerImage({ name, dockerfilePath, fullImageName }) { +async function buildDockerImage({ name, dockerfilePath, fullImageName, buildArgs = [] }) { const startTime = Date.now(); const containerEngine = await getContainerEngine(); // Push directly if image name contains a registry (e.g., ghcr.io/...) // This avoids the slow --load step (export/import tarball) when pushing to a registry const shouldPush = fullImageName.includes('/') && fullImageName.split('/').length > 2; + const extraFlags = [ + ...buildArgs.flatMap((arg) => ['--build-arg', arg]), + ...(noCache ? ['--no-cache'] : []), + ]; + echo(chalk.yellow(`INFO: Building ${name} Docker image using ${containerEngine}...`)); if (shouldPush) { echo(chalk.yellow(`INFO: Registry detected - pushing directly to ${fullImageName}`)); @@ -243,6 +273,7 @@ async function buildDockerImage({ name, dockerfilePath, fullImageName }) { const { stdout } = await $`podman build \ --platform ${platform} \ --build-arg TARGETPLATFORM=${platform} \ + ${extraFlags} \ -t ${fullImageName} \ -f ${dockerfilePath} \ ${config.buildContext}`; @@ -256,6 +287,7 @@ async function buildDockerImage({ name, dockerfilePath, fullImageName }) { const { stdout } = await $`docker buildx build \ --platform ${platform} \ --build-arg TARGETPLATFORM=${platform} \ + ${extraFlags} \ -t ${fullImageName} \ -f ${dockerfilePath} \ --provenance=false \