n8n/.github/workflows/docker-build-push.yml
2026-04-08 06:12:02 +00:00

457 lines
19 KiB
YAML

# This workflow is used to build and push the Docker image for n8nio/n8n and n8nio/runners
#
# - Uses docker-config.mjs for context determination, this determines what needs to be built based on the trigger
# - Uses docker-tags.mjs for tag generation, this generates the tags for the images
name: 'Docker: Build and Push'
env:
NODE_OPTIONS: '--max-old-space-size=7168'
NODE_VERSION: '24.14.1'
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: false
type: string
default: 'stable'
push_enabled:
description: 'Whether to push the built images'
required: false
type: boolean
default: true
workflow_dispatch:
inputs:
push_enabled:
description: 'Push image to registry'
required: false
type: boolean
default: true
success_url:
description: 'URL to call after the build is successful'
required: false
type: string
jobs:
determine-build-context:
name: Determine Build Context
runs-on: ubuntu-latest
outputs:
release_type: ${{ steps.context.outputs.release_type }}
n8n_version: ${{ steps.context.outputs.version }}
push_enabled: ${{ steps.context.outputs.push_enabled }}
push_to_docker: ${{ steps.context.outputs.push_to_docker }}
build_matrix: ${{ steps.context.outputs.build_matrix }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Determine build context
id: context
run: |
node .github/scripts/docker/docker-config.mjs \
--event "${{ github.event_name }}" \
--pr "${{ github.event.pull_request.number }}" \
--branch "${{ github.ref_name }}" \
--version "${{ inputs.n8n_version }}" \
--release-type "${{ inputs.release_type }}" \
--push-enabled "${{ inputs.push_enabled }}"
build-and-push-docker:
name: Build App, then Build and Push Docker Image (${{ matrix.platform }})
needs: determine-build-context
runs-on: ${{ matrix.runner }}
timeout-minutes: 25
strategy:
matrix: ${{ fromJSON(needs.determine-build-context.outputs.build_matrix) }}
outputs:
image_ref: ${{ steps.determine-tags.outputs.n8n_primary_tag }}
primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.n8n_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 }}
runners_sha_manifest_tag: ${{ steps.determine-tags.outputs.runners_sha_primary_tag }}
runners_distroless_sha_manifest_tag: ${{ steps.determine-tags.outputs.runners_distroless_sha_primary_tag }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup and Build
uses: ./.github/actions/setup-nodejs
with:
build-command: pnpm build:n8n
enable-docker-cache: 'true'
env:
RELEASE: ${{ needs.determine-build-context.outputs.n8n_version }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Determine Docker tags for all images
id: determine-tags
run: |
node .github/scripts/docker/docker-tags.mjs \
--all \
--version "${{ needs.determine-build-context.outputs.n8n_version }}" \
--platform "${{ matrix.docker_platform }}" \
--sha "${GITHUB_SHA::7}" \
${{ needs.determine-build-context.outputs.push_to_docker == 'true' && '--include-docker' || '' }}
echo "=== Generated Docker Tags ==="
cat "$GITHUB_OUTPUT" | grep "_tags=" | while IFS='=' read -r key value; do
echo "${key}: ${value%%,*}..." # Show first tag for brevity
done
- name: Login to Docker registries
if: needs.determine-build-context.outputs.push_enabled == 'true'
uses: ./.github/actions/docker-registry-login
with:
login-ghcr: true
login-dockerhub: ${{ needs.determine-build-context.outputs.push_to_docker == 'true' }}
dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push n8n Docker image
id: build-n8n
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 }}
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_tags }}
- name: Build and push task runners Docker image (Alpine)
id: build-runners
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
with:
context: .
file: ./docker/images/runners/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 }}
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.runners_tags }}
- name: Build and push task runners Docker image (distroless)
id: build-runners-distroless
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
with:
context: .
file: ./docker/images/runners/Dockerfile.distroless
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 }}
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.runners_distroless_tags }}
create_multi_arch_manifest:
name: Create Multi-Arch Manifest
needs: [determine-build-context, build-and-push-docker]
runs-on: ubuntu-latest
if: |
needs.build-and-push-docker.result == 'success' &&
needs.determine-build-context.outputs.push_enabled == 'true'
outputs:
n8n_digest: ${{ steps.get-digests.outputs.n8n_digest }}
n8n_image: ${{ steps.get-digests.outputs.n8n_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 }}
runners_distroless_image: ${{ steps.get-digests.outputs.runners_distroless_image }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker registries
uses: ./.github/actions/docker-registry-login
with:
login-ghcr: true
login-dockerhub: ${{ needs.determine-build-context.outputs.push_to_docker == 'true' }}
dockerhub-username: ${{ secrets.DOCKER_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
- name: Create GHCR multi-arch manifests
run: |
RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}"
# Function to create manifest for an image
create_manifest() {
local IMAGE_NAME=$1
local MANIFEST_TAG=$2
if [[ -z "$MANIFEST_TAG" ]]; then
echo "Skipping $IMAGE_NAME - no manifest tag"
return
fi
echo "Creating GHCR manifest for $IMAGE_NAME: $MANIFEST_TAG"
# For branch builds, only AMD64 is built
if [[ "$RELEASE_TYPE" == "branch" ]]; then
docker buildx imagetools create \
--tag "$MANIFEST_TAG" \
"${MANIFEST_TAG}-amd64"
else
docker buildx imagetools create \
--tag "$MANIFEST_TAG" \
"${MANIFEST_TAG}-amd64" \
"${MANIFEST_TAG}-arm64"
fi
}
# Create manifests for all images
create_manifest "n8n" "${{ needs.build-and-push-docker.outputs.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 "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 }}"
- name: Create Docker Hub manifests
if: needs.determine-build-context.outputs.push_to_docker == 'true'
run: |
VERSION="${{ needs.determine-build-context.outputs.n8n_version }}"
DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}"
# Create manifests for each image type
declare -A images=(
["n8n"]="${VERSION}"
["runners"]="${VERSION}"
["runners-distroless"]="${VERSION}-distroless"
)
SHORT_SHA="${GITHUB_SHA::7}"
for image in "${!images[@]}"; do
TAG_SUFFIX="${images[$image]}"
IMAGE_NAME="${image//-distroless/}" # Remove -distroless from image name
echo "Creating Docker Hub manifest for $image"
docker buildx imagetools create \
--tag "${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}" \
"${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}-amd64" \
"${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}-arm64"
# Create SHA-tagged manifest (immutable reference)
# For distroless, insert SHA between version and -distroless suffix
# to match docker-tags.mjs format: nightly-abc1234-distroless (not nightly-distroless-abc1234)
if [[ "$image" == *"-distroless"* ]]; then
SHA_SUFFIX="${VERSION}-${SHORT_SHA}-distroless"
else
SHA_SUFFIX="${TAG_SUFFIX}-${SHORT_SHA}"
fi
echo "Creating Docker Hub SHA manifest for $image: ${SHA_SUFFIX}"
docker buildx imagetools create \
--tag "${DOCKER_BASE}/${IMAGE_NAME}:${SHA_SUFFIX}" \
"${DOCKER_BASE}/${IMAGE_NAME}:${SHA_SUFFIX}-amd64" \
"${DOCKER_BASE}/${IMAGE_NAME}:${SHA_SUFFIX}-arm64"
done
- name: Get manifest digests for attestation
id: get-digests
env:
N8N_TAG: ${{ needs.build-and-push-docker.outputs.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
call-success-url:
name: Call Success URL
needs: [create_multi_arch_manifest]
runs-on: ubuntu-latest
if: needs.create_multi_arch_manifest.result == 'success' || needs.create_multi_arch_manifest.result == 'skipped'
steps:
- name: Call Success URL
env:
SUCCESS_URL: ${{ github.event.inputs.success_url }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.success_url != '' }}
run: |
echo "Calling success URL: ${{ env.SUCCESS_URL }}"
curl -v "${{ env.SUCCESS_URL }}" || echo "Failed to call success URL"
shell: bash
provenance-n8n:
name: SLSA Provenance (n8n)
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_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_image }}
digest: ${{ needs.create_multi_arch_manifest.outputs.n8n_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]
if: |
needs.create_multi_arch_manifest.result == 'success' &&
needs.create_multi_arch_manifest.outputs.runners_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.runners_image }}
digest: ${{ needs.create_multi_arch_manifest.outputs.runners_digest }}
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}
provenance-runners-distroless:
name: SLSA Provenance (runners-distroless)
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.runners_distroless_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.runners_distroless_image }}
digest: ${{ needs.create_multi_arch_manifest.outputs.runners_distroless_digest }}
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}
# VEX Attestation - Documents which CVEs affect us (security/vex.openvex.json)
vex-attestation:
name: VEX Attestation
needs:
[
determine-build-context,
build-and-push-docker,
create_multi_arch_manifest,
provenance-n8n,
provenance-runners,
provenance-runners-distroless,
]
if: |
always() &&
needs.create_multi_arch_manifest.result == 'success' &&
(needs.determine-build-context.outputs.release_type == 'stable' ||
needs.determine-build-context.outputs.release_type == 'rc' ||
needs.determine-build-context.outputs.release_type == 'nightly')
runs-on: ubuntu-latest
permissions:
id-token: write
packages: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Cosign
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
- name: Login to GHCR
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Attest VEX to n8n image
if: needs.create_multi_arch_manifest.outputs.n8n_digest != ''
run: |
cosign attest --yes \
--type openvex \
--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 runners image
if: needs.create_multi_arch_manifest.outputs.runners_digest != ''
run: |
cosign attest --yes \
--type openvex \
--predicate security/vex.openvex.json \
${{ needs.create_multi_arch_manifest.outputs.runners_image }}@${{ needs.create_multi_arch_manifest.outputs.runners_digest }}
- name: Attest VEX to runners-distroless image
if: needs.create_multi_arch_manifest.outputs.runners_distroless_digest != ''
run: |
cosign attest --yes \
--type openvex \
--predicate security/vex.openvex.json \
${{ needs.create_multi_arch_manifest.outputs.runners_distroless_image }}@${{ needs.create_multi_arch_manifest.outputs.runners_distroless_digest }}
security-scan:
name: Security Scan
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.image_ref }}
secrets: inherit
security-scan-runners:
name: Security Scan (runners)
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.runners_primary_ghcr_manifest_tag }}
secrets: inherit
notify-on-failure:
name: Notify Cats on nightly build failure
runs-on: ubuntu-latest
needs: [build-and-push-docker]
if: needs.build-and-push-docker.result == 'failure' && github.event_name == 'schedule'
steps:
- uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
with:
status: ${{ needs.build-and-push-docker.result }}
channel: '#team-catalysts'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: Nightly Docker build failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}