Self-Hosted n8n: Public Webhooks, Private Everything Else

n8n is an arbitrary code execution engine, and self-hosting it at home means running one on your own network with a public ingress. This is the split that makes that reasonable: webhooks public behind two edge rules, the editor bound to a tailnet and never published at all, and layers underneath that still hold when the one above them fails.

7 steps 20 min read 2026-08-20
AI Tools RecommendedSee full toolkit below →
Claude App
Architecture & decisions
Claude Code CLI
Compose & edge rule config
Codex CLI
Code review
Gemini
Cross-checking and research

n8n is a workflow automation platform, the self-hostable answer to Zapier and Make, and it is very good at what it does. It is also, and this is the part the tutorials skip, an arbitrary code execution engine that you are about to connect to the public internet.

That isn’t a criticism or an edge case. The Code node runs JavaScript or Python you write. The Execute Command node runs shell commands on the host. That capability is the product; it’s why n8n beats a hosted automation tool for anything non-trivial. But it means a compromise of your n8n instance is not “someone read my data.” It is code execution on the machine, plus every credential n8n holds, which by design is an API key for every service you have ever automated. Run it at home rather than on a disposable VPS, and the blast radius is your LAN.

So the security bar here is genuinely higher than for a CRUD app, and the architecture has to answer an awkward requirement: automation platforms need public webhook ingress, and must not have a public admin surface. Those pull in opposite directions.

If that sounds familiar, it’s the same shape as this site’s Invoice Ninja guide, where a payment webhook had to stay open while the admin UI stayed gated. The mechanics of Cloudflare Access carry over and I won’t re-derive them. What’s different here is that n8n lets you go further than Invoice Ninja could: the editor does not need to exist on the public hostname at all.

Important: This guide documents a running setup’s architecture and the reasoning behind it, not a fresh-box replay. The edge rules, the split, and the local Docker posture are real. Where something was not verified on the live instance it is marked as a recommendation rather than a description, and no measured numbers are published.

Step 1

What You’re Actually Exposing

Two things to settle before any of this is worth doing.

The license. n8n is fair-code under the Sustainable Use License, not OSI open source. In practice: you may freely use, modify, and self-host it for your own internal business purposes, and for personal or non-commercial use. What you may not do is make it available to your customers as a hosted product, or charge people to run their automations on your instance. Running it for your own business or your own projects is squarely fine; reselling it is not. Same category of care as Invoice Ninja’s Elastic License, and worth reading the actual terms rather than trusting a summary if there’s money involved.

The threat model. Be specific about what an attacker gets, because it determines how much of the rest of this guide you actually implement:

The practical takeaway is that “put a login on it” is not sufficient and “leave the editor on the internet with a strong password” is not a plan. The rest of this guide is layers.

Step 2

The Access Model

n8n presents two surfaces with opposite requirements, and the whole architecture is just taking that seriously.

The editor and REST API are the administrative surface. Building workflows, storing credentials, running code. This has no business being publicly reachable.

The webhook endpoints are the ingress. When a third-party service triggers an automation, it POSTs to a URL, and that URL must be reachable from the public internet by a machine that cannot authenticate the way a human can. Production webhooks live under /webhook/, and there’s a second set under /webhook-test/ that only functions while the editor has a workflow open in test mode.

The split follows directly:

Set WEBHOOK_URL to the public hostname so the URLs n8n generates and displays are the ones a third party should actually call. Without it n8n advertises its internal address and every webhook you hand out is wrong.

This is the meaningful improvement over the Invoice Ninja pattern. There, the app was one hostname and the admin paths had to be gated in front of an app that was genuinely published, so a mistake in an Access policy exposed the admin UI. Here the editor is not published at all, so a policy mistake at the edge cannot expose it. The tunnel route is the boundary, and it fails closed.

Step 3

The Docker Setup on a Local Server

n8n runs on a local Debian server here rather than a VPS. Docker Compose, one persistent volume, and one environment variable that matters more than all the others combined.

