On my Synology DS720+, n8n's Assistant now runs the code it writes in its own sandbox. I asked it to run this:
uname -a && id && python3 -c "print(sum(range(1000)))"
It came back with:
Linux sandbox 4.4.302+ #86009 SMP Wed Nov 26 18:19:17 CST 2025 x86_64 GNU/Linux
uid=1000(user) gid=1000(user) groups=1000(user)
499500
That is the DSM kernel, an unprivileged user inside the sandbox, and a working Python interpreter. Getting there took five environment variables, a volume and a memory limit, all on one container.
The sandbox is n8n-sandbox, the bundled option in n8n's Assistant setup: a sandbox-api service and a privileged Docker-in-Docker runner, sandbox-runner-1, which starts one container per Assistant session. On a mainstream Linux kernel it comes up from n8n's Compose guide. DSM's kernel is missing five features that setup expects, and each gap only shows itself once the previous one is fixed, in a different log, with a different message.
More notes on n8n on DSM follow. The next one sets up the whole stack this sandbox sits in, with the services it shares. The compose file, the full write-up and a script that runs the same checks on your own hardware are in w0rldart/n8n-assistant-synology. Plain n8n, its task runners, Postgres and Browserless need none of this; only the sandbox does.
Tested on
| Item | Value |
|---|---|
| Hardware | Synology DS720+, Celeron J4125, 9.8 GB RAM |
| Kernel | 4.4.302+ #86009 SMP Wed Nov 26 18:19:17 CST 2025 x86_64 |
| Host Docker storage driver | btrfs |
| n8n | 2.38.1 |
| Sandbox service (api, runner, sandbox image) | 1.4.0 |
| Inner Docker daemon | 29.3.1 |
| Checked | 20 September 2026 |
Error messages and the gap behind each
If you arrived here with an error, find it below. n8n's own dialog points at the network when the network is fine, so the log listed next to each message is where the real cause shows up.
| Error | Where it appears | Gap |
|---|---|---|
The service couldn't be reached. Check the URL and network access, then try again. | n8n's sandbox settings dialog | 4 |
NanoCPUs can not be set, as your kernel does not support CPU CFS scheduler or the cgroup is not mounted | sandbox-api log, or docker compose up if a service sets cpus: | 4 |
SANDBOX_RUNNER_DEFAULT_CPU_PERCENT must be a positive integer, got "0" | runner log, in a crash loop | 4 |
create sandbox failed: no eligible runners | sandbox-api log | runner crash-looping, or capacity reached |
cannot restrict inter-container communication or run without the userland proxy: stat /proc/sys/net/bridge/bridge-nf-call-iptables: no such file or directory | runner log, in a crash loop | 1 |
Module overlay not found, then fstype: overlay ... no such device | modprobe, then the inner daemon | 2 |
Unable to enable DIRECT ACCESS FILTERING - DROP rule, then can't initialize iptables table 'raw': Table does not exist | docker run inside the runner | 3 |
seccomp is not enabled in your kernel, running container without default profile | inner dockerd.log | 5 |
The three logs, and how to read each:
# sandbox-api: the real error behind the dialog's vague one
sudo docker logs --since 15m sandbox-api 2>&1 | grep -iE "create sandbox|error" | tail -3
# the runner
sudo docker logs --tail 15 sandbox-runner-1
# the inner Docker daemon, inside the runner
sudo docker exec sandbox-runner-1 tail -40 /var/log/dockerd.log
The five kernel gaps, in the order they surface
1. The bridge netfilter sysctl is invisible inside a container
The runner crash-loops. Its log repeats one error from the inner Docker daemon: it cannot read /proc/sys/net/bridge/bridge-nf-call-iptables from inside its own network namespace, so it cannot restrict inter-container communication. The error names its own fix, DOCKER_IGNORE_BR_NETFILTER_ERROR: "1". Loading br_netfilter on the host changes nothing: the module is there, and the file is still not visible to the container. With the flag set, the error becomes a warning, and the repo's SECURITY.md shows the warning is wrong on this kernel.
2. There is no overlayfs module
modprobe overlay returns Module overlay not found, which is also why the host's own Docker uses btrfs. Images pull and the inner daemon starts, then every container create fails with fstype: overlay ... no such device. Setting DOCKER_DRIVER: btrfs and giving the inner daemon its own volume at /var/lib/docker fixes it. The inner daemon log confirms storage-driver=btrfs.
3. There is no raw iptables table
iptable_raw is compiled out of the kernel: modprobe iptable_raw fails with Module iptable_raw not found, while filter, nat and mangle exist. Docker 28 and later write raw rules for every bridge endpoint, so a test container on runner-bridge failed like this:
docker: Error response from daemon: failed to set up container networking: failed to create endpoint naughty_hellman on network runner-bridge: Unable to enable DIRECT ACCESS FILTERING - DROP rule: (iptables failed: iptables --wait -t raw -A PREROUTING -d 172.18.0.2 ! -i runner-bridge -j DROP: iptables v1.8.11 (legacy): can't initialize iptables table `raw': Table does not exist (do you need to insmod?)
Perhaps iptables or your kernel needs to be upgraded.
(exit status 3))
DSM's own Docker, the Container Manager package, is 24.0.2 and predates those rules, which is why nothing else on the NAS ever noticed. DOCKER_INSECURE_NO_IPTABLES_RAW: "1", added in Docker 28.0.2, skips them. It takes effect here because the runner's start script launches dockerd as a child process that inherits the container's environment; there is no other hook for daemon flags. The inner daemon log then shows WARNING: DOCKER_INSECURE_NO_IPTABLES_RAW is set. The setting belongs to Docker itself, added for kernels built without the raw table, so the same fix applies to any Docker-in-Docker setup on DSM running Docker 28.0.2 or later.
4. There is no CPU CFS scheduler and no pids controller
The runner creates every sandbox with --memory 512m --cpus 1.00 --pids-limit 256, and this kernel rejects --cpus. n8n's settings dialog reports a network problem for a flag the kernel refused, and cuts the real error short; the sandbox-api log has it in full. Setting SANDBOX_RUNNER_DEFAULT_CPU_PERCENT: "0" crash-loops the runner. Of the runner settings, the one that works is SANDBOX_RUNNER_ENABLE_CGROUPS: "false", and it removes the CPU, memory and process limits together. The same gap makes Compose reject cpus: on any service.
5. There is no seccomp
Every sandbox start logs the seccomp warning in the table above. There is no fix. Sandboxes run without a syscall filter.
Everything DSM-specific in one compose block
If you already run n8n with the sandbox, this is the whole difference. It all goes on the runner service, apart from declaring its volume.
services:
sandbox-runner-1:
environment:
# Gap 1: the bridge-nf sysctl is not visible inside the container.
DOCKER_IGNORE_BR_NETFILTER_ERROR: "1"
# Gap 2: no overlayfs. btrfs matches the host filesystem.
DOCKER_DRIVER: btrfs
# Gap 3: no raw iptables table. Needs inner Docker 28.0.2 or later.
DOCKER_INSECURE_NO_IPTABLES_RAW: "1"
# Gap 4: no CPU CFS scheduler. Removes CPU, memory and pids limits together.
SANDBOX_RUNNER_ENABLE_CGROUPS: "false"
# With no per-sandbox limits, this caps how many sandboxes exist at once.
SANDBOX_RUNNER_CAPACITY_TOTAL: "4"
volumes:
# Gap 2 again: the inner daemon needs its own storage.
- sandbox-docker:/var/lib/docker
# The only memory ceiling left once cgroups are off.
mem_limit: 2g
volumes:
sandbox-docker:
mem_limit: 2g is the only memory ceiling left, and it covers the sandboxes nested inside the runner: a 400 MB allocation in a sandbox moved the runner's docker stats figure from 221 MiB to 627 MiB, then back to 228 MiB. After the runner's own share, they have roughly 1.8 GB between them.
SANDBOX_RUNNER_CAPACITY_TOTAL defaults to 1000. With no per-sandbox limits it is the only cap on how many sandboxes exist at once. I first set it to 2, which was too low: the settings dialog creates a sandbox to test the connection and each Assistant conversation gets its own, so one person holds three without trying, and the next request fails with no eligible runners. It is at 4.
Environment changes need docker compose up -d --force-recreate sandbox-runner-1; a restart keeps the old environment. Give it about 45 seconds before opening the Assistant. The inner daemon is slow to start on this CPU, and the runner registers only once it is up. Its log then shows runner registration heartbeat sent.
The cost of the workarounds
Gaps 3, 4 and 5 take away real controls: Docker's raw table hardening, the per-sandbox CPU and process limits, and the syscall filter. The memory ceiling moves from each sandbox to the runner. The network controls survive, because the sandbox service enforces them in the iptables filter table, which this kernel has. The runner also runs privileged: true, which is n8n's own fallback when sysbox-runc is unavailable, so a sandbox escape is root on the NAS. The measurements behind each of those statements are in the repo's SECURITY.md.
Verification
The repo's verify.sh runs the checks and prints a table. Its host section needs neither Synology nor a running stack, and reports which of the five gaps your kernel has. Its sandbox section starts two short-lived probe containers, tests egress and isolation, and removes them.
./verify.sh
None of the five gaps is specific to Synology. They come with an old or restricted kernel, so the host section is worth running on any box before following n8n's guide. Run it again after a DSM update, a sandbox service release or an inner Docker upgrade. Any of the three can break this setup, and the Docker maintainers describe DOCKER_INSECURE_NO_IPTABLES_RAW as not recommended for production, so it may not stay.
Related work
The full write-up, the sanitised compose file, the corrections log and verify.sh are in w0rldart/n8n-assistant-synology, MIT licensed. The sandbox service itself is n8n's, under its Sustainable Use License, and the repo contains none of its code. If you are running n8n on hardware that fights back, tell me what you are working on.