TL;DR: Self-host n8n with a pinned image, persistent encryption key, non-root user, read-only filesystem, dropped capabilities, resource limits, private editor access, narrowly exposed webhooks, and verified off-site backups.
To securely self-host n8n, treat it like a credential vault that can execute code—not like another disposable dashboard. An n8n instance contains API tokens, OAuth refresh tokens, workflow logic, and incoming webhook data. A compromised owner account or dangerous node can therefore reach far beyond the automation server.
I run n8n for ByteGuard's publishing and notification workflows. After a production security audit, I upgraded it to 2.35.7, moved the process to the non-root node user, made the root filesystem read-only, dropped every Linux capability, restricted the editor to a VPN address, capped it at 1 GiB RAM and one CPU, and verified the SQLite backup. On 23 August 2026 it used 339.5 MiB of RAM and had zero restarts.
What you need to securely self-host n8n
You need a hardened Docker host, a private path to the editor, and a public hostname only if workflows receive external webhooks.
- An Ubuntu or Debian server with Docker Engine and Compose.
- At least 2 GB RAM for a small instance; Code and AI workflows may require more.
- A domain for public webhook callbacks.
- SSH, WireGuard, or another VPN for private administration.
- A reverse proxy such as Nginx Proxy Manager for TLS and route restrictions.
Prepare the host with the Linux VPS hardening guide, Docker security practices, and the secure Nginx Proxy Manager setup.
Decide what must be public
The safest n8n design keeps the editor private and publishes only the webhook paths required by active workflows.
There are two different surfaces:
- The editor and API let authenticated users change workflows and credentials. Put these behind a VPN or identity-aware access proxy.
- Production webhooks must sometimes accept internet traffic. Give them a dedicated hostname or exact reverse-proxy routes, then require a secret header, signed payload, or node-level authentication.
Do not expose TCP 5678 directly to the internet. A private editor also removes the unclaimed-instance race in which a stranger reaches a new deployment before the owner account is created.
Create the n8n secrets file
A fixed encryption key is required to decrypt stored credentials after a migration or restore.
Create a directory and generate secrets without printing them to the terminal:
sudo install -d -m 0750 -o "$USER" -g "$USER" /opt/n8n
cd /opt/n8n
umask 077
{
printf 'N8N_ENCRYPTION_KEY='
openssl rand -hex 32
} > .env
chmod 600 .env
Back up .env separately in an encrypted password manager or secrets store. Losing the encryption key makes the credential records in a restored database unusable.
Run n8n as a hardened Docker container
A hardened Compose service limits what an n8n compromise can change on the host or inside its container.
Create /opt/n8n/compose.yaml:
services:
n8n:
image: docker.n8n.io/n8nio/n8n:2.35.7
container_name: n8n
user: node
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:size=256m,mode=1777
- /home/node/.cache:uid=1000,gid=1000,mode=0700
mem_limit: 1g
cpus: 1.0
pids_limit: 200
stop_grace_period: 30s
ports:
- "127.0.0.1:5678:5678"
expose:
- "5678"
env_file:
- .env
environment:
N8N_HOST: 127.0.0.1
N8N_PORT: 5678
N8N_PROTOCOL: http
N8N_EDITOR_BASE_URL: http://127.0.0.1:5678/
WEBHOOK_URL: https://hooks.example.com/
N8N_PROXY_HOPS: 1
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
N8N_BLOCK_ENV_ACCESS_IN_NODE: "true"
N8N_UNVERIFIED_PACKAGES_ENABLED: "false"
N8N_SECURE_COOKIE: "false"
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: 168
N8N_CONCURRENCY_PRODUCTION_LIMIT: 2
NODES_EXCLUDE: >-
["n8n-nodes-base.executeCommand","n8n-nodes-base.readWriteFile","n8n-nodes-base.localFileTrigger"]
GENERIC_TIMEZONE: Africa/Casablanca
TZ: Africa/Casablanca
volumes:
- n8n_data:/home/node/.n8n
networks:
- automation
- proxy
volumes:
n8n_data:
networks:
automation:
internal: true
proxy:
external: true
Replace the webhook hostname and timezone. This version keeps the editor on loopback and reaches it through an encrypted SSH tunnel, so its local cookie cannot use the HTTPS-only Secure flag. If you instead put the editor behind a private HTTPS proxy, set its real hostname in N8N_HOST and N8N_EDITOR_BASE_URL, change N8N_PROTOCOL to https, and restore N8N_SECURE_COOKIE=true.
Create the external proxy network first if your reverse proxy has not already created it:
docker network inspect proxy >/dev/null 2>&1 || docker network create proxy
docker compose config --quiet
docker compose up -d
The writable n8n_data volume preserves the database and configuration while read_only: true protects the rest of the container filesystem. cap_drop: [ALL] and no-new-privileges remove unnecessary kernel privileges, and the resource limits keep a runaway workflow from consuming the host.
The checklist I use for SSH, firewall rules, Docker, monitoring, and backups on a new VPS.
Keep the n8n editor private
Binding port 5678 to localhost makes SSH tunneling the simplest secure first login.
From your own computer, run:
ssh -L 5678:127.0.0.1:5678 <YOUR_USER>@<YOUR_SERVER_IP>
Open http://127.0.0.1:5678, create the owner account, and store its password in a password manager. The local tunnel uses SSH encryption even though the browser URL is HTTP.
For ongoing use, bind the port to a WireGuard address instead of localhost and reach it only while connected to the VPN. Do not change it to 5678:5678. The public reverse proxy can still reach n8n:5678 over the shared proxy network for webhooks.
If you intentionally publish the editor behind HTTPS, keep N8N_SECURE_COOKIE=true, use multi-factor authentication or SSO where available, rate-limit login attempts at the proxy, and restrict source identities or addresses.
Expose webhooks without exposing the editor
Public webhook routing should allow the required callback paths and return 404 for the editor, API, and every unrelated n8n route.
Use a dedicated hostname such as hooks.example.com. At the reverse proxy, forward only production paths such as /webhook/<unguessable-path> or the exact named routes your workflows require. Do not publish /webhook-test/, /rest/, /api/, or /.
Every sensitive Webhook node should verify at least one of these:
- A high-entropy secret header.
- HTTP Basic or Header authentication configured in the node.
- A provider signature, timestamp, and replay window.
- A strict request-body size and HTTP-method limit at the proxy.
An unguessable URL is helpful but is not sufficient authentication. Log rejected requests without recording authorization headers or full credential-bearing payloads.
Disable dangerous nodes and environment access
n8n's Execute Command and local-file nodes should remain unavailable unless a documented workflow genuinely requires them.
The Compose example blocks Execute Command, Read/Write Files from Disk, and Local File Trigger. It also leaves N8N_BLOCK_ENV_ACCESS_IN_NODE=true, preventing Code nodes from reading process environment variables.
If you must enable a dangerous node, do not replace the exclusion list with [] casually. Isolate that workload in a separate n8n instance, mount only the required directory read-only where possible, and never mount /var/run/docker.sock. Docker socket access is effectively host-root access.
Unverified community packages are disabled in the example because installing third-party code expands the supply-chain attack surface. Review and pin any package you decide to allow.
Isolate Code nodes with external task runners
External task runners move JavaScript and Python Code node execution into a separate sidecar container, reducing the impact of a sandbox escape.
n8n's production guidance recommends external rather than internal runner mode. Use the version-matched n8nio/runners image, authenticate it to the task broker with a random token, run it as UID/GID 65532, drop capabilities, make its root filesystem read-only, and provide only a small /tmp tmpfs.
The exact runner configuration changes across n8n releases, so follow the version-matched official task-runner setup and hardening guidance instead of copying an old sidecar block. Keep module allowlists empty unless a workflow requires specific imports.
If your workflows do not use Code nodes, blocking the Code node entirely is a smaller and safer solution.
Back up n8n consistently
A reliable n8n backup contains the SQLite database, the encryption key, and the deployment configuration, and it is verified by restoration.
Copying a live SQLite file with cp can capture an inconsistent state. Use SQLite's online backup API from the volume:
sudo install -d -m 0700 /var/backups/n8n
sudo sqlite3 \
/var/lib/docker/volumes/n8n_n8n_data/_data/database.sqlite \
".backup '/var/backups/n8n/n8n-$(date +%F-%H%M).sqlite'"
Install the distribution's sqlite3 package first if the command is missing. If you changed the Compose project or volume name, resolve the exact mountpoint with docker volume inspect instead of guessing it.
Verify the resulting database:
latest_backup="$(find /var/backups/n8n -maxdepth 1 -name 'n8n-*.sqlite' -print | sort | tail -1)"
sudo sqlite3 "$latest_backup" "PRAGMA quick_check;"
The expected output is ok. Archive the database together with compose.yaml and the encrypted .env backup, add a checksum, and send the result to an off-site destination. Test a restore into an isolated temporary stack before calling the backup complete.
For larger or multi-worker installations, move n8n to PostgreSQL and back it up with pg_dump. The same rule applies: test pg_restore --list and perform periodic restores.
Update n8n without losing workflows
A safe update pins the target version, backs up state first, and verifies workflows after recreation.
Read the release notes and breaking changes, create a fresh database backup, then edit the image tag in Compose and run:
docker compose pull n8n
docker compose up -d
docker compose logs --tail=150 n8n
Verify the owner login, active workflow count, credential access, a manual execution, a signed test webhook, and the database integrity check. Keep the previous image tag and backup until those tests pass.
Avoid docker compose down -v: the -v flag deletes the named volume that contains the default SQLite database.
Verify the container hardening
Docker inspection proves the controls were applied rather than merely written in the Compose file.
docker inspect n8n --format \
'user={{.Config.User}} readonly={{.HostConfig.ReadonlyRootfs}} caps={{json .HostConfig.CapDrop}} security={{json .HostConfig.SecurityOpt}}'
docker stats --no-stream n8n
docker compose ps
The first command should report the node user, readonly=true, ALL capabilities dropped, and no-new-privileges:true. Confirm separately that port 5678 is bound only to localhost or the VPN address:
sudo ss -lntp | grep ':5678\b'
n8n troubleshooting
Why does n8n restart with a permission error?
The persistent volume may be owned by root from an earlier deployment. Stop the service and change only the n8n data volume's contents to UID/GID 1000, then restart. Do not recursively change unrelated host directories.
Why are restored credentials unreadable?
The restored database was encrypted with a different N8N_ENCRYPTION_KEY. Restore the original key from the same recovery set; the credential data cannot be decrypted without it.
Why do webhooks generate the wrong URL?
WEBHOOK_URL does not match the public callback origin or the proxy-hop setting is wrong. Set the exact external HTTPS URL, configure N8N_PROXY_HOPS to the number of trusted reverse proxies, and recreate the container.
Why does the read-only container fail during a workflow?
The workflow is writing somewhere outside the persistent volume or /tmp. Mount a narrowly scoped data directory for that workflow rather than disabling the read-only root filesystem for the whole container.
Why is a Code node missing a module?
n8n blocks imports unless they are explicitly allowed in the runner. Add only the required built-in or external module to the version-appropriate allowlist; do not use * in production.
Secure self-hosted n8n FAQ
Is self-hosted n8n secure?
Self-hosted n8n can be secure when the editor is private, the image is patched, dangerous nodes are blocked, code runs in an isolated runner, secrets are protected, and backups are tested. The default quick-start is a starting point, not a complete production security posture.
Should n8n port 5678 be public?
No. Bind port 5678 to localhost or a VPN address and let a reverse proxy expose only the HTTPS routes that must be public. Publishing 5678:5678 unnecessarily exposes the editor and API.
Does n8n run as root in Docker?
The supported image provides a non-root node user, and your Compose service should explicitly run as that user. Verify the runtime user with docker inspect after deployment.
How much RAM does self-hosted n8n need?
A small instance can start on a 2 GB VPS, but memory use depends heavily on workflow concurrency, payload size, Code nodes, and AI workloads. ByteGuard's n8n 2.35.7 container used about 339.5 MiB at the measured idle-to-light workload snapshot and was capped at 1 GiB.
Is SQLite enough for n8n?
SQLite is adequate for a small single-instance deployment when backups use its online backup mechanism. PostgreSQL is the better choice for queue mode, multiple workers, larger execution volume, or stricter operational requirements.
Conclusion
A secure n8n deployment is defined by its boundaries: the editor stays private, public webhooks are narrowly routed and authenticated, the container cannot gain privileges or rewrite its root filesystem, and Code nodes do not receive broad host access. Version pinning, resource limits, and tested recovery complete the baseline.
Start with a private single-instance deployment, enable only the nodes your workflows need, and expand the architecture when actual load requires it. Monitor it with Uptime Kuma and use the 3-2-1 backup guide for the off-site layer. If you want the server and automation runtime configured for you, see ByteGuard's VPS setup service.
Sources
- Official n8n Docker installation
- n8n task runners
- n8n task-runner hardening
- n8n node blocking
- n8n releases
Affiliate disclosure
The linked VPS material may contain affiliate links. The $49 VPS setup is ByteGuard's own service. Recommendations are based on tools used for production workloads.
— enim
← Back
Comments
Sign in with GitHub to comment. Threads live in the byteguard-comments repo.