diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 28aadba746..f8ae294666 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -1,8 +1,9 @@ -# This workflow is used to build and push the Docker image for n8n +# This workflow is used to build and push the Docker image for n8nio/n8n and n8nio/runners # - 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. +# - security-scan: This scans the n8nio/n8n Docker image for security vulnerabilities using Trivy. +# - security-scan-runners: This scans the n8nio/runners Docker image for security vulnerabilities using Trivy. name: 'Docker: Build and Push' @@ -49,6 +50,7 @@ on: paths: - '.github/workflows/docker-build-push.yml' - 'docker/images/n8n/Dockerfile' + - 'docker/images/runners/Dockerfile' jobs: determine-build-context: @@ -168,6 +170,7 @@ jobs: outputs: image_ref: ${{ steps.determine-tags.outputs.primary_ghcr_manifest_tag }} primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.primary_ghcr_manifest_tag }} + runners_primary_ghcr_manifest_tag: ${{ steps.determine-runners-tags.outputs.primary_ghcr_manifest_tag }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -260,6 +263,69 @@ jobs: echo "primary_ghcr_manifest_tag=${PRIMARY_GHCR_MANIFEST_TAG_VALUE}" >> "$GITHUB_OUTPUT" fi + - name: Determine Docker tags (runners) + id: determine-runners-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 }}/runners" + DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}/runners" + PLATFORM="${{ matrix.platform }}" + + GHCR_TAGS_FOR_PUSH="" + DOCKER_TAGS_FOR_PUSH="" + PRIMARY_GHCR_MANIFEST_TAG_VALUE="" + + 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}" + DOCKER_TAGS_FOR_PUSH="" # mirror n8n logic: no Docker Hub for branch + ;; + "dev"|*) + if [[ "$N8N_VERSION_TAG" == pr-* ]]; then + 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 + 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 + + ALL_TAGS="${GHCR_TAGS_FOR_PUSH}" + if [[ -n "$DOCKER_TAGS_FOR_PUSH" ]]; then + ALL_TAGS="${ALL_TAGS}\n${DOCKER_TAGS_FOR_PUSH}" + fi + + { + 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 @@ -271,14 +337,16 @@ jobs: 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 != '' + - name: Login to Docker Hub + if: needs.determine-build-context.outputs.push_enabled == 'true' && ( + steps.determine-tags.outputs.dockerhub_platform_tag != '' || + steps.determine-runners-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 + - name: Build and push n8n Docker image uses: useblacksmith/build-push-action@574eb0ee0b59c6a687ace24192f0727dfb65d6d7 # v1.2 with: context: . @@ -293,6 +361,22 @@ jobs: push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }} tags: ${{ steps.determine-tags.outputs.tags }} + - name: Build and push task runners Docker image + uses: useblacksmith/build-push-action@574eb0ee0b59c6a687ace24192f0727dfb65d6d7 # v1.2 + with: + context: . + file: ./docker/images/runners/Dockerfile + build-args: | + NODE_VERSION=22.19 + PYTHON_VERSION=3.13 + 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-runners-tags.outputs.tags }} + create_multi_arch_manifest: name: Create Multi-Arch Manifest needs: [determine-build-context, build-and-push-docker] @@ -347,8 +431,45 @@ jobs: ;; esac + - name: Determine Docker Hub manifest tag (runners) + id: dockerhub_runners_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 }}/runners" + + # 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' + if: steps.dockerhub_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' || + steps.dockerhub_runners_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -374,6 +495,26 @@ jobs: ${MANIFEST_TAG}-arm64 fi + - name: Create GHCR multi-arch manifest (runners) + if: needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag != '' + run: | + MANIFEST_TAG="${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}" + RELEASE_TYPE="${{ needs.determine-build-context.outputs.release_type }}" + + echo "Creating GHCR runners 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: | @@ -386,6 +527,18 @@ jobs: ${MANIFEST_TAG}-amd64 \ ${MANIFEST_TAG}-arm64 + - name: Create Docker Hub multi-arch manifest (runners) + if: steps.dockerhub_runners_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' + run: | + MANIFEST_TAG="${{ steps.dockerhub_runners_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] @@ -412,3 +565,15 @@ jobs: 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] + 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.runners_primary_ghcr_manifest_tag }} + secrets: inherit diff --git a/docker/images/runners/Dockerfile b/docker/images/runners/Dockerfile index 0519370faf..1b55b134c4 100644 --- a/docker/images/runners/Dockerfile +++ b/docker/images/runners/Dockerfile @@ -1,4 +1,5 @@ -ARG PYTHON_IMAGE=python:3.13-slim +ARG NODE_VERSION=22.19 +ARG PYTHON_VERSION=3.13 # ============================================================================== # STAGE 1: JavaScript runner (@n8n/task-runner) artifact from CI @@ -8,25 +9,21 @@ COPY ./dist/task-runner-javascript /app/task-runner-javascript # ============================================================================== # STAGE 2: Python runner build (@n8n/task-runner-python) with uv -# Produces a relocatable venv tied to PYTHON_IMAGE +# Produces a relocatable venv tied to the python version used # ============================================================================== -FROM ${PYTHON_IMAGE} AS python-runner-builder +FROM python:${PYTHON_VERSION}-alpine AS python-runner-builder ARG TARGETPLATFORM ARG UV_VERSION=0.8.14 -RUN apt-get update && apt-get install -y --no-install-recommends \ - curl ca-certificates build-essential pkg-config git \ - && rm -rf /var/lib/apt/lists/* - RUN set -e; \ case "$TARGETPLATFORM" in \ - "linux/amd64") UV_ARCH="x86_64-unknown-linux-gnu" ;; \ - "linux/arm64") UV_ARCH="aarch64-unknown-linux-gnu" ;; \ + "linux/amd64") UV_ARCH="x86_64-unknown-linux-musl" ;; \ + "linux/arm64") UV_ARCH="aarch64-unknown-linux-musl" ;; \ *) echo "Unsupported platform: $TARGETPLATFORM" >&2; exit 1 ;; \ esac; \ mkdir -p /tmp/uv && cd /tmp/uv; \ - curl -fsSLO "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_ARCH}.tar.gz"; \ - curl -fsSLO "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_ARCH}.tar.gz.sha256"; \ + wget -q "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_ARCH}.tar.gz"; \ + wget -q "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_ARCH}.tar.gz.sha256"; \ sha256sum -c "uv-${UV_ARCH}.tar.gz.sha256"; \ tar -xzf "uv-${UV_ARCH}.tar.gz"; \ install -m 0755 "uv-${UV_ARCH}/uv" /usr/local/bin/uv; \ @@ -74,10 +71,14 @@ RUN set -e; \ cd / && rm -rf /launcher-temp # ============================================================================== -# STAGE 4: Runtime +# STAGE 4: Node alpine base for JS task runner # ============================================================================== -FROM ${PYTHON_IMAGE} AS runtime -ARG NODE_VERSION=22 +FROM node:${NODE_VERSION}-alpine AS node-alpine + +# ============================================================================== +# STAGE 5: Runtime +# ============================================================================== +FROM python:${PYTHON_VERSION}-alpine AS runtime ARG N8N_VERSION=snapshot ARG N8N_RELEASE_TYPE=dev @@ -85,20 +86,17 @@ ENV NODE_ENV=production ENV N8N_RELEASE_TYPE=${N8N_RELEASE_TYPE} ENV SHELL=/bin/sh -RUN apt-get update \ - && apt-get install -y --no-install-recommends curl gnupg ca-certificates tini \ - && mkdir -p /etc/apt/keyrings \ - && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ - | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ - && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" \ - > /etc/apt/sources.list.d/nodesource.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends nodejs \ - && apt-get remove curl -y \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb +# Copy over node from node alpine +COPY --from=node-alpine /usr/local/bin/node /usr/local/bin/node +COPY --from=node-alpine /usr/local/bin/npm /usr/local/bin/npm +COPY --from=node-alpine /usr/local/bin/npx /usr/local/bin/npx +COPY --from=node-alpine /usr/local/lib/node_modules /usr/local/lib/node_modules -RUN useradd -m -u 1000 runner +# Node needs libstdc++ +RUN apk add --no-cache ca-certificates tini libstdc++ + +RUN addgroup -g 1000 -S runner \ + && adduser -u 1000 -S -G runner -h /home/runner -D runner WORKDIR /home/runner COPY --from=app-artifact-processor /app/task-runner-javascript /opt/runners/task-runner-javascript diff --git a/docker/images/runners/n8n-task-runners.json b/docker/images/runners/n8n-task-runners.json index 20cc72bb73..d7c760114e 100644 --- a/docker/images/runners/n8n-task-runners.json +++ b/docker/images/runners/n8n-task-runners.json @@ -3,7 +3,7 @@ { "runner-type": "javascript", "workdir": "/home/runner", - "command": "/usr/bin/node", + "command": "/usr/local/bin/node", "args": [ "--disallow-code-generation-from-strings", "--disable-proto=delete", @@ -13,8 +13,10 @@ "allowed-env": [ "PATH", "GENERIC_TIMEZONE", - "N8N_RUNNERS_MAX_CONCURRENCY", "NODE_OPTIONS", + "N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT", + "N8N_RUNNERS_TASK_TIMEOUT", + "N8N_RUNNERS_MAX_CONCURRENCY", "N8N_SENTRY_DSN", "N8N_VERSION", "ENVIRONMENT", @@ -35,13 +37,18 @@ "allowed-env": [ "PATH", "N8N_RUNNERS_LAUNCHER_LOG_LEVEL", + "N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT", + "N8N_RUNNERS_TASK_TIMEOUT", + "N8N_RUNNERS_MAX_CONCURRENCY", "N8N_SENTRY_DSN", "N8N_VERSION", "ENVIRONMENT", "DEPLOYMENT_NAME" ], "env-overrides": { - "PYTHONPATH": "/opt/runners/task-runner-python" + "PYTHONPATH": "/opt/runners/task-runner-python", + "N8N_RUNNERS_STDLIB_ALLOW": "", + "N8N_RUNNERS_EXTERNAL_ALLOW": "" } } ]