diff --git a/.github/scripts/attest-image-sbom.mjs b/.github/scripts/attest-image-sbom.mjs index 186b741e8e8..b043b980cf5 100644 --- a/.github/scripts/attest-image-sbom.mjs +++ b/.github/scripts/attest-image-sbom.mjs @@ -27,6 +27,7 @@ const ALLOW_REFS = [ export function parseTargets(env) { return [ { label: 'n8n', image: env.N8N_IMAGE, digest: env.N8N_DIGEST }, + { label: 'n8n-pc', image: env.N8N_PC_IMAGE, digest: env.N8N_PC_DIGEST }, { label: 'runners', image: env.RUNNERS_IMAGE, digest: env.RUNNERS_DIGEST }, { label: 'runners-distroless', image: env.DISTROLESS_IMAGE, digest: env.DISTROLESS_DIGEST }, ].filter((t) => t.image && t.digest); diff --git a/.github/scripts/docker/docker-tags.mjs b/.github/scripts/docker/docker-tags.mjs index 7d73b07408d..b8121224804 100644 --- a/.github/scripts/docker/docker-tags.mjs +++ b/.github/scripts/docker/docker-tags.mjs @@ -18,6 +18,11 @@ class TagGenerator { versionSuffix = '-distroless'; } + if (image === 'n8n-pc') { + imageName = 'n8n'; + versionSuffix = '-pc'; + } + const platformSuffix = platform ? `-${platform.split('/').pop()}` : ''; const fullVersion = `${version}${versionSuffix}${platformSuffix}`; @@ -82,12 +87,12 @@ class TagGenerator { } generateAll({ version, platform, includeDockerHub = false, sha = '', date = '' }) { - const images = ['n8n', 'runners', 'runners-distroless']; + const images = ['n8n', 'n8n-pc', 'runners', 'runners-distroless']; const results = {}; for (const image of images) { const tags = this.generate({ image, version, platform, includeDockerHub, sha, date }); - const prefix = image.replace('-distroless', '_distroless'); + const prefix = image.replaceAll('-', '_'); results[prefix] = tags; if (this.githubOutput) { diff --git a/.github/scripts/docker/get-manifest-digests.mjs b/.github/scripts/docker/get-manifest-digests.mjs index 9f6bd7e66ae..ce70877590d 100755 --- a/.github/scripts/docker/get-manifest-digests.mjs +++ b/.github/scripts/docker/get-manifest-digests.mjs @@ -7,6 +7,7 @@ * * Environment variables: * N8N_TAG - Full image reference for n8n image + * N8N_PC_TAG - Full image reference for the pointer-compressed n8n image * RUNNERS_TAG - Full image reference for runners image * DISTROLESS_TAG - Full image reference for runners-distroless image * GITHUB_OUTPUT - Path to GitHub Actions output file (optional) @@ -36,17 +37,21 @@ function setOutput(name, value) { } const n8nTag = process.env.N8N_TAG || ''; +const n8nPcTag = process.env.N8N_PC_TAG || ''; const runnersTag = process.env.RUNNERS_TAG || ''; const distrolessTag = process.env.DISTROLESS_TAG || ''; const results = { n8n: { digest: getDigest(n8nTag), image: getImageName(n8nTag) }, + n8n_pc: { digest: getDigest(n8nPcTag), image: getImageName(n8nPcTag) }, runners: { digest: getDigest(runnersTag), image: getImageName(runnersTag) }, runners_distroless: { digest: getDigest(distrolessTag), image: getImageName(distrolessTag) }, }; setOutput('n8n_digest', results.n8n.digest); setOutput('n8n_image', results.n8n.image); +setOutput('n8n_pc_digest', results.n8n_pc.digest); +setOutput('n8n_pc_image', results.n8n_pc.image); setOutput('runners_digest', results.runners.digest); setOutput('runners_image', results.runners.image); setOutput('runners_distroless_digest', results.runners_distroless.digest); @@ -54,10 +59,12 @@ setOutput('runners_distroless_image', results.runners_distroless.image); console.log('=== Manifest Digests ==='); console.log(`n8n: ${results.n8n.digest || 'N/A'}`); +console.log(`n8n-pc: ${results.n8n_pc.digest || 'N/A'}`); console.log(`runners: ${results.runners.digest || 'N/A'}`); console.log(`runners-distroless: ${results.runners_distroless.digest || 'N/A'}`); console.log(''); console.log('=== Image Names ==='); console.log(`n8n: ${results.n8n.image || 'N/A'}`); +console.log(`n8n-pc: ${results.n8n_pc.image || 'N/A'}`); console.log(`runners: ${results.runners.image || 'N/A'}`); console.log(`runners-distroless: ${results.runners_distroless.image || 'N/A'}`); diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 50ae1476f5f..cb1e096c68c 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -131,6 +131,9 @@ jobs: outputs: image_ref: ${{ steps.determine-tags.outputs.n8n_primary_tag }} primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.n8n_primary_tag }} + n8n_pc_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.n8n_pc_primary_tag }} + n8n_pc_sha_manifest_tag: ${{ steps.determine-tags.outputs.n8n_pc_sha_primary_tag }} + n8n_pc_date_manifest_tag: ${{ steps.determine-tags.outputs.n8n_pc_date_primary_tag }} runners_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.runners_primary_tag }} runners_distroless_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.runners_distroless_primary_tag }} n8n_sha_manifest_tag: ${{ steps.determine-tags.outputs.n8n_sha_primary_tag }} @@ -206,6 +209,26 @@ jobs: push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }} tags: ${{ steps.determine-tags.outputs.n8n_tags }} + - name: Build and push n8n -pc Docker image + id: build-n8n-pc + if: needs.determine-build-context.outputs.push_enabled == 'true' + uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2 + with: + context: . + file: ./docker/images/n8n/Dockerfile + build-args: | + NODE_VERSION=${{ env.NODE_VERSION }} + N8N_VERSION=${{ needs.determine-build-context.outputs.n8n_version }} + N8N_RELEASE_TYPE=${{ needs.determine-build-context.outputs.release_type }} + BUILDER_IMAGE=n8nio/node-pc:26.5.1-dev@sha256:9ecfe9d5c7ea835bccc604d06c3cb76a837b64496d4a999338cb8b53a50bb0bd + RUNTIME_IMAGE=n8nio/node-pc:26.5.1@sha256:e535408e7815904c98f81fb0203f7f657047d7db3d4edc77de44d2ea72c6bf18 + IMAGE_DESCRIPTION=Workflow Automation Tool (pointer-compressed variant, internal to n8n Cloud, no support or stability guarantees) + platforms: ${{ matrix.docker_platform }} + provenance: false # Disabled - using SLSA L3 generator for isolated provenance + sbom: true + push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }} + tags: ${{ steps.determine-tags.outputs.n8n_pc_tags }} + - name: Kafka native binding smoke check # `load: true` isn't an option here: with sbom:true, buildx produces a manifest # list even for a single platform, and the docker exporter (what --load uses) @@ -264,6 +287,8 @@ jobs: outputs: n8n_digest: ${{ steps.get-digests.outputs.n8n_digest }} n8n_image: ${{ steps.get-digests.outputs.n8n_image }} + n8n_pc_digest: ${{ steps.get-digests.outputs.n8n_pc_digest }} + n8n_pc_image: ${{ steps.get-digests.outputs.n8n_pc_image }} runners_digest: ${{ steps.get-digests.outputs.runners_digest }} runners_image: ${{ steps.get-digests.outputs.runners_image }} runners_distroless_digest: ${{ steps.get-digests.outputs.runners_distroless_digest }} @@ -286,6 +311,7 @@ jobs: - name: Create GHCR multi-arch manifests env: N8N_DATE_MANIFEST_TAG: ${{ needs.build-and-push-docker.outputs.n8n_date_manifest_tag }} + N8N_PC_DATE_MANIFEST_TAG: ${{ needs.build-and-push-docker.outputs.n8n_pc_date_manifest_tag }} RUNNERS_DATE_MANIFEST_TAG: ${{ needs.build-and-push-docker.outputs.runners_date_manifest_tag }} RUNNERS_DISTROLESS_DATE_MANIFEST_TAG: ${{ needs.build-and-push-docker.outputs.runners_distroless_date_manifest_tag }} run: | @@ -318,11 +344,13 @@ jobs: # Create manifests for all images create_manifest "n8n" "${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}" + create_manifest "n8n-pc" "${{ needs.build-and-push-docker.outputs.n8n_pc_primary_ghcr_manifest_tag }}" create_manifest "runners" "${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}" create_manifest "runners-distroless" "${{ needs.build-and-push-docker.outputs.runners_distroless_primary_ghcr_manifest_tag }}" # Create SHA-tagged manifests (immutable references for deployments) create_manifest "n8n (sha)" "${{ needs.build-and-push-docker.outputs.n8n_sha_manifest_tag }}" + create_manifest "n8n-pc (sha)" "${{ needs.build-and-push-docker.outputs.n8n_pc_sha_manifest_tag }}" create_manifest "runners (sha)" "${{ needs.build-and-push-docker.outputs.runners_sha_manifest_tag }}" create_manifest "runners-distroless (sha)" "${{ needs.build-and-push-docker.outputs.runners_distroless_sha_manifest_tag }}" @@ -330,6 +358,7 @@ jobs: # date_tag input was set (e.g. nightly), and create_manifest skips empty tags — # so these calls are no-ops on non-dated builds. create_manifest "n8n (date)" "$N8N_DATE_MANIFEST_TAG" + create_manifest "n8n-pc (date)" "$N8N_PC_DATE_MANIFEST_TAG" create_manifest "runners (date)" "$RUNNERS_DATE_MANIFEST_TAG" create_manifest "runners-distroless (date)" "$RUNNERS_DISTROLESS_DATE_MANIFEST_TAG" @@ -346,13 +375,15 @@ jobs: # Create manifests for each image type declare -A images=( ["n8n"]="${VERSION}" + ["n8n-pc"]="${VERSION}-pc" ["runners"]="${VERSION}" ["runners-distroless"]="${VERSION}-distroless" ) for image in "${!images[@]}"; do TAG_SUFFIX="${images[$image]}" - IMAGE_NAME="${image//-distroless/}" # Remove -distroless from image name + IMAGE_NAME="${image//-distroless/}" # Remove the variant suffix from the image name + IMAGE_NAME="${IMAGE_NAME//-pc/}" echo "Creating Docker Hub manifest for $image" docker buildx imagetools create \ @@ -365,6 +396,8 @@ jobs: # to match docker-tags.mjs format: nightly-abc1234-distroless (not nightly-distroless-abc1234) if [[ "$image" == *"-distroless"* ]]; then SHA_SUFFIX="${VERSION}-${SHORT_SHA}-distroless" + elif [[ "$image" == *"-pc"* ]]; then + SHA_SUFFIX="${VERSION}-${SHORT_SHA}-pc" else SHA_SUFFIX="${TAG_SUFFIX}-${SHORT_SHA}" fi @@ -379,6 +412,8 @@ jobs: if [[ -n "$DATE_TAG" ]]; then if [[ "$image" == *"-distroless"* ]]; then DATE_SUFFIX="${VERSION}-${DATE_TAG}-distroless" + elif [[ "$image" == *"-pc"* ]]; then + DATE_SUFFIX="${VERSION}-${DATE_TAG}-pc" else DATE_SUFFIX="${TAG_SUFFIX}-${DATE_TAG}" fi @@ -394,6 +429,7 @@ jobs: id: get-digests env: N8N_TAG: ${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }} + N8N_PC_TAG: ${{ needs.build-and-push-docker.outputs.n8n_pc_primary_ghcr_manifest_tag }} RUNNERS_TAG: ${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }} DISTROLESS_TAG: ${{ needs.build-and-push-docker.outputs.runners_distroless_primary_ghcr_manifest_tag }} run: node .github/scripts/docker/get-manifest-digests.mjs @@ -432,6 +468,25 @@ jobs: secrets: registry-password: ${{ secrets.GITHUB_TOKEN }} + provenance-n8n-pc: + name: SLSA Provenance (n8n-pc) + needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest] + if: | + needs.create_multi_arch_manifest.result == 'success' && + needs.create_multi_arch_manifest.outputs.n8n_pc_digest != '' + permissions: + id-token: write + packages: write + actions: read + # SLSA L3 Provenance - Must use version tags (@vX.Y.Z), NOT SHAs + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 + with: + image: ${{ needs.create_multi_arch_manifest.outputs.n8n_pc_image }} + digest: ${{ needs.create_multi_arch_manifest.outputs.n8n_pc_digest }} + registry-username: ${{ github.actor }} + secrets: + registry-password: ${{ secrets.GITHUB_TOKEN }} + provenance-runners: name: SLSA Provenance (runners) needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest] @@ -479,6 +534,7 @@ jobs: build-and-push-docker, create_multi_arch_manifest, provenance-n8n, + provenance-n8n-pc, provenance-runners, provenance-runners-distroless, ] @@ -514,6 +570,14 @@ jobs: --predicate security/vex.openvex.json \ ${{ needs.create_multi_arch_manifest.outputs.n8n_image }}@${{ needs.create_multi_arch_manifest.outputs.n8n_digest }} + - name: Attest VEX to n8n-pc image + if: needs.create_multi_arch_manifest.outputs.n8n_pc_digest != '' + run: | + cosign attest --yes \ + --type openvex \ + --predicate security/vex.openvex.json \ + ${{ needs.create_multi_arch_manifest.outputs.n8n_pc_image }}@${{ needs.create_multi_arch_manifest.outputs.n8n_pc_digest }} + - name: Attest VEX to runners image if: needs.create_multi_arch_manifest.outputs.runners_digest != '' run: | @@ -544,6 +608,7 @@ jobs: build-and-push-docker, create_multi_arch_manifest, provenance-n8n, + provenance-n8n-pc, provenance-runners, provenance-runners-distroless, ] @@ -583,6 +648,8 @@ jobs: env: N8N_IMAGE: ${{ needs.create_multi_arch_manifest.outputs.n8n_image }} N8N_DIGEST: ${{ needs.create_multi_arch_manifest.outputs.n8n_digest }} + N8N_PC_IMAGE: ${{ needs.create_multi_arch_manifest.outputs.n8n_pc_image }} + N8N_PC_DIGEST: ${{ needs.create_multi_arch_manifest.outputs.n8n_pc_digest }} RUNNERS_IMAGE: ${{ needs.create_multi_arch_manifest.outputs.runners_image }} RUNNERS_DIGEST: ${{ needs.create_multi_arch_manifest.outputs.runners_digest }} DISTROLESS_IMAGE: ${{ needs.create_multi_arch_manifest.outputs.runners_distroless_image }} @@ -602,6 +669,19 @@ jobs: image_ref: ${{ needs.build-and-push-docker.outputs.image_ref }} secrets: inherit + security-scan-pc: + name: Security Scan (n8n-pc) + needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest] + if: | + success() && + (needs.determine-build-context.outputs.release_type == 'stable' || + needs.determine-build-context.outputs.release_type == 'nightly' || + needs.determine-build-context.outputs.release_type == 'rc') + uses: ./.github/workflows/security-trivy-scan-callable.yml + with: + image_ref: ${{ needs.build-and-push-docker.outputs.n8n_pc_primary_ghcr_manifest_tag }} + secrets: inherit + security-scan-runners: name: Security Scan (runners) needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest] diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index 5da59cf86ae..c46edacc7bc 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -1,9 +1,12 @@ ARG NODE_VERSION=26.5.1 ARG N8N_VERSION=snapshot -# The runtime base has no compiler, so this stage supplies one. # The digest pins node:26.5.1-alpine3.24 for amd64 and arm64. Change the tag and the digest together. -FROM node:26.5.1-alpine3.24@sha256:233761595746769ebfdb6090f44fc7cdf818ae0ce62d2b37e0367723b9823e36 AS toolchain +ARG BUILDER_IMAGE=node:26.5.1-alpine3.24@sha256:233761595746769ebfdb6090f44fc7cdf818ae0ce62d2b37e0367723b9823e36 +ARG RUNTIME_IMAGE=n8nio/base:26.5.1@sha256:1b0bca5c94bbd04ad2120b9e9892a8bc717a1d88efa76fefe452c59bdc611d25 + +# The runtime base has no compiler, so this stage supplies one. +FROM ${BUILDER_IMAGE} AS toolchain RUN apk add --no-cache python3 make g++ # Build isolated-vm in a separate stage. The only cache input is the module source. @@ -40,12 +43,13 @@ COPY --from=native-builder /build/isolated-vm/build/Release/isolated_vm.node \ # node-gyp-build finds build/Release first. Delete the prebuilds to remove the glibc fallback. RUN rm -rf /usr/local/lib/node_modules/n8n/node_modules/isolated-vm/prebuilds -# The digest pins n8nio/base:26.5.1 for amd64 and arm64. A separate workflow builds that -# image, so a new base reaches this image only when you change the digest here. -FROM n8nio/base:26.5.1@sha256:1b0bca5c94bbd04ad2120b9e9892a8bc717a1d88efa76fefe452c59bdc611d25 +# A separate workflow builds the base image, so a new base reaches this image only +# when you change the reference here (see BUILDER_IMAGE/RUNTIME_IMAGE above). +FROM ${RUNTIME_IMAGE} ARG N8N_VERSION ARG N8N_RELEASE_TYPE=dev +ARG IMAGE_DESCRIPTION="Workflow Automation Tool" ENV NODE_ENV=production ENV N8N_RELEASE_TYPE=${N8N_RELEASE_TYPE} ENV SHELL=/bin/sh @@ -67,7 +71,7 @@ USER node ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] LABEL org.opencontainers.image.title="n8n" \ - org.opencontainers.image.description="Workflow Automation Tool" \ + org.opencontainers.image.description="${IMAGE_DESCRIPTION}" \ org.opencontainers.image.source="https://github.com/n8n-io/n8n" \ org.opencontainers.image.url="https://n8n.io" \ org.opencontainers.image.version=${N8N_VERSION} diff --git a/docker/images/n8n/README.md b/docker/images/n8n/README.md index 7707c183d5d..f77d9e6df71 100644 --- a/docker/images/n8n/README.md +++ b/docker/images/n8n/README.md @@ -146,6 +146,10 @@ docker pull docker.n8n.io/n8nio/n8n:0.220.1 docker pull docker.n8n.io/n8nio/n8n:next ``` +### About `-pc` tags + +Tags with a `-pc` suffix (for example `n8nio/n8n:2.36.0-pc`) are a pointer-compressed variant, internal to n8n Cloud. They carry no support or stability guarantees and can change or disappear without notice. Use the regular tags instead. + Stop the container and start it again: 1. Get the container ID: