# This workflow is used to build and push the Docker image for n8n # - determine-build-context: Determines what needs to be built based on the trigger # - build-and-push-docker: This builds on both an ARM64 and AMD64 runner so the builds are native to the platform. Uses blacksmith native runners and build-push-action # - create_multi_arch_manifest: This creates the multi-arch manifest for the Docker image. Needed to recombine the images from the build-and-push-docker job since they are separate runners. # - security-scan: This scans the Docker image for security vulnerabilities using Trivy. name: 'Docker: Build and Push' env: NODE_OPTIONS: '--max-old-space-size=7168' 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 pull_request: types: - opened - ready_for_review paths: - '.github/workflows/docker-build-push.yml' - 'docker/images/n8n/Dockerfile' 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.n8n_version }} push_enabled: ${{ steps.context.outputs.push_enabled }} build_matrix: ${{ steps.matrix.outputs.matrix }} steps: - name: Determine build context values id: context run: | # Debug info echo "Event: ${{ github.event_name }}" echo "Ref: ${{ github.ref }}" echo "Ref Name: ${{ github.ref_name }}" # Check if called by another workflow (has n8n_version input) if [[ -n "${{ inputs.n8n_version }}" ]]; then # workflow_call - used for releases { echo "release_type=${{ inputs.release_type }}" echo "n8n_version=${{ inputs.n8n_version }}" echo "push_enabled=${{ inputs.push_enabled }}" } >> "$GITHUB_OUTPUT" elif [[ "${{ github.event_name }}" == "schedule" ]]; then # Nightly builds { echo "release_type=nightly" echo "n8n_version=snapshot" echo "push_enabled=true" } >> "$GITHUB_OUTPUT" elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then # Build branches for Nathan deploy BRANCH_NAME="${{ github.ref_name }}" # Fallback to parsing ref if ref_name is empty if [[ -z "$BRANCH_NAME" ]] && [[ "${{ github.ref }}" =~ ^refs/heads/(.+)$ ]]; then BRANCH_NAME="${BASH_REMATCH[1]}" fi # Sanitize branch name for Docker tag SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-' | tr -cd '[:alnum:]-_') if [[ -z "$SAFE_BRANCH_NAME" ]]; then echo "Error: Could not determine valid branch name" exit 1 fi { echo "release_type=branch" echo "n8n_version=branch-${SAFE_BRANCH_NAME}" echo "push_enabled=${{ inputs.push_enabled }}" } >> "$GITHUB_OUTPUT" elif [[ "${{ github.event_name }}" == "pull_request" ]]; then # Direct PR triggers for testing Dockerfile changes { echo "release_type=dev" echo "n8n_version=pr-${{ github.event.pull_request.number }}" echo "push_enabled=false" } >> "$GITHUB_OUTPUT" fi # Output summary for logs echo "=== Build Context Summary ===" echo "Release type: $(grep release_type "$GITHUB_OUTPUT" | cut -d= -f2)" echo "N8N version: $(grep n8n_version "$GITHUB_OUTPUT" | cut -d= -f2)" echo "Push enabled: $(grep push_enabled "$GITHUB_OUTPUT" | cut -d= -f2)" - name: Determine build matrix id: matrix run: | RELEASE_TYPE="${{ steps.context.outputs.release_type }}" # Branch builds only need AMD64, everything else needs both platforms if [[ "$RELEASE_TYPE" == "branch" ]]; then MATRIX='{ "platform": ["amd64"], "include": [{ "platform": "amd64", "runner": "blacksmith-4vcpu-ubuntu-2204", "docker_platform": "linux/amd64" }] }' else # All other builds (stable, nightly, dev, PR) need both platforms MATRIX='{ "platform": ["amd64", "arm64"], "include": [{ "platform": "amd64", "runner": "blacksmith-4vcpu-ubuntu-2204", "docker_platform": "linux/amd64" }, { "platform": "arm64", "runner": "blacksmith-4vcpu-ubuntu-2204-arm", "docker_platform": "linux/arm64" }] }' fi # Output matrix as single line for GITHUB_OUTPUT echo "matrix=$(echo "$MATRIX" | jq -c .)" >> "$GITHUB_OUTPUT" echo "Build matrix: $(echo "$MATRIX" | jq .)" 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: 15 strategy: matrix: ${{ fromJSON(needs.determine-build-context.outputs.build_matrix) }} outputs: image_ref: ${{ steps.determine-tags.outputs.primary_ghcr_manifest_tag }} primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.primary_ghcr_manifest_tag }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Setup and Build uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1 with: build-command: pnpm build:n8n - name: Determine Docker tags id: determine-tags run: | RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" N8N_VERSION_TAG="${{ needs.determine-build-context.outputs.n8n_version }}" GHCR_BASE="ghcr.io/${{ github.repository_owner }}/n8n" DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}/n8n" PLATFORM="${{ matrix.platform }}" GHCR_TAGS_FOR_PUSH="" DOCKER_TAGS_FOR_PUSH="" PRIMARY_GHCR_MANIFEST_TAG_VALUE="" # Validate inputs if [[ "$RELEASE_TYPE" == "stable" && -z "$N8N_VERSION_TAG" ]]; then echo "Error: N8N_VERSION_TAG is empty for a stable release." exit 1 fi if [[ "$RELEASE_TYPE" == "branch" && -z "$N8N_VERSION_TAG" ]]; then echo "Error: N8N_VERSION_TAG is empty for a branch release." exit 1 fi # Determine tags based on release type case "$RELEASE_TYPE" in "stable") PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" DOCKER_TAGS_FOR_PUSH="${DOCKER_BASE}:${N8N_VERSION_TAG}-${PLATFORM}" ;; "nightly") PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:nightly" GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" DOCKER_TAGS_FOR_PUSH="${DOCKER_BASE}:nightly-${PLATFORM}" ;; "branch") PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" # No Docker Hub tags for branch builds DOCKER_TAGS_FOR_PUSH="" ;; "dev"|*) if [[ "$N8N_VERSION_TAG" == pr-* ]]; then # PR builds only go to GHCR PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" DOCKER_TAGS_FOR_PUSH="" else # Regular dev builds go to both registries PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:dev" GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" DOCKER_TAGS_FOR_PUSH="${DOCKER_BASE}:dev-${PLATFORM}" fi ;; esac # Combine all tags ALL_TAGS="${GHCR_TAGS_FOR_PUSH}" if [[ -n "$DOCKER_TAGS_FOR_PUSH" ]]; then ALL_TAGS="${ALL_TAGS}\n${DOCKER_TAGS_FOR_PUSH}" fi echo "Generated Tags for push: $ALL_TAGS" { echo "tags<> "$GITHUB_OUTPUT" { echo "ghcr_platform_tag=${GHCR_TAGS_FOR_PUSH}" echo "dockerhub_platform_tag=${DOCKER_TAGS_FOR_PUSH}" } >> "$GITHUB_OUTPUT" # Only output manifest tags from the first platform to avoid duplicates if [[ "$PLATFORM" == "amd64" ]]; then echo "primary_ghcr_manifest_tag=${PRIMARY_GHCR_MANIFEST_TAG_VALUE}" >> "$GITHUB_OUTPUT" fi - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Login to GitHub Container Registry if: needs.determine-build-context.outputs.push_enabled == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to DockerHub if: needs.determine-build-context.outputs.push_enabled == 'true' && steps.determine-tags.outputs.dockerhub_platform_tag != '' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: useblacksmith/build-push-action@6fe3b1c3665ca911656e8249f6195103b7dc9782 # v1.2 with: context: . file: ./docker/images/n8n/Dockerfile build-args: | NODE_VERSION=22 N8N_VERSION=${{ needs.determine-build-context.outputs.n8n_version }} N8N_RELEASE_TYPE=${{ needs.determine-build-context.outputs.release_type }} platforms: ${{ matrix.docker_platform }} provenance: true sbom: true push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }} tags: ${{ steps.determine-tags.outputs.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' steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Login to GitHub Container Registry uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Determine Docker Hub manifest tag id: dockerhub_check run: | RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" N8N_VERSION="${{ needs.determine-build-context.outputs.n8n_version }}" DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}/n8n" # Determine if Docker Hub manifest is needed and construct the tag case "$RELEASE_TYPE" in "stable") { echo "DOCKER_MANIFEST_TAG=${DOCKER_BASE}:${N8N_VERSION}" echo "CREATE_DOCKERHUB_MANIFEST=true" } >> "$GITHUB_OUTPUT" ;; "nightly") { echo "DOCKER_MANIFEST_TAG=${DOCKER_BASE}:nightly" echo "CREATE_DOCKERHUB_MANIFEST=true" } >> "$GITHUB_OUTPUT" ;; "dev") if [[ "$N8N_VERSION" != pr-* ]]; then { echo "DOCKER_MANIFEST_TAG=${DOCKER_BASE}:dev" echo "CREATE_DOCKERHUB_MANIFEST=true" } >> "$GITHUB_OUTPUT" else echo "CREATE_DOCKERHUB_MANIFEST=false" >> "$GITHUB_OUTPUT" fi ;; *) echo "CREATE_DOCKERHUB_MANIFEST=false" >> "$GITHUB_OUTPUT" ;; esac - name: Login to Docker Hub if: steps.dockerhub_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Create GHCR multi-arch manifest if: needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag != '' run: | MANIFEST_TAG="${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}" RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" echo "Creating GHCR manifest: $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 - name: Create Docker Hub multi-arch manifest if: steps.dockerhub_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' run: | MANIFEST_TAG="${{ steps.dockerhub_check.outputs.DOCKER_MANIFEST_TAG }}" echo "Creating Docker Hub manifest: $MANIFEST_TAG" docker buildx imagetools create \ --tag $MANIFEST_TAG \ ${MANIFEST_TAG}-amd64 \ ${MANIFEST_TAG}-arm64 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 security-scan: name: Security Scan needs: [determine-build-context, build-and-push-docker] if: | success() && (needs.determine-build-context.outputs.release_type == 'stable' || needs.determine-build-context.outputs.release_type == 'nightly') uses: ./.github/workflows/security-trivy-scan-callable.yml with: image_ref: ${{ needs.build-and-push-docker.outputs.image_ref }} secrets: inherit