This page covers the Docker images produced by the n8n repository, the build scripts that assemble them, and the CI workflow that publishes them. It describes the n8n main image, the n8n-base base image, and the runners sidecar images (Alpine and distroless variants).
For details on the Task Broker/Runner runtime architecture and the code that runs inside the runner containers, see 7.4. For the CI pipeline that triggers Docker builds as part of a release, see 9.4. For the CLI commands that start n8n processes inside the container, see 7.2.
Three distinct image families are produced from this repository:
| Image | Dockerfile | Registry Names |
|---|---|---|
n8n | docker/images/n8n/Dockerfile | n8nio/n8n, ghcr.io/n8n-io/n8n |
runners (Alpine) | docker/images/runners/Dockerfile | n8nio/runners, ghcr.io/n8n-io/runners |
runners (distroless) | docker/images/runners/Dockerfile.distroless | ghcr.io/n8n-io/runners:<version>-distroless |
The n8n-base image (docker/images/n8n-base/Dockerfile) is not published by the open-source CI workflow; it is the upstream Alpine-based foundation for the n8n image and is maintained separately at n8nio/base.
Image hierarchy:
Sources: docker/images/n8n/Dockerfile1-33 docker/images/n8n-base/Dockerfile1-36 docker/images/runners/Dockerfile1-154 docker/images/runners/Dockerfile.distroless1-198
File: docker/images/n8n-base/Dockerfile
n8nio/base is an Alpine 3.22 image built on top of a hardened Node distribution from dhi.io/node. It installs the following OS-level dependencies in a single layer to minimize image size:
| Package | Purpose |
|---|---|
git, openssh | Source control integration (EE feature) |
graphicsmagick | Image manipulation (pinned version to avoid AGPL Ghostscript fonts) |
tini | PID 1 init process |
tzdata | Timezone data for GENERIC_TIMEZONE env var |
ca-certificates | TLS root certificates |
libc6-compat | glibc compatibility shims for binaries that require it |
msttcorefonts | Microsoft core fonts for PDF/report generation |
NODE_PATH is set to /opt/nodejs/node-v${NODE_VERSION}/lib/node_modules to ensure that globally installed packages are discoverable by require().
Port 5678/tcp is declared as the default HTTP exposure.
Sources: docker/images/n8n-base/Dockerfile1-36
File: docker/images/n8n/Dockerfile
This is a single-stage build that copies pre-built artifacts from the CI build into the n8nio/base layer.
Build ARGs:
| ARG | Default | Purpose |
|---|---|---|
NODE_VERSION | 24.13.1 | Must match the base image Node version |
N8N_VERSION | snapshot | Set as org.opencontainers.image.version label |
N8N_RELEASE_TYPE | dev | Set as N8N_RELEASE_TYPE env var inside container |
What the RUN layer does docker/images/n8n/Dockerfile17-22:
sqlite3 native module against the container's Node binary./usr/local/bin/n8n ā /usr/local/lib/node_modules/n8n/bin/n8n./home/node/.n8n (user data directory)./home/node to the node user.~/.npm and /tmp/* to keep image size small.Key runtime properties:
| Property | Value |
|---|---|
WORKDIR | /home/node |
USER | node (non-root) |
EXPOSE | 5678/tcp |
ENTRYPOINT | tini -- /docker-entrypoint.sh |
NODE_ENV | production |
The entrypoint script docker/images/n8n/docker-entrypoint.sh is copied into the root of the filesystem and executed via tini as the init process.
Build input layout (what must exist before docker build runs):
compiled/ ā pnpm deploy output of the n8n package
docker/images/n8n/docker-entrypoint.sh
The .dockerignore file uses a whitelist approach: everything is excluded by default, and only compiled/ and docker/images/n8n/docker-entrypoint.sh are included for the n8n image build.
Sources: docker/images/n8n/Dockerfile1-33 .dockerignore1-40
The runners image is a sidecar container that runs the JavaScript and Python task runner processes. It connects to the Task Broker inside the main n8n container via WebSocket. For the runtime protocol, see 7.4.
File: docker/images/runners/Dockerfile
This is a five-stage multi-stage build:
Multi-stage build of docker/images/runners/Dockerfile:
Final runtime stage copies:
| Source (from stage) | Destination |
|---|---|
node-alpine:/usr/local/bin/node | /usr/local/bin/node |
node-alpine: corepack | /usr/local/lib/node_modules/corepack |
python-runner-builder:/usr/local/bin/uv | /usr/local/bin/uv |
javascript-runner-builder:/app/task-runner-javascript | /opt/runners/task-runner-javascript |
python-runner-builder:/app/task-runner-python | /opt/runners/task-runner-python |
launcher-downloader:/launcher-bin/* | /usr/local/bin/ |
docker/images/runners/n8n-task-runners.json | /etc/n8n-task-runners.json |
The apk add step installs only ca-certificates tini libstdc++ libc6-compat and then removes apk-tools entirely to reduce attack surface.
Runtime properties:
| Property | Value |
|---|---|
USER | runner (UID 1000) |
EXPOSE | 5680/tcp |
ENTRYPOINT | tini -- /usr/local/bin/task-runner-launcher |
CMD | ["javascript", "python"] |
Sources: docker/images/runners/Dockerfile1-154
File: docker/images/runners/Dockerfile.distroless
This is a six-stage build targeting gcr.io/distroless/cc-debian12 as the final base. It is intended for cloud deployments where a minimal attack surface is required.
Key differences from the Alpine variant:
| Aspect | Alpine variant | Distroless variant |
|---|---|---|
| C library | musl (Alpine) | glibc (Debian bookworm) |
| Builder images | *-alpine | *-bookworm-slim |
| Shell in final image | /bin/sh (busybox from Python alpine) | None |
| Package manager in final image | None (apk removed) | None (distroless) |
| Final base | python:PYTHON_VERSION-alpine | gcr.io/distroless/cc-debian12 |
| User | runner (UID 1000) | nonroot (UID 65532) |
tini | Present | Absent ā distroless has no init |
Because distroless has no shell, an extra runtime-prep stage (Stage 5, debian:bookworm-slim) is used to create symlinks, set permissions, and organize the filesystem before the final COPY --from=runtime-prep into the distroless layer.
The Python runtime libraries must be explicitly copied by architecture-specific paths (/lib/*-linux-gnu*, /usr/lib/*-linux-gnu*) since distroless does not bundle them for the Python interpreter.
Sources: docker/images/runners/Dockerfile.distroless1-198
File: docker/images/runners/n8n-task-runners.json
This JSON file is baked into both runner images at /etc/n8n-task-runners.json. The task-runner-launcher binary reads it at startup to know how to launch each runner type.
/etc/n8n-task-runners.json
task-runners[]
runner-type: "javascript"
command: /usr/local/bin/node
args: [--disallow-code-generation-from-strings, --disable-proto=delete, /opt/runners/task-runner-javascript/dist/start.js]
health-check-server-port: 5681
env-overrides:
NODE_FUNCTION_ALLOW_BUILTIN: crypto
NODE_FUNCTION_ALLOW_EXTERNAL: moment
runner-type: "python"
command: /opt/runners/task-runner-python/.venv/bin/python
args: [-I, -B, -X disable_remote_debug, -m, src.main]
health-check-server-port: 5682
env-overrides:
N8N_RUNNERS_STDLIB_ALLOW: ""
N8N_RUNNERS_EXTERNAL_ALLOW: ""
The allowed-env list for each runner acts as an allowlist: only the listed environment variables are passed through from the launcher's environment to the runner subprocess. This is a security boundary that prevents credential leakage.
Sources: docker/images/runners/n8n-task-runners.json1-55
File: scripts/build-n8n.mjs
This script is the single entry point for producing the artifacts that Docker images consume. It is invoked by pnpm build:n8n (used in CI as pnpm build:n8n).
Build pipeline diagram:
Key output directories produced by build-n8n.mjs:
| Directory | Docker consumer |
|---|---|
compiled/ | docker/images/n8n/Dockerfile |
dist/task-runner-javascript/ | docker/images/runners/Dockerfile and Dockerfile.distroless |
Package.json cleanup steps are performed before pnpm deploy:
trim-fe-packageJson.js removes frontend-only workspace references that would confuse pnpm deploy.pnpm.patchedDependencies; only pdfjs-dist, pkce-challenge, and bull patches are retained for the backend.If not running in CI (CI !== 'true'), package.json files are backed up to .bak and restored after deployment to avoid polluting the local workspace.
Sources: scripts/build-n8n.mjs1-320
File: .github/workflows/docker-build-push.yml
| Trigger | Effect |
|---|---|
schedule (daily at 00:00 UTC) | Nightly build, version nightly |
workflow_call (from release pipeline) | Stable/RC build with explicit version |
workflow_dispatch | Manual branch build, AMD64 only |
push to master | Dev build, version dev |
Sources: .github/workflows/docker-build-push.yml1-423
File: .github/scripts/docker/docker-config.mjs
The BuildContext class determines what to build from the GitHub Actions event type:
| Event | Version string | Platforms | Push to Docker Hub |
|---|---|---|---|
schedule | nightly | amd64 + arm64 | Yes |
workflow_call / release | Passed-in version | amd64 + arm64 | Yes |
push to master | dev | amd64 + arm64 | Yes |
workflow_dispatch | branch-<sanitized-branch> | amd64 only | No |
pull_request | pr-<number> | amd64 only | No |
The buildMatrix method maps linux/amd64 and linux/arm64 to Blacksmith runner labels (blacksmith-4vcpu-ubuntu-2204 and blacksmith-4vcpu-ubuntu-2204-arm).
Sources: .github/scripts/docker/docker-config.mjs1-177
File: .github/scripts/docker/docker-tags.mjs
The TagGenerator class produces per-arch and manifest tags for GHCR and Docker Hub:
| Image | GHCR tag pattern | Docker Hub tag pattern |
|---|---|---|
n8n | ghcr.io/n8n-io/n8n:<version>-<arch> | n8nio/n8n:<version>-<arch> |
runners | ghcr.io/n8n-io/runners:<version>-<arch> | n8nio/runners:<version>-<arch> |
runners-distroless | ghcr.io/n8n-io/runners:<version>-distroless-<arch> | n8nio/runners:<version>-distroless-<arch> |
Per-arch tags are pushed in the build-and-push-docker matrix job. The create_multi_arch_manifest job then combines them into a manifest list under the base tag (without -<arch> suffix).
Sources: .github/scripts/docker/docker-tags.mjs1-113
File: .github/actions/docker-registry-login/action.yml
A reusable composite action that handles login to:
ghcr.io) ā always enabled for image pushes.push_to_docker is true (stable/nightly releases).dhi.io) ā used for the n8n-base image build pipeline.Sources: .github/actions/docker-registry-login/action.yml1-52
After multi-arch manifests are created, the workflow attaches supply chain metadata to each image:
| Step | Tool | Purpose |
|---|---|---|
| SLSA L3 Provenance | slsa-github-generator | Cryptographic build provenance attestation |
| VEX Attestation | cosign attest --type openvex | Documents which CVEs are not applicable (vex.openvex.json) |
| Trivy scan | security-trivy-scan-callable.yml | Vulnerability scan on the published image |
SLSA and VEX attestations only run on stable, rc, and nightly release types. provenance: false is set on the build-push-action steps because SLSA provenance is instead generated in isolation by the slsa-github-generator reusable workflow, which provides stronger guarantees.
Sources: .github/workflows/docker-build-push.yml278-384
File: .dockerignore
The .dockerignore uses a whitelist approach ā all files are excluded first (*), then only necessary paths are explicitly allowed. This reduces the Docker build context from ~900 MB to a small subset.
| Included path | Used by |
|---|---|
compiled/, compiled/** | docker/images/n8n/Dockerfile |
dist/task-runner-javascript/, dist/task-runner-javascript/** | docker/images/runners/Dockerfile* |
packages/@n8n/task-runner-python/** | docker/images/runners/Dockerfile* |
docker/images/n8n/docker-entrypoint.sh | docker/images/n8n/Dockerfile |
docker/images/runners/n8n-task-runners.json | docker/images/runners/Dockerfile* |
package.json, pnpm-lock.yaml, pnpm-workspace.yaml, patches/**, scripts/** | packages/@n8n/benchmark/Dockerfile |
Sources: .dockerignore1-40
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.