N8N_ENCRYPTION_KEY is the one to get right, and you get one chance. n8n encrypts stored credentials before writing them to the database. If you don’t set this variable, n8n generates a random key on first launch and writes it into ~/.n8n inside the container. That’s the trap: recreate the container without a persistent volume for ~/.n8n, restore a database dump into a fresh instance, or migrate to a new host with the database but not the settings file, and you get Credentials could not be decrypted. The workflows survive. Every credential inside them does not. There is no master password and no reset.

So: set it explicitly before first boot, before you create a single credential, and store it in your password manager rather than only in the .env on the server. The key has to outlive the machine. This is the same lesson as Invoice Ninja’s APP_KEY, and it bites more people here because n8n’s auto-generation makes it easy to never notice the key exists until it’s gone.

The compose shape:

services:
  n8n:
    image: docker.n8n.io/n8nio/n8n:${TAG}
    restart: unless-stopped
    env_file:
      - ./.env
    volumes:
      - n8n_data:/home/node/.n8n
    # No ports: published. cloudflared reaches it on the compose network.

  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    env_file:
      - ./cloudflared.env
    depends_on:
      - n8n

volumes:
  n8n_data:

Environment worth setting deliberately:

N8N_ENCRYPTION_KEY=<generated once, stored in your password manager>
WEBHOOK_URL=https://hooks.yourdomain.com/
GENERIC_TIMEZONE=America/New_York
TZ=America/New_York
N8N_DIAGNOSTICS_ENABLED=false

On the database: n8n defaults to SQLite, which is fine for a personal instance and keeps the moving parts down. Postgres is the right answer once you have real workflow volume or want backups that don’t involve stopping the service. Either way the database alone is not a sufficient backup, because it’s useless without the encryption key.

On exposure: note the absence of a ports: key. Publishing 5678 to the host is the default instinct and it puts an unauthenticated-by-default automation engine on every interface the machine has. Let cloudflared reach it over the compose network instead, and reach the editor over Tailscale. If you do want a host binding for local access, bind it to the tailnet address or 127.0.0.1, never 0.0.0.0.

Warning: Do not mount the Docker socket into the n8n container. A surprising number of n8n tutorials suggest /var/run/docker.sock:/var/run/docker.sock to let workflows manage containers. Combined with the Execute Command node, that is a direct, trivial path from “someone reached n8n” to “someone is root on the host,” because access to the Docker socket is equivalent to root. Same reasoning applies to privileged: true and to mounting host paths you don’t need. If a workflow genuinely needs to orchestrate Docker, put a narrow authenticated API in front of that capability and have n8n call it.

How AI can help

Hand over the compose file and the env file together and ask specifically what an attacker who reaches the n8n container gets. It's good at spotting the compounding mistakes here, a published port plus a mounted socket plus a missing encryption key, which individually look survivable and together are not. Also worth asking it to write the backup script, because the correct one has an unobvious requirement: the encryption key and the database have to be captured together and restored together, and a script that backs up only the database produces a restore that looks successful right up until a workflow tries to authenticate.

Step 4

The Tunnel

Cloudflare Tunnel is what makes hosting this at home reasonable. cloudflared makes an outbound connection to Cloudflare’s edge, Cloudflare accepts public traffic and forwards it back down that connection, and no port is forwarded on the home router. Your residential IP is not in DNS, there is nothing to port-scan, and the thing an attacker would need to reach is not reachable.

Create the tunnel, put the token in a separate cloudflared.env with chmod 600, and route the public hostname to n8n’s internal address on the compose network. The Invoice Ninja guide covers the tunnel creation sequence in detail, including the ordering that avoids a window where a hostname resolves before any policy is in front of it, and that ordering applies here too.

The one n8n-specific decision: route only what has to be public. The tunnel is where you enforce “webhooks only,” because a path that is not routed cannot be reached no matter what else is misconfigured. This is a stronger guarantee than a policy, since it’s the absence of a route rather than the presence of a rule.

