Self-hosting n8n with no inbound access, has anyone done this?

first time self-hosting n8n on-prem in a restricted network
environment and wanted to sanity check my approach with people who’ve
done this before.
The setup:

  • On-prem Ubuntu VM, internal network only, no public route
  • No public DNS for the instance
  • Docker Compose: n8n + Postgres + Caddy + Uptime Kuma
  • Community edition, versions pinned, 2FA enforced, credentials in n8n’s
    encrypted store, encryption key stored separately from DB backups
    The constraint driving everything: any inbound path is treated as new attack
    surface that needs its own review. Default answer is “no ingress.” Editor
    is internal-only, end users only ever receive output (email etc).
    My plan for triggers:
    Most of what we’re building isn’t real-time. Tolerable latency is minutes,
    not seconds. So I’m swapping webhooks for scheduled polling on the first
    batch of workflows (Graph, Zoom recordings, mailbox checks). No inbound
    needed, and outbound calls fit a small allowlist for the network team.
    For editor access without DNS I’m planning hosts file entries on the
    machines that need it, with Caddy’s self-signed cert accepted once per
    machine. Nothing gets published anywhere.
    Stuff I’ve already ruled out so we don’t go there:
  • Cloudflare Tunnel / Tailscale Funnel — TLS terminates at the provider,
    doesn’t work for us
  • Public A record pointing at a private IP — works but leaks the hostname
    and internal addressing forever (CT logs). Would only do this if the
    simpler options fail
    What I’d love input on:
  1. Anyone running production n8n outbound-only? What bit you that I’m
    probably not seeing?
  2. Polling frequency in practice — how do you handle EXECUTIONS_DATA_PRUNE
    and DB bloat when most runs find nothing?
  3. Heartbeat/monitoring — workflow-level “success” is misleading when
    you’re polling (a broken poll looks the same as a quiet day). What do
    you actually alert on?
  4. For anything that eventually needs push, where did you draw the line
    and what pattern did you land on? WAF + IP allowlist, reverse tunnel,
    message queue in between?
  5. Any gotchas with N8N_HOST / WEBHOOK_URL when the instance has no
    public name?
  6. Community nodes — they run with full credential access. How are you
    vetting third-party ones before install?
    Happy to write back with what we end up doing. Cheers

Hey @Olu, while you wait for a response, here are some things that might help:

Suggested resources

Automatically matched to your question.

Docs:

Forum:

@hoiyothaheem, @Parintele_Damaskin, @Glorious - you’ve helped with similar issues before, can you take a look?

Automatically suggested by n8n’s community bot. It’s a pilot - please share feedback here.

Welcome @Olu!
On DB bloat with polling: set EXECUTIONS_DATA_PRUNE=true plus EXECUTIONS_DATA_MAX_AGE (hours) and EXECUTIONS_DATA_PRUNE_MAX_COUNT together, the count cap matters more than age when polls run every few minutes and mostly find nothing. On monitoring, don’t alert on workflow success, alert on “no execution recorded in X minutes” via Uptime Kuma hitting a lightweight endpoint your polling workflow pings on each run, that catches a silently dead poll that a green checkmark would hide. For N8N_HOST/WEBHOOK_URL with no public name, set WEBHOOK_URL explicitly to your internal Caddy URL, otherwise n8n falls back to guessing from the request and breaks any node that generates callback URLs (like Wait/Form nodes). For community nodes, run them in an isolated queue-mode worker first with a scoped service account before promoting to your main instance, that limits blast radius if one turns out to be misbehaving.

Really appreciate this; all four points are going straight into the config. The PRUNE_MAX_COUNT one especially.

I was planning age-only and would have watched the table swell. The Uptime Kuma push pattern is much cleaner than what I thought of.
Still curious if anyone’s landed on a specific pattern for when you eventually do need push (question 4) - WAF + IP allowlist is what I’m working toward for that day, but interested in what others have tried.

Hi @Olu
Of your three options, the queue is the one that keeps n8n outbound-only. The AMQP, RabbitMQ and MQTT triggers are not HTTP listeners, the node dials the broker itself, so nothing binds a port for it and TLS terminates on your broker. The RabbitMQ credential takes a CA cert plus a client cert and key, so you can mTLS that hop instead of trusting a provider.
It relocates ingress rather than removing it. Graph and Zoom still have to POST somewhere, so a small DMZ receiver verifies the vendor signature and publishes to the queue. That is a single-purpose endpoint your network team can review properly, unlike n8n’s editor and REST API sitting behind a WAF and an IP allowlist you have to re-check every time a vendor rotates ranges.

Thanks, the “relocating ingress rather than removing it” point gives me a much clearer picture of what a future push-based setup could look like.
Appreciate you are taking the time to share your experience.

Hey!
i will be short on what i may add something to respective points.

  1. beside what other community helpers and @Anshul_Namdev said, if you stick with pooling , then you can as well per workflow settingss enable only the failed executions to be saved.
    Adding an IF node to check for redundant data and stop(or start subworkflow) will save as well not running the whole workflow.

  2. at the end of the succesuful executtion , i would ping with the workflow id and make aditional logic , like… in X time if no pings from that id, sonehting is wrong.

  3. hmm.. i use nginx and i can say that i simply map some weird location blocks(so creating fake internal path[but carefull on oauth’s] and playing further with the n8n env variables. .

  4. Verified nodes are verified by some cool persons , and vouch for them… community nodes are build and verified by “trust me bro”(unless you know what are you doing and the impact it may have)…

My summary if worth the pennies .
If i didnt answered to other points, it means is doable (1. my localhost pc is my “production” in this RESTRICT case maybe. 2. tunnel )…

Cheers!
Automatically suggested by 3 coffess. It s a pilot since i was tagged by @n8n :slight_smile:

On the monitoring question specifically — a polling trigger that returns nothing looks identical to a polling trigger that’s broken, and neither shows up under failed executions. The cheapest fix is a separate schedule workflow that checks the last successful run timestamp for each polled workflow and alerts when it goes stale. One check catches a dead trigger, an expired credential, and a workflow somebody quietly deactivated.

For the bloat, set ‘save successful production executions’ to ‘do not save’ per workflow on the high-frequency pollers rather than turning it off globally. You keep full history where you’d actually want it during an incident and stop the writes where they’re coming from.

Thanks, I think I’ve gotten clarity…the instance is live and it’s currently polling. I applied the suggestions above for what applies to the company. I appreciate your response.

Thanks for the information, it saved me some time too.