feat: Publish a pointer-compressed n8n image variant (#36720)

This commit is contained in:
Iván Ovejero 2026-08-21 08:58:01 +00:00 committed by GitHub
parent 1c1367b4ba
commit 63d8747070
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 110 additions and 9 deletions

View File

@ -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);

View File

@ -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) {

View File

@ -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'}`);

View File

@ -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]

View File

@ -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}

View File

@ -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: