Self-hosting the n8n Python task runner with Docker

6 min read

TL;DR

  • The n8n python task runner is a separate process that executes user code in isolation, so a crashing script cannot stall your main n8n instance.
  • Docker Compose is the recommended self-hosting setup: two services, one shared auth token, both images pinned to the exact same version tag.
  • Three mandatory env vars on the n8n side: N8N_RUNNERS_ENABLED=true, N8N_RUNNERS_MODE=external, N8N_TASK_RUNNER_AUTH_TOKEN.
  • Custom Python packages require a custom Dockerfile built FROM n8nio/runners, not a runtime pip install.
  • Version mismatch between the n8n and runner images is the single most common setup failure reported across 2025 community threads.

You know that sinking feeling when your automation workflow freezes mid-execution because some Python script decided to eat all your memory? That's exactly what the n8n python task runner prevents. Without an external runner, a memory spike or blocking operation in user code can freeze the n8n event loop entirely. With it, the blast radius of a rogue script stays contained to an isolated container that n8n can drop and restart without touching queued workflows.

This architecture became the production-grade standard for Python-heavy n8n deployments through late 2025. Going into 2026, it's still the default recommendation (and honestly, after debugging enough frozen workflows, I get why).

Why n8n offloads Python execution to a separate process

n8n's event loop is single-threaded. A Python snippet that blocks the thread (infinite loop, runaway memory allocation, uncaught exception propagating up the call stack) stalls every other workflow waiting in the queue. In the worst case, the process crashes and takes all in-flight executions with it.

The task runner architecture separates concerns cleanly. The n8n main instance receives a trigger, reaches a Code node, and dispatches the task to a standalone code execution worker over an authenticated local connection. The external code executor runs the script, streams results back, and n8n continues processing. If the runner crashes mid-execution, n8n marks that execution as failed and moves on. Nothing else gets disrupted.

This mirrors how Celery workers sit alongside a Django application rather than inside it: the web server handles HTTP, the workers absorb CPU-bound or blocking tasks. n8n applies the same pattern to its workflow code runner layer.

The n8nio/runners image packages this Python execution node as a Docker container that connects to the main n8n container through your Compose network. No code executes inside the n8n process itself once the runner is active.

Setting up the n8n Python task runner with Docker Compose

The official n8nio/runners image (available at hub.docker.com/r/n8nio/runners, released in 2025) is the starting point for any Docker-based setup. Configuration requires three env vars on the n8n side and one matching var on the runner side.

Required environment variables at a glance

On the n8n service:

VariableRequired value
N8N_RUNNERS_ENABLEDtrue
N8N_RUNNERS_MODEexternal
N8N_TASK_RUNNER_AUTH_TOKENa long random string

On the runner service:

VariableRequired value
N8N_RUNNERS_AUTH_TOKENsame string as above

Generate the token with openssl rand -hex 32. Store it in a .env file next to your Compose file and reference it as ${RUNNER_TOKEN}. Never hard-code it in the Compose file (trust me on this one).

Minimal docker-compose.yml

services:
  n8n:
    image: n8nio/n8n:<VERSION>
    environment:
      N8N_RUNNERS_ENABLED: "true"
      N8N_RUNNERS_MODE: "external"
      N8N_TASK_RUNNER_AUTH_TOKEN: "${RUNNER_TOKEN}"
    ports:
      - "5678:5678"
    networks:
      - n8n-net

  n8n-runner:
    image: n8nio/runners:<VERSION>
    environment:
      N8N_RUNNERS_AUTH_TOKEN: "${RUNNER_TOKEN}"
    networks:
      - n8n-net

networks:
  n8n-net:
    driver: bridge

Replace both <VERSION> placeholders with the same semver tag (for example, 1.50.0). Community discussions from October 2025 consistently identify version mismatches between the n8n and runner images as the most common setup failure. The runner protocol changes between minor versions, so staggered updates produce silent connection failures or cryptic authentication errors. Pin both images to the same tag and update them together in a single docker compose pull && docker compose up -d operation.

Adding custom Python packages to the runner image

The n8nio/runners base image ships a minimal Python environment. Libraries such as pandas, requests, httpx, or markitdown are not included. If your Code nodes depend on third-party packages, build a custom image.

FROM n8nio/runners:<VERSION>
RUN pip install --no-cache-dir requests pandas markitdown

Two rules apply here. First, the FROM version must match your n8n image tag exactly (consistent with the version-pin constraint above). Second, install packages at image build time, not at container startup. A 2025 community thread documented markitdown failing in production because it was installed via a startup script: the isolated Python sandbox lost the package after every container restart. Baking it into a Dockerfile layer guarantees the dependency survives container recreation.

Reference your custom image in the Compose file:

  n8n-runner:
    build: ./runner
    image: n8n-runner-custom:<VERSION>

Security model: guarantees and limits

Running code inside the n8n python task runner's dedicated container provides concrete isolation. A runner crash does not affect the main n8n process. The runner filesystem is separated from n8n's unless you explicitly mount shared volumes. The runner process cannot access n8n's internal memory or database connection.

What the isolation does not cover is network egress. By default, the runner container has full outbound internet access. A buggy or intentionally malicious script can make arbitrary HTTP requests and exfiltrate data. If your workflows execute untrusted or user-submitted code, restrict outbound traffic with Docker network policies before deploying to production.

A dedicated bridge network without external access (drop the driver: bridge default, add explicit firewall rules or a null route) is the minimum for multi-tenant contexts. The complete security model, including how the runner authenticates with n8n and what syscalls are available by default, is documented in the n8n Task runners reference. Review it before deploying in any environment where Code node inputs are controlled by end users.

Production checklist and common pitfalls in 2026

Three failure modes appear repeatedly across community threads from 2025 and early 2026. I've debugged enough of these to spot the patterns πŸ˜…

The first is version mismatch. Update n8n and the runner image together. Because the runner protocol negotiates capabilities on connection, a one-minor-version gap between the two containers is enough to cause silent task drops or authentication refusals.

The second is an incorrect or missing auth token. If the token values do not match exactly (including trailing whitespace in an .env file), the runner fails to register and the n8n UI shows no error. Check the runner container logs immediately after startup: a successful connection prints a registration confirmation. No confirmation message means auth failure.

The third is N8N_RUNNERS_MODE left unset. Without an explicit external value, n8n defaults to internal mode and ignores the runner container entirely. Code nodes execute in-process regardless of whether the runner is running. The Compose stack appears healthy, but none of the isolation applies.

For production hardening: set restart: unless-stopped on both services, add a health check on the runner's listener port, and route both containers' logs to your aggregation stack. The Railway n8n runner template (published in October 2025) provides a working reference deployment that's useful for validating the full Compose setup before committing to a self-hosted environment.

As of 2026, the external Docker runner is the architecture documented and recommended by n8n for any self-host running Python-heavy workflows. Internal mode remains available for lightweight single-node setups where process isolation is not a requirement.

Key takeaways

The n8n Python task runner decouples script execution from the main n8n process, preventing rogue code from stalling your workflow engine. Docker Compose setup requires three env vars on n8n and one on the runner, connected by a shared auth token. Pin both images to the same semver tag and always update them together. Install custom Python packages at image build time, not at runtime. Lock down network egress before running untrusted code in the isolated Python sandbox.


Self-hosting Python task runners means keeping your n8n workflows isolated from rogue scripts, but version mismatches between the n8n and runner images trip up most setups. The production CLAUDE.md template in the kit shows how to document and pin dependencies so Claude doesn't drift your config mid-deploy.

β†’ Get the welcome kit