Installing Python packages in n8n so they survive a restart
TL;DR
- n8n Python libraries installed at runtime (via exec or entrypoint scripts) are discarded on every container restart.
- The only durable fix on self-hosted Docker is a custom Dockerfile that bakes pip dependencies into the image layer.
- n8n Cloud is a hard ceiling: only Pyodide built-ins and micropip-compatible pure-Python wheels are available, with no C extensions.
- For ML-heavy workloads, a FastAPI sidecar called via the HTTP Request node is the cleaner 2026 architecture.
Working with n8n Python libraries feels straightforward until your container restarts. Any package you installed at runtime (via docker exec, an entrypoint script, or a Code node subprocess) is gone the next time the container is recreated. The cause is architectural. n8n runs Python through a sandboxed task runner process, and that sandbox does not persist the writable layer you installed packages into. This article explains why runtime installs fail, maps what you can import on Cloud versus self-hosted, and walks through the Dockerfile pattern that actually survives restarts.
Why n8n Python libraries don't install like a normal pip package
n8n executes Code node logic through a task runner, introduced as stable in n8n 1.x (2024). The task runner is a separate sandboxed process responsible for Code node execution. It does not share filesystem state with the main n8n container the way a single-process app would.
When you run pip install pandas inside a running container, pip writes to the container's writable layer. That layer exists only for the lifetime of the container instance. When the container is recreated (a restart, a new deploy, an image pull), the writable layer is discarded and n8n starts from the base image. Your Python packages vanish with it.
A common workaround attempt is an entrypoint script that runs pip install before n8n starts. This re-runs on every cold start, adds 30 to 90 seconds of startup latency depending on package size, and breaks entirely if the PyPI index is unreachable at boot time. It also does nothing for n8n Cloud, which blocks all external pip installs at the infrastructure level.
The task runner model is the correct architecture for n8n: it enables process isolation and horizontal scaling. But it requires you to think about third-party imports at build time, not runtime.
Cloud vs self-hosted: the real import gap
The deployment mode you are on sets a hard ceiling on which Python modules you can import.
On n8n Cloud, you run against Pyodide, a WebAssembly port of CPython. Pyodide 0.26 bundles roughly 100 packages (numpy, pandas, scipy, matplotlib, Pillow, and others) that you can import in the Code node with no configuration. For packages outside that bundle, micropip is a partial escape hatch: it can fetch and install pure-Python wheels from PyPI at workflow runtime, adding 1 to 3 seconds of latency per install. The hard limit is any package with native C extensions. psycopg2, lxml compiled against libxml2, or anything that wraps a system library cannot run inside a WebAssembly sandbox.
On self-hosted Docker, you have full CPython and the entire PyPI catalog, provided the package is present in the image. C extensions, system calls, GPU bindings: no restriction. The operational cost is that every new Python extension requires a new image build and a container restart.
If your n8n Python libraries are all in the Pyodide 0.26 bundle, Cloud removes operational overhead entirely. If even one package requires a C extension, self-hosted Docker is your only path.
Custom Dockerfile: baking n8n Python libraries into the image
For self-hosted Docker, a custom Dockerfile is the only approach for n8n Python libraries that survives restarts. Any pip install that happens at runtime is ephemeral by definition.
Extending the official n8n image
The n8n image on Docker Hub is Alpine-based. Extend it with pip installs at the build layer:
FROM n8nio/n8n:latest
USER root
RUN apk add --no-cache python3 py3-pip \
&& pip3 install --break-system-packages \
pandas==2.2.3 \
requests==2.32.3
USER node
The --break-system-packages flag is required on Alpine Python 3.12+ because pip enforces PEP 668 by default. Adding pandas 2.x and requests increases the image size by roughly 180 MB over the base. That cost is paid once at build time. This pattern was discussed in n8n community threads from May 2025 and documented more widely in public posts by January 2026.
Configuring the task runner to see the packages
Installing pip dependencies is not sufficient on its own. You must enable the n8n task runner and point it at the system Python binary. Add these environment variables to your Docker Compose service definition:
environment:
N8N_RUNNERS_ENABLED: "true"
N8N_RUNNERS_MODE: "internal"
PYTHON_PATH: "/usr/bin/python3"
N8N_RUNNERS_ENABLED=true activates the task runner. Without it, the Code node may fall back to an execution path that does not see the packages you installed. PYTHON_PATH ensures n8n resolves to the system Python binary rather than a virtual environment or a Pyodide shim.
Rebuild the image after any change to the Dockerfile. Changes to environment variables alone do not re-run pip install.
Which Python packages are available on n8n Cloud without setup
If you are on n8n Cloud, the following Code node packages are importable immediately: numpy, pandas, scipy, matplotlib, Pillow, sympy, and a subset of the Python standard library that Pyodide exposes. The full Pyodide package index lists what is bundled in each version.
micropip handles packages outside the bundle. Import it in the Code node with import micropip, then use await micropip.install("httpx") to fetch a pure-Python module at runtime. This works for httpx, aiohttp, pydantic (pure build), and similar packages distributed as pure-Python wheels.
What micropip cannot install: psycopg2, lxml, cryptography (OpenSSL bindings), Pillow native builds, or any package that calls compiled C code. The constraint is not arbitrary; WebAssembly has no foreign function interface for native system libraries.
Cross-referencing the Pyodide package list against your dependency tree before committing to a Cloud deployment takes 10 minutes and avoids hours of debugging later. I learned this the hard way after spending a Tuesday afternoon wondering why my workflow kept failing silently on imports that worked fine locally π
When to route around the Code node
For ML-heavy workflows, GPU inference, or Python extensions that would make the n8n image impractically large, a Python sidecar service is the right 2026 architecture. The approach routes around the Code node entirely.
You deploy a FastAPI (or Flask) service as an additional container in the same Docker Compose stack as n8n. Your workflow calls it via the HTTP Request node. The sidecar handles computation and returns JSON. n8n never needs to import the heavy modules directly.
services:
n8n:
image: n8nio/n8n:latest
python-sidecar:
build: ./sidecar
ports:
- "8001:8001"
Co-locating the two containers on the same Docker network keeps round-trip latency below 5 ms in practice. The sidecar has its own build cycle, independent of n8n upgrades. When your pip dependencies change, you rebuild the sidecar image, not the n8n image.
The trade-off: you maintain two images and a service interface contract between them. For workflows that call the sidecar infrequently, that overhead is disproportionate. For workflows that run inference or data transformation on every execution, it eliminates packaging friction and keeps the n8n image lean.
A secondary benefit: a Cloud n8n workflow can call an external Python service hosted on your own infrastructure, partially bypassing the Pyodide ceiling at the cost of egress latency. Not elegant, but it works when you need that one C extension that breaks everything.
Key takeaways
n8n Python libraries require a build-time approach on self-hosted Docker: a custom Dockerfile baking pip dependencies into the image layer is the only setup that survives restarts. On Cloud, you are limited to the Pyodide bundle and micropip-compatible pure-Python wheels, with no C extensions. For ML-heavy workloads, a FastAPI sidecar called via HTTP Request is the correct 2026 architecture. Match the approach to your deployment mode before writing your first Code node.
The problem is architectural: n8n's task runner discards any pip install at runtime. The production CLAUDE.md template in the welcome kit covers exactly this pattern: documenting your stack's constraints and baking dependencies into your build layer instead of chasing runtime installs.