From 8a1645aab664bdf454ec8124ba51ce06639d78dc Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 6 Aug 2026 09:00:17 +0200 Subject: [PATCH] feat: Extract the get-n8n stack definition into a versioned standalone file (#35620) Co-authored-by: Claude Fable 5 --- .github/WORKFLOWS.md | 2 +- .github/workflows/test-get-n8n.yml | 1 + docker/get-n8n-compose.yml | 104 +++++++++++++++++++ docker/get-n8n.sh | 161 ++++++++++------------------- docker/test-get-n8n.sh | 17 +++ 5 files changed, 175 insertions(+), 110 deletions(-) create mode 100644 docker/get-n8n-compose.yml diff --git a/.github/WORKFLOWS.md b/.github/WORKFLOWS.md index 4c5314ea7d6..2913905eebd 100644 --- a/.github/WORKFLOWS.md +++ b/.github/WORKFLOWS.md @@ -181,7 +181,7 @@ These only run if specific files changed: | `packages/@n8n/benchmark/**` | `build-benchmark-image.yml` | master | | `packages/cli/src/public-api/**/*.yml`, `packages/cli/src/public-api/**/*.yaml`, `packages/cli/src/public-api/**/*.css`, `packages/cli/src/public-api/v1/openapi-gen/**/*.ts`, `packages/cli/scripts/build.mjs`, `packages/cli/package.json` | `util-publish-api-schema.yml` | master | | `packages/@n8n/instance-ai/src/**`, `packages/@n8n/instance-ai/skills/**`, `packages/@n8n/instance-ai/knowledge-base/**`, `packages/@n8n/instance-ai/evaluations/**`, `packages/cli/src/modules/instance-ai/**`, `packages/core/src/execution-engine/eval-mock-helpers.ts` | `ci-instance-ai-evals.yml` | on PR `opened` / `reopened` / `ready_for_review` | -| `docker/get-n8n.sh`, `docker/test-get-n8n.sh` | `test-get-n8n.yml` | any | +| `docker/get-n8n.sh`, `docker/get-n8n-compose.yml`, `docker/test-get-n8n.sh` | `test-get-n8n.yml` | any | ### On PR Review diff --git a/.github/workflows/test-get-n8n.yml b/.github/workflows/test-get-n8n.yml index c640aa53d12..0753832109d 100644 --- a/.github/workflows/test-get-n8n.yml +++ b/.github/workflows/test-get-n8n.yml @@ -12,6 +12,7 @@ on: pull_request: paths: - 'docker/get-n8n.sh' + - 'docker/get-n8n-compose.yml' - 'docker/test-get-n8n.sh' - '.github/workflows/test-get-n8n.yml' schedule: diff --git a/docker/get-n8n-compose.yml b/docker/get-n8n-compose.yml new file mode 100644 index 00000000000..0e48c709b7e --- /dev/null +++ b/docker/get-n8n-compose.yml @@ -0,0 +1,104 @@ +# n8n stack definition for get-n8n.sh (https://get.n8n.io). +# Downloaded at install time; the copy in your install directory is yours to +# edit and the script never rewrites it. To review changes between versions: +# https://github.com/n8n-io/n8n/commits/master/docker/get-n8n-compose.yml +# compose-version: 1 + +volumes: + n8n-data: + sandbox-tls: + +services: + sandbox-certs: + image: ghcr.io/n8n-io/n8n-sandbox-service-api:latest + user: '0:0' + entrypoint: ['sh', '-c'] + command: + - > + bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api + --control-san-prefix sandbox-runner --world-readable && + chown -R sandbox-api:sandbox-api /tls/api && chmod -R a+rX /tls + environment: + NUM_RUNNERS: '1' + volumes: + - sandbox-tls:/tls + + sandbox-api: + image: ghcr.io/n8n-io/n8n-sandbox-service-api:latest + depends_on: + sandbox-certs: + condition: service_completed_successfully + env_file: .env + environment: + SANDBOX_API_GRPC_TLS_CERT_FILE: /tls/api/grpc-server.crt + SANDBOX_API_GRPC_TLS_KEY_FILE: /tls/api/grpc-server.key + SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE: /tls/api/ca.crt + SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE: /tls/api/ca.crt + SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/api/control-grpc-api-client.crt + SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/api/control-grpc-api-client.key + SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME: sandbox-runner-1 + volumes: + - sandbox-tls:/tls:ro + healthcheck: + test: ['CMD', 'wget', '-qO-', 'http://localhost:8080/healthz'] + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + # Never publish 8080/9090 to the host on an internet-facing server. + # n8n reaches this container by service name, over the default Compose network. + + sandbox-runner-1: + image: ghcr.io/n8n-io/n8n-sandbox-service-runner-dind:latest + privileged: true + depends_on: + sandbox-api: + condition: service_healthy + env_file: .env + environment: + SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090 + SANDBOX_RUNNER_HTTP_BASE_URL: http://sandbox-runner-1:8080 + SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ':9091' + SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091 + SANDBOX_RUNNER_ID: runner-1 + SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE: ghcr.io/n8n-io/n8n-sandbox-service-sandbox:latest + SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE: /tls/runner/ca.crt + SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE: /tls/runner/grpc-client.crt + SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE: /tls/runner/grpc-client.key + SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME: sandbox-api + SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/runner/control-grpc-server.crt + SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/runner/control-grpc-server.key + SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE: /tls/runner/ca.crt + volumes: + - sandbox-tls:/tls:ro + # Never expose this container's ports publicly — it runs privileged Docker-in-Docker. + + n8n: + image: docker.io/n8nio/n8n:${N8N_VERSION} + depends_on: + sandbox-api: + condition: service_healthy + ports: + - '5678:5678' # the only port that should be internet-facing + env_file: .env + volumes: + - n8n-data:/home/node/.n8n + + runners: + image: ghcr.io/n8n-io/runners:${N8N_VERSION} + depends_on: + - n8n + environment: + N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN} + N8N_RUNNERS_TASK_BROKER_URI: http://n8n:5679 + # Idle runners exit and are relaunched on demand (per the task-runners docs) + N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: '15' + # Runs user code from Code nodes. Never publish this container's ports. + + searxng: + image: ghcr.io/searxng/searxng:latest + environment: + SEARXNG_SECRET: ${SEARXNG_SECRET} + volumes: + - ./searxng-settings.yml:/etc/searxng/settings.yml:ro + # Internal-only: n8n reaches it by service name. Never publish its port. diff --git a/docker/get-n8n.sh b/docker/get-n8n.sh index 140f3deb1ec..6907d770152 100755 --- a/docker/get-n8n.sh +++ b/docker/get-n8n.sh @@ -12,13 +12,18 @@ # Source: https://github.com/n8n-io/n8n/blob/master/docker/get-n8n.sh set -eu -SCRIPT_VERSION="1.0.0" +SCRIPT_VERSION="1.1.0" # The version to install is derived from the latest stable GitHub release in # resolve_n8n_version(); this fallback only applies when that lookup fails. FALLBACK_N8N_VERSION="2.32.0" N8N_DIR="${N8N_DIR:-./n8n}" N8N_PORT=5678 SOURCE_URL="https://github.com/n8n-io/n8n/blob/master/docker/get-n8n.sh" +# The stack definition lives next to this script in the repo and is downloaded +# at install time. A plain filesystem path also works (used by the test +# harness to install from a working copy). +COMPOSE_SOURCE="${N8N_COMPOSE_URL:-https://raw.githubusercontent.com/n8n-io/n8n/master/docker/get-n8n-compose.yml}" +COMPOSE_HISTORY_URL="https://github.com/n8n-io/n8n/commits/master/docker/get-n8n-compose.yml" DOCS_HOSTING_URL="https://docs.n8n.io/hosting/" UPGRADE=0 @@ -108,6 +113,22 @@ check_deps() { ok "Docker Compose found ($(docker compose version --short 2>/dev/null))" } +# Prints the content of a URL (or plain file path) to stdout. +fetch() { + case "$1" in + *://*) + if command -v curl >/dev/null 2>&1; then + curl -fsSL --max-time 10 "$1" 2>/dev/null + elif command -v wget >/dev/null 2>&1; then + wget -qO- -T 10 "$1" 2>/dev/null + else + return 2 + fi + ;; + *) cat "$1" 2>/dev/null ;; + esac +} + # Returns 0 if an HTTP GET against $1 gets any response at all. http_get() { if command -v curl >/dev/null 2>&1; then @@ -159,15 +180,7 @@ valid_n8n_version() { # which would silently upgrade (and run DB migrations) on any container recreate. resolve_n8n_version() { releases_url="https://api.github.com/repos/n8n-io/n8n/releases/latest" - if command -v curl >/dev/null 2>&1; then - v="$(curl -fsSL --max-time 5 "$releases_url" 2>/dev/null | - sed -n 's/.*"tag_name": *"n8n@\([0-9][0-9.]*\)".*/\1/p' | head -n1)" - elif command -v wget >/dev/null 2>&1; then - v="$(wget -qO- -T 5 "$releases_url" 2>/dev/null | - sed -n 's/.*"tag_name": *"n8n@\([0-9][0-9.]*\)".*/\1/p' | head -n1)" - else - v="" - fi + v="$(fetch "$releases_url" | sed -n 's/.*"tag_name": *"n8n@\([0-9][0-9.]*\)".*/\1/p' | head -n1)" if valid_n8n_version "$v"; then printf '%s\n' "$v" else @@ -229,107 +242,35 @@ EOF chmod 600 "${N8N_DIR}/.env" } +# The stack definition is maintained in the repo (COMPOSE_SOURCE) and +# versioned via its '# compose-version: N' line. It is downloaded once at +# install time; after that the local copy belongs to the user. +compose_version() { sed -n 's/^# compose-version: *//p' | head -n1; } + write_compose() { - cat >"${N8N_DIR}/compose.yml" <<'EOF' -volumes: - n8n-data: - sandbox-tls: + if ! fetch "$COMPOSE_SOURCE" >"${N8N_DIR}/compose.yml.tmp" || + ! grep -q '^services:' "${N8N_DIR}/compose.yml.tmp"; then + rm -f "${N8N_DIR}/compose.yml.tmp" + fail "could not download the stack definition from + ${COMPOSE_SOURCE} + Check your network connection and re-run." + fi + mv "${N8N_DIR}/compose.yml.tmp" "${N8N_DIR}/compose.yml" +} -services: - sandbox-certs: - image: ghcr.io/n8n-io/n8n-sandbox-service-api:latest - user: '0:0' - entrypoint: ['sh', '-c'] - command: - - > - bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api - --control-san-prefix sandbox-runner --world-readable && - chown -R sandbox-api:sandbox-api /tls/api && chmod -R a+rX /tls - environment: - NUM_RUNNERS: '1' - volumes: - - sandbox-tls:/tls - - sandbox-api: - image: ghcr.io/n8n-io/n8n-sandbox-service-api:latest - depends_on: - sandbox-certs: - condition: service_completed_successfully - env_file: .env - environment: - SANDBOX_API_GRPC_TLS_CERT_FILE: /tls/api/grpc-server.crt - SANDBOX_API_GRPC_TLS_KEY_FILE: /tls/api/grpc-server.key - SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE: /tls/api/ca.crt - SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE: /tls/api/ca.crt - SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/api/control-grpc-api-client.crt - SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/api/control-grpc-api-client.key - SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME: sandbox-runner-1 - volumes: - - sandbox-tls:/tls:ro - healthcheck: - test: ['CMD', 'wget', '-qO-', 'http://localhost:8080/healthz'] - interval: 5s - timeout: 3s - retries: 5 - start_period: 10s - # Never publish 8080/9090 to the host on an internet-facing server. - # n8n reaches this container by service name, over the default Compose network. - - sandbox-runner-1: - image: ghcr.io/n8n-io/n8n-sandbox-service-runner-dind:latest - privileged: true - depends_on: - sandbox-api: - condition: service_healthy - env_file: .env - environment: - SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090 - SANDBOX_RUNNER_HTTP_BASE_URL: http://sandbox-runner-1:8080 - SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ':9091' - SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091 - SANDBOX_RUNNER_ID: runner-1 - SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE: ghcr.io/n8n-io/n8n-sandbox-service-sandbox:latest - SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE: /tls/runner/ca.crt - SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE: /tls/runner/grpc-client.crt - SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE: /tls/runner/grpc-client.key - SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME: sandbox-api - SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/runner/control-grpc-server.crt - SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/runner/control-grpc-server.key - SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE: /tls/runner/ca.crt - volumes: - - sandbox-tls:/tls:ro - # Never expose this container's ports publicly — it runs privileged Docker-in-Docker. - - n8n: - image: docker.io/n8nio/n8n:${N8N_VERSION} - depends_on: - sandbox-api: - condition: service_healthy - ports: - - '5678:5678' # the only port that should be internet-facing - env_file: .env - volumes: - - n8n-data:/home/node/.n8n - - runners: - image: ghcr.io/n8n-io/runners:${N8N_VERSION} - depends_on: - - n8n - environment: - N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN} - N8N_RUNNERS_TASK_BROKER_URI: http://n8n:5679 - # Idle runners exit and are relaunched on demand (per the task-runners docs) - N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: '15' - # Runs user code from Code nodes. Never publish this container's ports. - - searxng: - image: ghcr.io/searxng/searxng:latest - environment: - SEARXNG_SECRET: ${SEARXNG_SECRET} - volumes: - - ./searxng-settings.yml:/etc/searxng/settings.yml:ro - # Internal-only: n8n reaches it by service name. Never publish its port. -EOF +# Existing installs keep their compose.yml untouched, so when the published +# stack definition has moved on, say so instead of silently rewriting it. +# Best-effort: stays quiet if offline or if the user removed the version line. +check_compose_freshness() { + [ -f "${N8N_DIR}/compose.yml" ] || return 0 + installed="$(compose_version <"${N8N_DIR}/compose.yml")" + [ -n "$installed" ] || return 0 + latest="$(fetch "$COMPOSE_SOURCE" | compose_version)" + [ -n "$latest" ] && [ "$latest" != "$installed" ] || return 0 + say "" + say "Note: the n8n stack definition is now v${latest}; this install was generated" + say "from v${installed}. get-n8n.sh never rewrites an existing compose.yml." + say "To review what changed: ${COMPOSE_HISTORY_URL}" } write_searxng_settings() { @@ -386,6 +327,7 @@ do_upgrade() { printf 'N8N_VERSION=%s\n' "$target" >>"${N8N_DIR}/.env" fi ok "n8n version: ${current:-unset} -> ${target}" + check_compose_freshness if [ "$NO_START" -eq 1 ]; then say "" @@ -461,6 +403,7 @@ main() { say "Once started, n8n runs at: http://localhost:${N8N_PORT}" fi say "To upgrade: curl -fsSL https://get.n8n.io | sh -s -- --upgrade" + check_compose_freshness exit 0 fi [ "$UPGRADE" -eq 0 ] || fail "no existing install found in ${N8N_DIR} — run without --upgrade to install." diff --git a/docker/test-get-n8n.sh b/docker/test-get-n8n.sh index 894b76f738e..ff5345580e6 100644 --- a/docker/test-get-n8n.sh +++ b/docker/test-get-n8n.sh @@ -13,6 +13,10 @@ set -u SCRIPT="$(cd "$(dirname "$0")" && pwd)/get-n8n.sh" +# Install from the working copy of the stack definition, not the published one +# on master — the harness must test this branch's compose file. +COMPOSE_SRC="$(cd "$(dirname "$0")" && pwd)/get-n8n-compose.yml" +export N8N_COMPOSE_URL="$COMPOSE_SRC" E2E=0 [ "${1:-}" = "--e2e" ] && E2E=1 @@ -100,6 +104,19 @@ rerun_out="$(env N8N_DIR="$WORK/a" sh "$SCRIPT" 2>&1)" && pass "re-run on existi echo "$rerun_out" | grep -q 'http://localhost:5678' && pass "re-run tells the user where n8n runs" || fail "re-run tells the user where n8n runs" +# stack definition versioning +[ -n "$(sed -n 's/^# compose-version: *//p' "$WORK/a/compose.yml")" ] && + pass "installed compose keeps the stack version marker" || fail "installed compose keeps the stack version marker" +echo "$rerun_out" | grep -q 'stack definition is now' && fail "up-to-date install gets no stack notice" || + pass "up-to-date install gets no stack notice" +sed 's/^# compose-version: .*/# compose-version: 999/' "$COMPOSE_SRC" >"$WORK/newer-compose.yml" +notice_out="$(env N8N_DIR="$WORK/a" N8N_COMPOSE_URL="$WORK/newer-compose.yml" sh "$SCRIPT" 2>&1)" +echo "$notice_out" | grep -q 'stack definition is now v999' && pass "re-run notices a newer stack definition" || + fail "re-run notices a newer stack definition" +check_not "unreachable stack definition fails" \ + env N8N_DIR="$WORK/nofetch" N8N_COMPOSE_URL="$WORK/does-not-exist.yml" sh "$SCRIPT" --no-start +check "failed fetch writes no config" test ! -e "$WORK/nofetch/compose.yml" + # version pinning env N8N_DIR="$WORK/pin" sh "$SCRIPT" --version 2.31.4 --no-start >/dev/null 2>&1 [ "$(env_value "$WORK/pin" N8N_VERSION)" = "2.31.4" ] && pass "--version x.y.z pins in .env" ||