Skip to content

Docker: Build and Push #124

Docker: Build and Push

Docker: Build and Push #124

# 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. Uses standard GitHub runners and docker/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 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'
env:
NODE_OPTIONS: '--max-old-space-size=7168'
NODE_VERSION: '22.21.0'
on:
# Build on push to master
push:
branches: [master, main]
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'
- 'docker/images/runners/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"
elif [[ "${{ github.event_name }}" == "push" ]]; then
# Push to master - build and deploy
{
echo "release_type=dev"
echo "n8n_version=latest"
echo "push_enabled=true"
} >> "$GITHUB_OUTPUT"
else
# Fallback for any other event type
{
echo "release_type=dev"
echo "n8n_version=dev"
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": "ubuntu-latest",
"docker_platform": "linux/amd64"
}]
}'
else
# All other builds (stable, nightly, dev, PR) need both platforms
# Note: For ARM64, we use ubuntu-latest with QEMU emulation
MATRIX='{
"platform": ["amd64", "arm64"],
"include": [{
"platform": "amd64",
"runner": "ubuntu-latest",
"docker_platform": "linux/amd64"
}, {
"platform": "arm64",
"runner": "ubuntu-latest",
"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 }}
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
with:
fetch-depth: 0
- name: Setup and Build
uses: ./.github/actions/setup-nodejs-github
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 }}"
# Convert repository owner to lowercase for Docker compatibility
REPO_OWNER_LOWER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
GHCR_BASE="ghcr.io/${REPO_OWNER_LOWER}/n8n"
# Convert Docker Hub username to lowercase
DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]')
DOCKER_BASE="${DOCKER_USERNAME_LOWER}/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, using the version tag (e.g., "latest" for push builds)
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}"
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<<EOF"
echo -e "$ALL_TAGS"
echo "EOF"
} >> "$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: 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 }}"
# Convert repository owner to lowercase for Docker compatibility
REPO_OWNER_LOWER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
GHCR_BASE="ghcr.io/${REPO_OWNER_LOWER}/runners"
# Convert Docker Hub username to lowercase
DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]')
DOCKER_BASE="${DOCKER_USERNAME_LOWER}/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
# Regular dev builds go to both registries, using the version tag (e.g., "latest" for push builds)
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}"
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<<EOF"
echo -e "$ALL_TAGS"
echo "EOF"
} >> "$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: Convert Docker Hub username to lowercase
if: needs.determine-build-context.outputs.push_enabled == 'true'
id: docker_username
run: |
DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]')
echo "username=${DOCKER_USERNAME_LOWER}" >> "$GITHUB_OUTPUT"
- 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: ${{ steps.docker_username.outputs.username }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push n8n Docker image
uses: docker/build-push-action@v5 # v1.2
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: true
sbom: true
push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }}
tags: ${{ steps.determine-tags.outputs.tags }}
- name: Build and push task runners Docker image
uses: docker/build-push-action@v5 # v1.2
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: 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]
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 }}"
# Convert Docker Hub username to lowercase
DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]')
DOCKER_BASE="${DOCKER_USERNAME_LOWER}/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: 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 }}"
# Convert Docker Hub username to lowercase
DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]')
DOCKER_BASE="${DOCKER_USERNAME_LOWER}/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: Convert Docker Hub username to lowercase
if: steps.dockerhub_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true' ||
steps.dockerhub_runners_check.outputs.CREATE_DOCKERHUB_MANIFEST == 'true'
id: docker_username_manifest
run: |
DOCKER_USERNAME_LOWER=$(echo "${{ secrets.DOCKER_USERNAME }}" | tr '[:upper:]' '[:lower:]')
echo "username=${DOCKER_USERNAME_LOWER}" >> "$GITHUB_OUTPUT"
- name: Login to Docker Hub
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: ${{ steps.docker_username_manifest.outputs.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 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: |
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
- 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]
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, create_multi_arch_manifest]
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
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
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 }}
deploy-to-kubernetes:
name: Deploy to Kubernetes
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
runs-on: ubuntu-latest
# Only deploy on push to master (not on schedule, PR, or workflow_dispatch)
if: |
success() &&
github.event_name == 'push' &&
github.ref == 'refs/heads/master'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Update kubeconfig
run: |
aws eks update-kubeconfig --name ${{ secrets.EKS_CLUSTER_NAME }} --region ${{ secrets.AWS_REGION }}
- name: Inject secrets into manifests
run: |
cd n8n-hosting/kubernetes
# Replace placeholders in n8n-secret.yaml
sed -i "s|N8N_ENCRYPTION_KEY: \"PLACEHOLDER_REPLACED_BY_CI\"|N8N_ENCRYPTION_KEY: \"${{ secrets.N8N_ENCRYPTION_KEY }}\"|g" n8n-secret.yaml
sed -i "s|N8N_RUNNERS_AUTH_TOKEN: \"PLACEHOLDER_REPLACED_BY_CI\"|N8N_RUNNERS_AUTH_TOKEN: \"${{ secrets.N8N_RUNNERS_AUTH_TOKEN }}\"|g" n8n-secret.yaml
# Replace placeholders in postgres-secret.yaml
sed -i "s|POSTGRES_PASSWORD: PLACEHOLDER_REPLACED_BY_CI|POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}|g" postgres-secret.yaml
sed -i "s|POSTGRES_NON_ROOT_PASSWORD: PLACEHOLDER_REPLACED_BY_CI|POSTGRES_NON_ROOT_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}|g" postgres-secret.yaml
echo "✅ Secrets injected successfully"
- name: Update image tags to use custom images
run: |
cd n8n-hosting/kubernetes
# Convert repository owner to lowercase for Docker compatibility
REPO_OWNER_LOWER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
# Update to use your custom images from GHCR (using 'latest' tag for production)
sed -i "s|image: docker.n8n.io/n8nio/n8n:.*|image: ghcr.io/${REPO_OWNER_LOWER}/n8n:latest|g" \
n8n-deployment-queue-mode.yaml n8n-worker-deployment.yaml
# Update runner images (match the actual image name in the file)
sed -i "s|image: ghcr.io/n8n-io/n8n-task-runner-javascript:.*|image: ghcr.io/${REPO_OWNER_LOWER}/runners:latest|g" \
n8n-runner-deployment.yaml
sed -i "s|image: ghcr.io/n8n-io/n8n-task-runner-python:.*|image: ghcr.io/${REPO_OWNER_LOWER}/runners:latest|g" \
n8n-runner-deployment.yaml
echo "✅ Image references updated to custom images"
- name: Check if namespace exists
id: check_namespace
run: |
if kubectl get namespace n8n &> /dev/null; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "✅ Namespace 'n8n' already exists - will update existing deployment"
# Check for problematic states that require fresh deployment
echo "🔍 Checking deployment health..."
kubectl get pods -n n8n 2>/dev/null || true
NEEDS_RESET=false
# Check for unbound PVCs
UNBOUND=$(kubectl get pvc -n n8n -o json 2>/dev/null | jq -r '.items[] | select(.status.phase != "Bound") | .metadata.name' | wc -l)
if [ "$UNBOUND" -gt 0 ]; then
echo "❌ Found $UNBOUND unbound PVCs"
NEEDS_RESET=true
fi
# Check for CrashLoopBackOff pods (e.g., encryption key mismatch)
CRASHING=$(kubectl get pods -n n8n --field-selector=status.phase=Running -o json 2>/dev/null | jq -r '.items[].status.containerStatuses[]? | select(.state.waiting.reason == "CrashLoopBackOff") | .name' | wc -l)
if [ "$CRASHING" -gt 0 ]; then
echo "❌ Found $CRASHING pods in CrashLoopBackOff"
kubectl logs -n n8n -l service=n8n-main --tail=20 || true
NEEDS_RESET=true
fi
if [ "$NEEDS_RESET" = true ]; then
echo "❌ Unhealthy deployment detected. Force deleting namespace for fresh start..."
# Force delete the namespace
kubectl delete namespace n8n --grace-period=0 --force || true
# Wait for deletion (with timeout)
echo "⏳ Waiting for namespace deletion..."
kubectl wait --for=delete namespace/n8n --timeout=180s || true
# Double check it's gone
sleep 10
if kubectl get namespace n8n &> /dev/null; then
echo "❌ Namespace still exists! Manual intervention required."
exit 1
fi
echo "✅ Namespace deleted. Setting flag for initial deployment."
echo "exists=false" >> $GITHUB_OUTPUT
fi
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "📦 Namespace 'n8n' does not exist - will perform initial deployment"
fi
- name: Initial Deployment (if namespace doesn't exist)
if: steps.check_namespace.outputs.exists == 'false'
run: |
cd n8n-hosting/kubernetes
echo "🚀 Performing initial deployment..."
kubectl apply -f namespace.yaml
kubectl apply -f postgres-secret.yaml
kubectl apply -f n8n-secret.yaml
# Apply PVCs (they will bind when pods start consuming them due to WaitForFirstConsumer)
echo "📦 Creating Persistent Volume Claims..."
kubectl apply -f postgres-claim0-persistentvolumeclaim.yaml
kubectl apply -f n8n-claim0-persistentvolumeclaim.yaml
kubectl apply -f redis-claim0-persistentvolumeclaim.yaml
echo "✅ PVCs created (will bind when pods start due to WaitForFirstConsumer mode)"
kubectl get pvc -n n8n
kubectl apply -f redis-deployment.yaml
kubectl apply -f postgres-configmap.yaml
kubectl apply -f postgres-deployment.yaml
kubectl apply -f postgres-service.yaml
echo "⏳ Waiting for PostgreSQL..."
if ! kubectl wait --for=condition=ready pod -l service=postgres-n8n -n n8n --timeout=600s; then
echo "❌ PostgreSQL failed to start. Debugging info:"
kubectl get pods -n n8n
kubectl describe pod -l service=postgres-n8n -n n8n
kubectl logs -l service=postgres-n8n -n n8n --tail=50
exit 1
fi
echo "⏳ Waiting for Redis..."
kubectl wait --for=condition=ready pod -l service=redis -n n8n --timeout=300s
kubectl apply -f n8n-deployment-queue-mode.yaml
kubectl apply -f n8n-service.yaml
kubectl apply -f n8n-main-service.yaml
echo "⏳ Waiting for n8n main..."
kubectl wait --for=condition=ready pod -l service=n8n-main -n n8n --timeout=300s
kubectl apply -f n8n-worker-deployment.yaml
kubectl apply -f n8n-runner-deployment.yaml
echo "✅ Initial deployment complete!"
- name: Update Existing Deployment (if namespace exists)
if: steps.check_namespace.outputs.exists == 'true'
run: |
cd n8n-hosting/kubernetes
echo "🔄 Updating existing deployment..."
# Update secrets (in case they changed)
kubectl apply -f n8n-secret.yaml
kubectl apply -f postgres-secret.yaml
# Update deployments (will pick up new image tags)
kubectl apply -f n8n-deployment-queue-mode.yaml
kubectl apply -f n8n-worker-deployment.yaml
kubectl apply -f n8n-runner-deployment.yaml
# Force restart to pull latest images (required when using :latest tag)
echo "🔄 Forcing rollout restart to pull new images..."
kubectl rollout restart deployment/n8n-main -n n8n
kubectl rollout restart deployment/n8n-worker -n n8n
kubectl rollout restart deployment/n8n-runner -n n8n
echo "✅ Update applied and rollout restart triggered!"
- name: Wait for rollout to complete
run: |
echo "⏳ Waiting for rollout to complete..."
# Wait for n8n-main with debugging
if ! kubectl rollout status deployment/n8n-main -n n8n --timeout=15m; then
echo "❌ n8n-main deployment failed. Debugging info:"
kubectl get pods -n n8n -l service=n8n-main
kubectl describe pod -l service=n8n-main -n n8n
kubectl logs -l service=n8n-main -n n8n --tail=100 --all-containers=true
exit 1
fi
# Wait for n8n-worker with debugging
if ! kubectl rollout status deployment/n8n-worker -n n8n --timeout=10m; then
echo "❌ n8n-worker deployment failed. Debugging info:"
kubectl get pods -n n8n -l service=n8n-worker
kubectl describe pod -l service=n8n-worker -n n8n
kubectl logs -l service=n8n-worker -n n8n --tail=100 --all-containers=true
exit 1
fi
# Wait for n8n-runner with debugging
if ! kubectl rollout status deployment/n8n-runner -n n8n --timeout=10m; then
echo "❌ n8n-runner deployment failed. Debugging info:"
kubectl get pods -n n8n -l service=n8n-runner
kubectl describe pod -l service=n8n-runner -n n8n
kubectl logs -l service=n8n-runner -n n8n --tail=100 --all-containers=true
exit 1
fi
echo "✅ All deployments rolled out successfully!"
- name: Display deployment status
run: |
echo "=========================================="
echo "🎉 n8n Deployment Complete!"
echo "=========================================="
echo ""
echo "📊 Pod Status:"
kubectl get pods -n n8n
echo ""
echo "🔗 Access URL:"
kubectl get svc n8n -n n8n
echo ""
echo "Access n8n at the EXTERNAL-IP above on port 5678"
echo "=========================================="