One note on residential hosting: check your ISP’s terms. Plenty of consumer agreements prohibit running services regardless of whether you forwarded a port, and a tunnel doesn’t change the contract even though it changes the mechanics.

Step 5

Two Rules at the Edge

Everything that reaches the public hostname passes two WAF custom rules before it gets near the house.

Find them at Domain → Security → Security rules. Select the domain first; the Security section lives inside it. Cloudflare’s own docs write this as “Security → Security rules” and omit the domain-selection step, which is why it’s a hunt, and accounts on the older dashboard layout have it under Security → WAF → Custom rules instead. The deep link https://dash.cloudflare.com/?to=/:account/:zone/security/security-rules skips the question.

Rule one, geography. Block anything that isn’t from the country you actually operate in, using the ip.src.country field. For a US-only instance that’s a Block on (ip.src.country ne "US"). This is not sophisticated and it doesn’t stop a determined attacker with a US proxy, but it removes the overwhelming majority of untargeted scanning for free. Note the field name: ip.src.country. The legacy ip.geoip.country still appears in older write-ups and is a common reason a rule silently matches nothing.

Worth knowing why this has to be a custom rule: country blocking through IP Access Rules is Enterprise-only, so on a free or Pro zone a WAF custom rule is the documented path.

Rule two, the shared secret. Require a header carrying a secret value, and block anything without it. Now an attacker who finds the webhook URL still gets nothing, because the URL alone isn’t sufficient. The sending service is configured to include the header.

Three constraints that shape this:

Important: Be honest about what this rule is. A WAF custom rule comparing a header against a literal value means the secret lives in a rule expression, readable by anyone with dashboard access, and rotating it is a manual edit in two places with a window where one side has changed and the other hasn’t. It is cheap, effective, and enforced before traffic reaches your network, all of which are real advantages. It is also a shared secret rather than a managed credential, which is exactly why the n8n-side authentication in Step 7 matters rather than being redundant.

How AI can help

Cloudflare rule expressions are a small domain-specific language, and the failure mode is a rule that saves cleanly and matches nothing, which looks identical to a rule that's working until you test it. Describe the intent and ask for the expression, then ask it to explain what traffic would pass, which is how you catch an inverted condition. It's also useful for the constraint arithmetic: given five rules on a free plan and a list of things you want to enforce, ask what can be combined into a single expression and what genuinely needs its own rule.

Step 6

Reaching the Editor

The editor is where workflows get built and credentials get entered, so it’s the surface worth being paranoid about.

The recommended answer is Tailscale, and not publishing it at all. n8n listens on the tailnet, you reach it from your own devices at the tailnet address, and there is no public route to it. Not gated by a policy, not protected by a password prompt on the internet: absent. An attacker cannot attack a hostname that does not resolve. If you already run Tailscale for SSH, this costs nothing extra, and it’s the same pattern this site’s Proxmox guide uses for admin surfaces.

That’s the setup here, and it’s what I’d recommend to most readers, with one honest caveat: it means you cannot open the editor from a device that isn’t on the tailnet. For most people that’s not a real limitation, since installing Tailscale on a phone is a two-minute job. If it is a limitation for you, the alternative is publishing the editor on its own hostname behind Cloudflare Access, configured exactly as the Invoice Ninja guide describes.

If you go that route, understand what you’re relying on. Cloudflare Access with email one-time-PIN is exactly as strong as the mailbox behind it. The whole gate in front of an arbitrary code execution engine reduces to “can someone read this inbox.” That’s an uncomfortable amount of weight on an email account, and the fix is to secure that account properly: a hardware security key on the mailbox, not SMS, and not a recovery flow that falls back to a weaker factor.

Stronger options exist, and honesty about who needs them is more useful than listing them as a checklist:

