# 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 env: N8N_VERSION: ${{ inputs.n8n_version }} RELEASE_TYPE: ${{ inputs.release_type }} PUSH_ENABLED: ${{ inputs.push_enabled }} run: | node .github/scripts/docker/docker-config.mjs \ --event "${{ github.event_name }}" \ --pr "${{ github.event.pull_request.number }}" \ --branch "${{ github.ref_name }}" \ --version "$N8N_VERSION" \ --release-type "$RELEASE_TYPE" \ --push-enabled "$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 }}