Receive external events in self-hosted n8n without opening inbound router ports
A local n8n instance can build and run workflows, but external providers cannot deliver events to localhost. This guide runs n8n with Docker Compose, connects it to a public Localtonet HTTP tunnel, and configures its public webhook address and trusted-proxy settings. You will verify test and production callbacks, check the public service surface, add request authentication, and prepare for updates and recovery. The primary setup keeps n8n's host port bound to loopback rather than exposing it to the LAN.
๐ What's in this guide
Understand the request path and public surface
When n8n runs at http://localhost:5678, that address works only in the relevant machine's network context. A provider calling localhost would contact its own machine, not yours. Private LAN addresses are also not normally routable from the public internet. Webhook providers need a public callback URL that reaches the correct route and accepts the correct HTTP method.
With Localtonet, our client runs on a device that can reach n8n and establishes an outbound connection to a relay server. An HTTP tunnel supplies a public HTTPS URL and forwards requests to the configured local IP address and port. The same-host workflow does not require inbound router port forwarding, firewall changes, VPN setup, or a public IP address. Existing network policies still apply.
The request passes through the provider, Localtonet relay, running tunnel, connected client, and local n8n target. n8n then looks for a registered webhook matching the route and method. A working network path does not guarantee that the workflow is registered or that the request is authorized.
Pointing a tunnel at n8n's service port can make the editor, login page, API routes, and other paths on that port publicly reachable. A webhook path is not an access boundary. If you require public callbacks with a private editor, use a path-filtering reverse proxy or another architecture explicitly designed for that separation. Loopback binding prevents direct LAN access to the host port; it does not restrict requests arriving through the tunnel.
Prerequisites and deployment scope
This tutorial uses Docker Compose with the Localtonet client installed directly on the same host. n8n also supports other self-hosting methods, described in its deployment documentation, but mixing those methods would change startup, networking, and persistence instructions.
- A host that can remain awake and connected while callbacks are expected.
- Docker Engine or Docker Desktop, with the
docker composecommand available. - A terminal, browser, and permission to manage containers.
- A Localtonet account and the appropriate client for the host operating system.
- Access to the provider's callback configuration and a non-sensitive sample event.
- Space for persistent n8n data and a separate, protected backup location.
Check Docker before proceeding:
docker version
docker compose version
Both commands should succeed, with Docker able to contact its engine. The examples use a POSIX-compatible shell, available on Linux, macOS, and suitable Windows environments such as WSL. Windows users can use Docker Desktop, but should keep the shell, project directory, and Docker context consistent.
The configuration uses n8n's default local database rather than an external database. It preserves /home/node/.n8n in a named volume. If you later add PostgreSQL, external binary storage, or other services, extend the backup plan to cover them; copying this volume alone will no longer be a complete backup.
This setup suits development, integration testing, demonstrations, and controlled self-hosted scenarios. Production availability also depends on the host, Docker, n8n, Localtonet client, tunnel, and provider retry behavior. A workstation that sleeps is not an always-on webhook service.
Run n8n locally with a loopback-only port
First establish a working local service without a public URL override. Select a currently supported n8n release, review its release notes, and use its exact image version tag. No fixed release number is supplied here because the appropriate supported version changes over time. Do not leave the deployment on an untagged image or a moving latest tag indefinitely.
- Create a dedicated project directory.
- Record the selected version in
.env. - Create the Compose file and start n8n.
- Complete account setup locally before publishing the service.
mkdir n8n-local
cd n8n-local
Create .env with the following entry. Replace the placeholder with an actual available release tag before running Compose:
N8N_VERSION=REPLACE_WITH_EXACT_RELEASE_TAG
Save this as compose.yaml:
services:
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION:?Set N8N_VERSION in .env}
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:
The address before the first colon matters. 127.0.0.1:5678:5678 publishes the service on host loopback. A mapping of 5678:5678 normally publishes on all host interfaces, unnecessarily widening access in this same-host topology. Keep Docker current and verify actual reachability rather than treating configuration text as proof.
docker compose pull n8n
docker compose up -d
docker compose ps
docker compose logs --tail=100 n8n
Open http://localhost:5678 on the Docker host. Complete n8n's initial owner-account setup, use a strong unique password, and confirm that you can create a workflow. If the page fails, inspect logs and check for a port conflict before adding a tunnel.
The restart policy restarts the container in appropriate Docker lifecycle situations, but it does not start Docker itself or the Localtonet client. A container deliberately stopped by the operator also needs an explicit start. We cover host recovery later.
Alternate topology: Localtonet on another LAN device
A client on another machine cannot reach the Docker host's loopback address. Do not solve this by silently changing the mapping to all interfaces. Instead, select a specific trusted LAN address on the Docker host and deliberately secure that listener.
ports:
- "${N8N_LAN_IP:?Set the Docker host LAN IP}:5678:5678"
This is an alternative to the loopback mapping, not an additional requirement for the main setup. Set N8N_LAN_IP to the Docker host's actual LAN address. Before starting the listener, configure Docker-aware host or network firewall rules to permit port 5678 only from the Localtonet client's address, and deny other sources. Docker-published ports interact with host firewalls differently across platforms, so verify enforcement from an unauthorized device as well as the permitted client.
Test the LAN URL from the client device, then use that address as the tunnel target. Do not forward the port on your router. Plain HTTP on this LAN segment is not protected merely because the public endpoint uses HTTPS; use an appropriately protected network or a separately secured transport where required. This alternate topology may require firewall configuration and is not the no-firewall-change same-host example.
Install the client and start the HTTP tunnel
Use the installation entry point in our HTTP tunnel documentation and the current application instructions available through Localtonet documentation. Obtain the client for your operating system through those official instructions. Installation packaging and startup integration vary by platform, so this guide does not invent a universal installation command.
Install and run the Localtonet client
Run the application on the device that can reach n8n. Complete the device authentication procedure shown by the current client instructions. Keep the client running and confirm that n8n responds from that device.
Open the HTTP tunnel configuration
Choose the Process Type: Random Sub Domain, Custom Sub Domain, or Custom Domain, according to current availability. All provide a public HTTPS address for the target content. Consult current documentation before configuring custom-domain DNS.
Select the device and relay
Select the client device's AuthToken and an available server or region. Confirm that the corresponding device/token is reported as connected in the dashboard, rather than merely existing in the account. If disconnected, check the running client's connection state and outbound network access before continuing.
Enter the local target
For the primary setup, enter 127.0.0.1 and port 5678. Use the secured LAN target only for the alternate topology. A client inside a container has its own loopback context and is not the same-host application setup shown here.
Start the tunnel
Press Start, confirm the tunnel is running, and copy its public HTTPS URL. The device connection and tunnel running state are separate checks. Both must remain active.
The AuthToken identifies the client device. It is not part of the public callback and must not be sent to a provider or included in screenshots, repositories, workflow input, or logs.
Configure WEBHOOK_URL and trusted proxy hops
The tunnel does not automatically change n8n's generated webhook addresses. Set WEBHOOK_URL to the assigned external HTTPS base URL. Behind a reverse proxy, n8n also needs an appropriate N8N_PROXY_HOPS configuration so it can interpret proxy-provided request information correctly.
These settings solve different problems. WEBHOOK_URL defines the generated callback base address. Proxy trust determines how n8n interprets forwarded information such as the original client address and external protocol. An incorrect trust configuration can produce misleading source addresses or allow caller-supplied forwarded headers to influence security decisions.
Determine the proxy chain rather than guessing
Inventory the HTTP proxies between the external caller and n8n. Include any CDN, load balancer, or local reverse proxy you add. Do not count Docker port publishing, router NAT, or each physical machine as an HTTP proxy hop. The relevant count is the trusted HTTP forwarding chain as presented to n8n, not the number of boxes in a connectivity diagram.
n8n's common single-reverse-proxy example uses N8N_PROXY_HOPS=1. That is not a universal value for every Localtonet deployment. The supplied product information does not establish a fixed forwarded-header chain for every client, relay, or additional proxy arrangement. Confirm the applicable behavior for your deployment through current documentation or support before selecting the value.
The last proxy must supply the appropriate X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto information. The trusted boundary must also handle untrusted incoming forwarded headers safely. If you manage that proxy, check its configuration explicitly. If you cannot establish the chain, do not rely on forwarded client IP addresses for authorization, and do not increase the hop count until a warning disappears.
Keep the existing version entry in .env and add:
WEBHOOK_URL=https://your-public-host.example/
N8N_PROXY_HOPS=REPLACE_WITH_VERIFIED_HOP_COUNT
Both values require replacement. The example hostname is intentionally nonfunctional, and the hop-count placeholder must become the verified integer for your topology. Use the public base URL, not an individual /webhook/... route.
Update compose.yaml:
services:
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION:?Set N8N_VERSION in .env}
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
WEBHOOK_URL: ${WEBHOOK_URL:?Set WEBHOOK_URL in .env}
N8N_PROXY_HOPS: ${N8N_PROXY_HOPS:?Set verified N8N_PROXY_HOPS in .env}
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:
docker compose up -d --force-recreate n8n
docker compose ps
docker compose exec n8n printenv WEBHOOK_URL N8N_PROXY_HOPS
Recreation applies the changed environment while retaining the named volume. A plain docker compose restart does not apply changed Compose environment values. Inspect the displayed webhook URLs in n8n after recreation; they should begin with the public HTTPS hostname.
For proxy verification, send a synthetic request from a separate network and inspect its received headers in a temporary diagnostic workflow. Confirm that the forwarded external protocol and host are correct. Separately verify n8n's effective client-address handling before enabling IP restrictions. Seeing a raw X-Forwarded-For header in node input does not prove that n8n trusts or interprets it correctly. Where possible, perform a controlled spoofed-header test and confirm it cannot defeat your restriction. Remove diagnostic data afterward.
Update WEBHOOK_URL, recreate n8n, inspect both generated URLs, and update every provider storing the old callback. Revisit proxy trust when the network topology changes, even if the hostname stays the same.
Configure test and production webhooks
The Webhook node displays separate test and production URLs. Use Listen for Test Event for a temporary editor-side capture. For ongoing callbacks, publish the workflow using the production workflow controls in your installed version. Older releases may describe this as activating the workflow. Merely saving an unpublished edit is not proof that the production route is registered.
Consult the n8n Webhook node documentation for options matching your pinned version. Labels and advanced options can differ between releases.
| Address or mode | Purpose | Required verification |
|---|---|---|
| Local address | Setup and host-side diagnosis | n8n responds on host loopback. Never register localhost with an external provider. |
| Test webhook | Capture a development event | Start the temporary listener and send while it is waiting. |
| Production webhook | Ongoing callbacks | Publish or activate the workflow and inspect its execution history. |
| Random subdomain | Short-lived integration testing | Check the assigned address after replacing a tunnel. |
| Custom subdomain or domain | A deliberately selected callback name | Confirm current availability and any DNS requirements. |
Create a minimal POST endpoint
Create a workflow containing a Webhook node with method POST, path localtonet-check, and the immediate response mode. Use None authentication only for this brief synthetic test, with no sensitive downstream actions. Stop or secure the endpoint immediately afterward.
The caller's method must match the node. The Webhook node supports common methods including DELETE, GET, HEAD, PATCH, POST, and PUT. Copy the node's actual URL rather than constructing a path from memory. A manually selected path helps callback management but does not authenticate callers.
Choose the response mode deliberately
- Immediately: acknowledge the request without waiting for all downstream work.
- When Last Node Finishes: wait for processing and return the configured result.
- Using Respond to Webhook Node: explicitly control the response in the workflow.
- Streaming response: where supported by the installed version and compatible nodes, stream output to a compatible caller.
Providers often impose short acknowledgement deadlines. Waiting for slow downstream work can trigger retries even when processing eventually succeeds. Conversely, immediate acknowledgement is not proof that all workflow actions completed. Return only the acknowledgement or result the provider needs, and monitor downstream failures separately.
Plan payload limits
n8n's documented default webhook payload maximum is 16 MB, with N8N_PAYLOAD_SIZE_MAX available for self-hosted configuration. Check the setting's supported units and behavior for your release before changing it. Provider limits, intermediary limits, available memory, and timeout behavior also matter. Raise a limit only for an understood requirement, then retest with controlled input.
Verify test, production, and unintended routes
Capture a test event
- Select the node's Test URL and choose Listen for Test Event.
- Copy the complete URL and confirm its public HTTPS hostname.
- Send the following synthetic request, replacing the entire example URL.
curl -i -X POST "https://your-public-host.example/webhook-test/localtonet-check" \
-H "Content-Type: application/json" \
-d '{"event":"localtonet-check","id":"demo-001","secret":false}'
Expect the response configured in the node and captured input in the editor. The immediate response commonly includes Workflow got started, but do not make a health check depend on that exact text if the version or response configuration differs. Verify the sample event and identifier rather than only an HTTP success status.
Verify the production callback
- Publish or activate the workflow using your installed version's controls.
- Copy the Production URL.
- Send a new event to that exact address.
- Inspect the workflow's Executions tab for the matching input and result.
curl -i -X POST "https://your-public-host.example/webhook/localtonet-check" \
-H "Content-Type: application/json" \
-d '{"event":"localtonet-check","id":"demo-002","secret":false}'
Production data does not appear as a temporary test capture. Execution visibility also depends on retention and execution-saving settings. Correlate the event identifier with execution records and logs, then test from the actual provider using its test-delivery feature where available.
Inspect the public service surface
Open the public base URL in a private browser window and inspect its response headers:
curl -I "https://your-public-host.example/"
A login page or sign-in redirect confirms that more than the webhook path is reachable. Authentication can restrict use, but it does not make the editor private. A HEAD request alone is not a complete route audit; check browser behavior and any routes relevant to your deployment.
If you introduce a path-filtering proxy, tunnel to that proxy instead of directly to n8n. Permit only the required callback routes, deny unrelated routes, and repeat both positive and negative tests. Exact proxy configuration is outside this direct-tunnel tutorial because route requirements and proxy software vary. Recalculate N8N_PROXY_HOPS for the changed chain.
Authenticate and validate public requests
Public reachability and authorization are separate concerns. With Localtonet, we provide the route; n8n and the workflow must decide whether to accept the request. Treat every body, header, parameter, and query value as untrusted.
Provider signatures may cover exact body bytes, timestamps, and other values. Preserve the raw request body when required, using the Webhook node's Raw Body option where applicable. Follow the provider's algorithm, use an appropriate constant-time comparison, and reject stale requests when the protocol defines a replay window. Generic Header authentication is not automatically equivalent to HMAC verification.
Test rejection paths as deliberately as successful delivery: missing credentials, incorrect credentials, invalid signatures, malformed input, and duplicate events. Authentication failures must not trigger sensitive side effects. For requests requiring a specific rejection response, verify the response flow rather than assuming an immediate acknowledgement can later be replaced.
Do not use optional request-filter expressions as your only security boundary. In particular, this guide makes no claim about the failure behavior of an Only Run If expression. Availability, evaluation order, and error handling must be confirmed for the pinned release before relying on such an option. Explicitly test failure cases and keep authorization independent of convenience filters.
Providers may retry deliveries after a timeout or connection failure. Use a stable provider event identifier and durable deduplication or idempotent downstream operations where repeated processing could cause harm. Acknowledgement timing alone cannot prevent duplicate payments, messages, or record creation.
- Keep device tokens and provider secrets out of workflow payloads and source control.
- Use credential-management facilities rather than embedding secrets in callback URLs.
- Review execution retention, error output, and backups for sensitive data.
- Remember that downstream APIs, messaging systems, and external storage may receive workflow data.
- Use non-sensitive synthetic events for routine health checks.
This HTTP tunnel publishes a configured local web target through a public address. Our separate VPN Manager provides private mesh networking; it is not the public webhook mechanism used here.
Operate, update, back up, and recover n8n
Routine startup and recovery
After a reboot, start Docker and confirm its engine is ready. From the original project directory, run:
docker compose up -d n8n
docker compose ps
docker compose logs --tail=100 n8n
Verify local browser access, run the Localtonet client, confirm the correct device is connected, and start the tunnel if necessary. Then check the public hostname, production workflow registration, and a synthetic callback. Configure client startup using the documented method for your operating system; do not assume Docker's restart policy manages it.
For a process restart without configuration changes, use docker compose restart n8n. For changed environment variables or image versions, use docker compose up -d so Compose can recreate the service. Keep a short recovery checklist and test it after an intentional reboot.
Back up the persistent data consistently
The default deployment stores its database and instance configuration under /home/node/.n8n. The encryption key used for stored credentials must be preserved with the database. If you supply a key externally, back it up separately through secure secret-management procedures. Workflow JSON exports alone are not a full instance backup.
For this small single-container setup, a straightforward cold backup avoids copying a changing database. Pause provider deliveries where possible, stop the tunnel, and allow in-flight work to finish. Then stop n8n and copy its entire data directory from the stopped container:
docker compose stop n8n
mkdir -p backups/pre-update/n8n-data
docker cp "$(docker compose ps -aq n8n)":/home/node/.n8n/. \
./backups/pre-update/n8n-data/
cp compose.yaml .env backups/pre-update/
Use a fresh backup directory for each snapshot rather than merging generations. Restrict access: this copy can contain the database, encryption material, and sensitive execution data. Encrypt backups, keep a copy outside the host, and record the exact image tag or digest. Do not paste backup contents into troubleshooting tickets.
After confirming the copy completed successfully, restart the original service if you are not updating it. A backup is not proven until restored and tested. Additional bind mounts, external databases, or externally configured binary data require their own consistent backups.
Perform a controlled update
- Read release and migration notes for the target version, including any required intermediate upgrades.
- Record the current image version and create a complete pre-update backup.
- Change
N8N_VERSIONin.envto the selected exact release tag. - Pull and recreate the service during a planned maintenance window.
- Verify locally before restoring public delivery.
docker compose pull n8n
docker compose up -d n8n
docker compose logs --tail=100 n8n
Check login, workflow availability, credential usability, generated webhook URLs, proxy behavior, and both test and production callbacks. Confirm authentication failures still reject requests and unintended routes have not become accessible. Inspect provider delivery history for failed events requiring controlled replay.
Do not treat switching back to an older image as a guaranteed rollback. Database migrations may make the upgraded data incompatible. A reliable rollback restores the matching pre-update data and configuration with the previous image version.
Restore into an isolated project
Practice restoration with the public tunnel stopped and without allowing restored workflows to produce real downstream side effects. A restored instance may contain active schedules and production credentials. Use an isolated environment with controlled outbound access for a rehearsal.
Create a separate project directory, copy the backed-up compose.yaml and .env into it, and ensure the original service is stopped if reusing port 5678. Keep the original image version. Using a different Compose project name creates a separate default named volume for this configuration:
docker compose -p n8n-restore create n8n
docker cp /absolute/path/to/backup/n8n-data/. \
"$(docker compose -p n8n-restore ps -aq n8n)":/home/node/.n8n/
docker compose -p n8n-restore run --rm --no-deps --user root \
--entrypoint chown n8n -R node:node /home/node/.n8n
docker compose -p n8n-restore up -d n8n
Replace the backup path with the actual location. The ownership correction lets the image's normal user access restored files. If a future image changes its user or tooling, adapt this step to that release. Verify logs, login, workflows, and credential decryption before reconnecting any public route. Do not run the original and restored production instances concurrently.
Container recreation normally retains the volume, but host failure, accidental deletion, and destructive commands can still remove data. Avoid docker compose down -v and volume-pruning commands for this deployment unless deletion is intentional and recovery has been verified.
Troubleshoot from the inside out
| Symptom | Likely cause | Action |
|---|---|---|
| Localhost does not load | Stopped service, startup failure, or port conflict | Inspect Compose status, logs, and the loopback port mapping. |
| Another LAN device cannot connect | Expected loopback-only binding | Keep the client on the host, or intentionally implement the secured alternate topology. |
| Local access works but public access fails | Disconnected client, stopped tunnel, or wrong target | Check device connection and tunnel state separately; test the target from the client's context. |
| Test webhook is not registered | Temporary listener expired or was not started | Choose Listen for Test Event and resend while it is waiting. |
| Production webhook does not execute | Unpublished workflow or wrong route/method | Publish or activate it and recopy the production URL and HTTP method. |
| URLs still show localhost | Environment change not applied | Check WEBHOOK_URL and recreate the container rather than only restarting it. |
| Wrong client address or proxy warnings | Incorrect proxy trust or forwarded headers | Inspect the actual chain and verify N8N_PROXY_HOPS without guessing. |
| Legitimate request receives 403 | Allowlist mismatch or incorrect source interpretation | Check provider ranges and trusted-proxy behavior; retain strong request authentication. |
| Large payload is rejected | n8n or an intermediary limit | Measure the request and review every applicable limit before raising one. |
| Provider times out but workflow finishes | Slow acknowledgement | Review response mode, provider deadlines, and duplicate-event handling. |
| Callbacks fail after reboot | A dependency did not restart | Recover Docker, n8n, client, and tunnel in order, then send a synthetic event. |
| Callbacks fail after replacing the tunnel | Stored hostname is stale | Update WEBHOOK_URL and provider registrations, then verify again. |
Maintain a callback inventory containing provider, workflow, path, method, public hostname, authentication scheme, owner, and last successful verification date. Do not store secrets in the inventory. Stop temporary tunnels when finished, and disable provider registrations that would otherwise continue retrying. Delete unused tunnel configurations only after checking which integrations depend on them.
Frequently asked questions
Why bind Docker port 5678 to 127.0.0.1?
The host-side Localtonet client can reach loopback without making the port directly available on every host interface. This reduces direct LAN exposure but does not restrict routes forwarded through the public tunnel.
Is WEBHOOK_URL enough behind a reverse proxy?
No. It sets the external callback base URL. Configure N8N_PROXY_HOPS for the actual trusted HTTP proxy chain and verify forwarded header handling. Do not assume one hop applies to every deployment.
Should a provider use the test or production URL?
Use the test URL only while the temporary listener is active. Use the production URL for ongoing callbacks after publishing or activating the workflow, and inspect production execution history.
Does the tunnel expose only the Webhook node?
No. A basic HTTP tunnel forwards to the configured web service. Other n8n routes can remain publicly reachable. A private editor requires explicit route separation or another suitable architecture.
Does self-hosting mean webhook data never leaves my device?
No. Incoming requests traverse the provider and public Localtonet relay path. n8n runs locally with locally configured persistence, but downstream services, external storage, and backups may also receive data.
Does the tunnel authenticate webhook callers?
Public connectivity does not replace authorization. Configure compatible Webhook node authentication, verify provider signatures, validate input, and use least-privilege downstream credentials.
Will callbacks work while the host is asleep?
No reliable delivery can occur while the host is asleep or offline. Docker and n8n must run, the selected Localtonet client must be connected, and the tunnel must be started. Do not assume offline requests are queued by this setup.
Do I need router forwarding, a public IP, or a VPN?
Not for the same-host HTTP tunnel workflow. Our client connects outbound to the relay. The separate LAN-client topology may require local access rules, but it still does not require exposing n8n through router port forwarding.
Connect your local n8n webhook with Localtonet
Start with loopback-only local access, create an HTTP tunnel, configure the public URL and verified proxy trust, and test both callback delivery and unintended exposure. Add authentication and a tested recovery plan before processing real events.
Get Started Free โ