These are standard practice in corporate Zero Trust deployments and generally overkill for a homelab. They are worth knowing about, and worth reaching for specifically if the thing behind the gate is high-value, which n8n arguably is. None of them are in use here; the Tailscale-only approach makes the question mostly moot.

Step 7

The Layers Underneath, Backups, and What This Skips

Everything above happens before a request reaches n8n. Two more layers sit inside it, and they’re what holds if the edge fails.

Per-workflow webhook authentication. n8n’s Webhook node has an Authentication setting, including a Header Auth credential. Using it means the webhook validates a token itself, independently of the Cloudflare rule. That matters precisely because the edge rule is a shared secret in a rule expression: two layers with different failure modes, rather than one control that everything depends on. This is the pattern to follow when building automations. If your Webhook nodes are currently set to None, the edge is your only control, and it’s worth knowing that rather than assuming depth you don’t have.

n8n’s own login. The editor sits behind n8n’s user management regardless of what’s in front of it. Someone who somehow got past Tailscale or Access still lands on a login wall rather than an open workflow canvas. Turn on 2FA for the owner account. This is the last layer and it should never be the only one.

Backups. The set is the database, the workflows, and the encryption key, and the key is the part people miss. Back up the database without N8N_ENCRYPTION_KEY and you have a restore that appears to succeed and then fails the first time a workflow authenticates to anything. Store the key somewhere that survives the server dying, test a restore into a scratch instance, and while you’re testing, disable outbound anything first: a restored n8n will happily start running scheduled workflows against production systems the moment it boots. Restore with the workflows deactivated.

What this guide skips:

What You Spent

Close to nothing, which is the point.

n8n Cloud exists and is priced by execution volume across several tiers; check current pricing rather than trusting a number in a guide, since it moves. The comparison worth making is not really price, though. Self-hosting buys you unlimited executions, workflow data that never leaves your network, and the Execute Command node reaching things on your LAN that a cloud instance structurally cannot. It costs you being the person responsible for the security posture described above. If that trade sounds bad, the hosted version is genuinely a reasonable purchase.

Against Zapier or Make, the math is lopsided in the usual self-hosting way: those price per task or per operation, and a busy automation habit runs into real monthly cost quickly. But the reason to pick n8n over them is usually capability rather than price, and the reason to self-host it is usually data locality rather than either.

Toolkit Reference

The components that appear across this guide, and the concrete spots where an AI assistant earns its keep.

Tools and Services

n8n
The automation platform. Fair-code under the Sustainable Use License: self-host freely for internal business or personal use, do not resell as a hosted service.
Cloudflare Tunnel
Outbound-only connector. No forwarded ports on the home router, and the tunnel route is what enforces "webhooks only" by simply not routing anything else.
Cloudflare WAF custom rules
The two edge rules: country restriction via ip.src.country, and an exact-match header secret. 5 rules on Free, no regex on Free or Pro.
Tailscale
How the editor is reached. Not a gate in front of a public service; the reason there is no public service to gate.
Cloudflare Access
The alternative if the editor must be publicly reachable. Email OTP is only as strong as the mailbox behind it.
Sustainable Use License
Read it directly if there is money involved. Internal business use is fine; making n8n available to your customers is not.

Where AI Earns Its Keep

Compose and env review
Ask what an attacker who reaches the container gets. The compounding mistakes (published port, mounted Docker socket, missing encryption key) each look survivable alone and are not together.
Cloudflare rule expressions
A rule that saves cleanly and matches nothing looks exactly like a rule that works. Ask what traffic would pass, which is how an inverted condition surfaces.
Rule budget arithmetic
Given five rules on a free plan and a list of things to enforce, what combines into one expression and what genuinely needs its own rule.
Backup and restore harness
The correct script captures the encryption key and the database together. Specify that workflows restore deactivated, or the scratch instance starts firing real automations at production systems.
Workflow code review
Code nodes are code. They deserve the same review as anything else you would run on the host, especially any node handling input that arrived from a webhook.