Cloudflare Tunnel Self Hosted Services: The Honest 2026 Setup Guide for Home Assistant, n8n, and Immich

There is exactly one sane way to access your self-hosted apps from outside your home network without turning your router into a liability: Cloudflare Tunnel. Not port forwarding. Not a $10/month VPS running nginx as a reverse proxy. Not hoping your ISP gives you a static IP.

This guide covers the full cloudflare tunnel self hosted services setup, from the first docker-compose.yml line to the specific gotchas that freeze your Home Assistant dashboard, break your n8n webhooks, and stop Immich from accepting any photo file bigger than 100MB. Most guides on this topic cover one service, confirm the concept works, and leave you on your own. This one covers the full stack: Home Assistant, n8n, Immich, and Vaultwarden, in a single tunnel with working Docker Compose configs.

If these services are already running on your home server and you want them reachable from anywhere, this is the guide. No open ports. No exposed home IP address. No monthly VPN bill.

If you haven’t set up these services yet, start with the cluster posts first: the n8n beginner’s guide and the Home Assistant dashboard guide. Get the services running locally before you try to expose them.

Why Port Forwarding Is the Wrong Answer

Leaving port 8123 open on your home router is a lot like nailing your home address to a public bulletin board that every automated port scanner on the internet reads continuously. You might get lucky for a while. Or you might find someone poking at your Home Assistant login page, trying default credentials, or worse.

Port forwarding works. It’s just reckless.

The alternative most people reach for next is a VPN. Tailscale is excellent for this and I’ve covered it in depth in this post. But Tailscale is private-mesh-only by design: your devices connect to each other directly, but you can’t hand out a public URL to someone who doesn’t have Tailscale installed. No external webhooks. No sharing a dashboard with someone outside your network.

That’s where Cloudflare Tunnel fits. Your server makes an outbound connection to Cloudflare’s edge network. Cloudflare routes incoming requests through that connection to your local service. Your home IP never appears anywhere. No inbound firewall rules required. Nothing listening for external connections on your router.

That’s the whole concept. Now let’s build it.

What You Need Before You Start

Skipping prerequisites is how you end up 30 minutes in, staring at a “502 Bad Gateway” and no idea which layer broke.

You need four things:

A domain name managed through Cloudflare DNS. This is non-negotiable. Tunnel routing requires a domain on Cloudflare’s nameservers. The free Cloudflare plan covers everything. If your domain is at Namecheap or GoDaddy right now, you just need to update the nameservers to Cloudflare’s and wait for propagation (usually under an hour).

Docker and Docker Compose installed on your server. If you’re starting from scratch, the Linux home server setup guide covers getting from a bare machine to a working Docker environment.

At least one self-hosted service already running. You should be able to hit Home Assistant on its local port before you touch any tunnel config. The service has to work locally before you try to expose it.

A free Cloudflare account. Sign up at cloudflare.com if you don’t have one. Nothing paid required.

That’s it. Four things. If you have all four, you’re ready.

Cloudflare Tunnel vs. Tailscale: Pick the Right Tool?

I covered Tailscale in depth in this post, so I’ll keep this comparison tight. They solve different problems and you probably want both running.

Use Tailscale when:

  • You want private device-to-device access for yourself (SSH into your server, internal dashboards, admin panels)
  • The people who need access already have Tailscale installed
  • You don’t need a public-facing URL
  • You want WireGuard-level encryption without managing any WireGuard config

Use Cloudflare Tunnel when:

  • You need a public URL, specifically for n8n webhooks, a shared app, anything an external service calls
  • You want Cloudflare’s authentication layer in front of your services
  • You’re exposing something a non-technical person needs to access from a normal browser

The real-world pattern worth following: use both. Tailscale handles private SSH access and admin tasks. Cloudflare Tunnel handles the one or two things that actually need a public URL. The State of Self-Hosting mid-2026 post covers this split in more detail, and it’s the approach that gives you strong security without giving up flexibility.

Architecture diagram comparing Cloudflare Tunnel for public access versus Tailscale private mesh VPN, with the takeaway to run both
Cloudflare Tunnel exposes a service to the public internet; Tailscale is a private mesh between your own devices. Most homelabs want both.

The Core Setup: Cloudflare Tunnel Self Hosted Services in Docker Compose

This is what most guides skip: a complete cloudflare tunnel self hosted services config in a single file for multiple services.

Here’s the high-level flow. Create a tunnel in the Cloudflare dashboard. Get a token. Run the cloudflared container with that token. Configure which subdomain maps to which local service in the dashboard. All the routing config lives in Cloudflare’s UI, no YAML files to maintain on the server side unless you want them.

Step 1: Create the Tunnel in Cloudflare

Go to one.dash.cloudflare.com. Find Zero Trust in the left nav, then Networks. Cloudflare renamed that submenu from “Tunnels” to “Connectors” earlier this year, but the flow is the same: click Connectors, then Create a tunnel. Name it something like homelab. Select Docker as the connector type. (Cloudflare also added a shortcut straight from the main dashboard at Networking > Tunnels if you would rather skip Zero Trust entirely for this step.)

Cloudflare shows you a full docker run command. Don’t run that. Copy only the token value from it. It’s a long string that starts with eyJ....

Save it to a .env file in your Docker Compose project directory:

CLOUDFLARE_TUNNEL_TOKEN=eyJhIjoiY...your-actual-token-here

Never put this token directly in docker-compose.yml. Put it in .env and add .env to .gitignore. This token gives whoever has it full control of your tunnel.

Step 2: Add cloudflared to Your Docker Compose File

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
    networks:
      - homelab

networks:
  homelab:
    driver: bridge

The --no-autoupdate flag stops cloudflared from trying to update itself inside a container. Without it, the container spams logs with update checks. Your upgrade mechanism should be docker compose pull && docker compose up -d, not an internal auto-updater.

If your services (Home Assistant, n8n, Immich, Vaultwarden) all share the same Docker Compose network named homelab, cloudflared can reach them by their service name. http://homeassistant:8123, http://n8n:5678, and so on. If your services live in separate Compose files, attach cloudflared to each of their networks, or consider consolidating to a single file.

Routing Home Assistant (Read This Before You Touch the Config)

Home Assistant’s cloudflare tunnel self hosted services config has one gotcha that breaks almost every first-time setup: trusted proxies.

When requests come in through Cloudflare Tunnel, Home Assistant sees the source IP as the Docker network gateway, not your actual IP. HA treats unrecognized proxy IPs as untrusted, which breaks WebSocket connections, Lovelace dashboard live updates, and automation state tracking. You’ll connect and the login page will load, and then everything will freeze or show stale data and you’ll spend an hour wondering what’s wrong.

The Home Assistant trusted_proxies issue is the most commonly reported failure mode in setup threads for cloudflare tunnel self hosted services, and it’s buried in the HA docs under a heading that doesn’t hint at why you’d need it. Fix it before you do anything else.

Add this to your configuration.yaml:

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 172.16.0.0/12
    - 10.0.0.0/8
    - 192.168.0.0/16

The 172.16.0.0/12 range covers Docker bridge network addresses. The 10.x.x.x and 192.168.x.x ranges cover typical home LAN and VPN subnets. Restart Home Assistant after adding this. WebSockets will start working correctly.

In the Cloudflare dashboard, when you set up the tunnel’s public hostname for Home Assistant, set the service type to HTTP and the URL to http://homeassistant:8123. If you’ve configured HA with its own self-signed TLS cert (uncommon in a Docker setup, but it happens), use https://homeassistant:8123 and enable “No TLS verify” in the additional application settings for that hostname.

Routing n8n (Don’t Skip This Part)

n8n’s cloudflare tunnel self hosted services config has a specific webhook pitfall that trips up everyone eventually. When n8n generates a webhook URL for a trigger node, it builds that URL from its WEBHOOK_URL environment variable. If that variable isn’t set, n8n generates a URL that looks like http://localhost:5678/webhook/abc123.

GitHub can’t call localhost. Stripe can’t call localhost. Nothing external can call localhost. The webhook looks valid inside n8n but fails silently when triggered.

Add these to your n8n service in Docker Compose:

environment:
  - WEBHOOK_URL=https://n8n.yourdomain.com
  - N8N_HOST=n8n.yourdomain.com
  - N8N_PORT=5678
  - N8N_PROTOCOL=https

Replace yourdomain.com with your actual domain. Restart n8n. Create a new webhook trigger node and the URL it shows you should start with https://n8n.yourdomain.com/webhook/.... That’s the URL your external services call. The n8n beginner’s guide covers how to build automations with those webhooks.

Routing Immich (Know the Upload Limit First)

Point immich.yourdomain.com to http://immich_server:2283 in your tunnel config. That’s the standard Immich Docker setup port. Works for browsing, sharing albums, and daily mobile app syncing without any special configuration.

The thing to know: Cloudflare’s free tier has a 100MB per-request body limit. For most photo uploads this isn’t a problem. The Immich mobile app uses chunked uploads that stay well under 100MB per request, so everyday phone syncing works fine. But if you try to upload a large video file or a batch of RAW files through the web interface and one of them exceeds 100MB, you’ll see a 413 error or a silent failure.

For bulk initial imports from a Google Takeout archive, run the import from inside the server rather than through the tunnel. The Immich CLI import documentation covers this process. Run that job locally, then use the tunnel for everyday access once your library is in place.

If you eventually want larger web uploads, know that Pro doesn’t help here: the free and Pro plans share the same 100MB cap. The limit only rises on Business (200MB) or Enterprise (500MB), and neither is worth the money for home use. The cheaper answer is to keep bulk uploads off the tunnel entirely. Upload big media to Immich over your LAN or Tailscale, and let the tunnel handle everyday access. For most personal homelab use, the mobile app’s chunked upload makes this a non-issue.

Routing Vaultwarden (Easiest One Here)

Vaultwarden runs on port 80 internally by default in the standard Docker setup. The tunnel config is simple:

# In Cloudflare dashboard:
# Zero Trust > Networks > Tunnels > Configure > Public Hostnames > Add a hostname
# Subdomain: vault
# Domain: yourdomain.com
# Service type: HTTP
# Service URL: vaultwarden:80

No special headers. No WebSocket config. No extra environment variables. Vaultwarden handles the rest. One thing to be aware of: if you’re using Vaultwarden’s built-in /admin panel, don’t leave it publicly accessible through the tunnel. Either disable it when you’re not actively using it, or put it behind Cloudflare Access (next section) with a separate policy requiring your email to authenticate.

The Full Ingress Config (for Version Control)

If you prefer version-controlled routing configs, you can create the tunnel via the cloudflared CLI instead of the dashboard. This is a different approach from the token method above, not something you layer on top. With the CLI approach, you run cloudflared tunnel create homelab on your server, which gives you a tunnel UUID and downloads a credentials.json file. You then define all routing in a local config.yml rather than the Cloudflare dashboard. Here’s the template:

tunnel: YOUR-TUNNEL-UUID
credentials-file: /etc/cloudflared/creds/credentials.json

ingress:
  - hostname: ha.yourdomain.com
    service: http://homeassistant:8123
  - hostname: n8n.yourdomain.com
    service: http://n8n:5678
  - hostname: immich.yourdomain.com
    service: http://immich_server:2283
  - hostname: vault.yourdomain.com
    service: http://vaultwarden:80
  - service: http_status:404

That final - service: http_status:404 line is required. It’s the catch-all rule for any hostname that doesn’t match your configured routes. Leave it out and cloudflared won’t start.

To use this file, mount it into the container and update your Compose service to reference it:

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run --config /etc/cloudflared/config.yml
    volumes:
      - ./cloudflared:/etc/cloudflared
    networks:
      - homelab

Your ./cloudflared/ directory needs both config.yml and the creds/credentials.json file the cloudflared tunnel create command downloaded. The token-based approach from Steps 1-2 does not use this file layout. Pick one method and stick with it for a given tunnel.

3 Failure Modes Nobody Warns You About

Most cloudflare tunnel self hosted services guides walk through the happy path and call it done. These are the three things that will actually get you after setup.

Failure Mode 1: Home Assistant dashboard loads but everything freezes

You’ve added the trusted_proxies config and it’s still broken. Before you dig further, check this in the Cloudflare dashboard: go to your tunnel, click Configure, go to Public Hostnames, click on your HA hostname, then look under Additional application settings. There’s a toggle for HTTP2 Connection. For Home Assistant, try setting this to Off. Some HA versions in Docker don’t handle HTTP/2 through the tunnel cleanly, and forcing HTTP/1.1 fixes the WebSocket behavior.

If you’re still seeing issues after that, double-check that configuration.yaml is actually loading. In HA Docker, changes to that file don’t take effect until you do a full restart (docker compose restart homeassistant), not just a configuration reload from the UI. The UI reload handles most things, but the http: block is one of the sections that requires a full container restart to take effect.

Failure Mode 2: n8n webhook URLs still show localhost after setting WEBHOOK_URL

Two things to check. First: did you actually restart n8n after adding the variable? The environment variable only takes effect at container start, not on a config reload. Run docker compose stop n8n && docker compose up -d n8n and check the webhook URL in a new node. Second: look for N8N_EDITOR_BASE_URL in your environment config. If that’s set to something else, it can conflict with the webhook URL display. For most setups, WEBHOOK_URL alone is enough.

Failure Mode 3: Large Immich uploads fail without a clear error

You’ll see a 413 “Request Entity Too Large” response in the browser’s network tab, or the upload just stalls and times out. This is Cloudflare’s 100MB body limit, not an Immich bug. If you’re on the Immich mobile app and hitting this, update to the latest version. Older app versions used a single-request upload that could exceed 100MB; newer versions chunk. If you’re hitting this on the web interface with video files or large RAW images, use the mobile app for those files or run the Immich CLI directly on the server to bypass the tunnel for bulk transfers.

Zero Trust Access: Lock It Down Beyond a Password

A public subdomain pointing to your Home Assistant is only protected by your HA login. That’s one layer. Cloudflare Access adds a second layer before the request even reaches your service.

The free Cloudflare Zero Trust plan covers up to 50 users. For a personal homelab this is more than enough.

Go to Zero Trust in the Cloudflare dashboard. Click Access controls, then Applications, then Create new application. Choose Self-hosted and private. Walk through the form:

1. Application name: Home Assistant (or whatever service you’re protecting)
2. Session duration: 24 hours is practical for daily use
3. Subdomain and domain: the hostname you want to protect (e.g., ha on yourdomain.com)
4. Identity providers: Add at least one. Email OTP works without any third-party account. Cloudflare sends you a one-time code each login. GitHub and Google OAuth are both free options if you’d rather use an existing account.
5. Access policy: Set the action to Allow. Add a rule for your email address or email domain.

Save it. Now anyone who tries to reach ha.yourdomain.com hits a Cloudflare authentication page before they ever see your Home Assistant login. Two layers. The first one is Cloudflare’s problem. The second one is HA’s.

One thing to watch: Cloudflare Access injects a Cf-Access-Authenticated-User-Email header into requests. Most apps ignore it. Some apps will auto-login the user based on it. Know your app’s behavior before enabling this, especially for admin-facing services.

Free Tier Reality Check: What Does “Free” Actually Include?

Before committing your cloudflare tunnel self hosted services stack to this approach, here’s exactly what the free tier covers:

Free, no catch:

  • Tunnel creation and operation (Cloudflare documents this as free with no bandwidth caps)
  • Unlimited tunnels on a single account
  • Cloudflare Access / Zero Trust authentication for up to 50 users
  • About 24 hours of request log retention in the Zero Trust dashboard (30 days requires the paid $7/user tier)

Not free:

  • Request body sizes over 100MB per request (Pro is capped at the same 100MB; 200MB requires Business, 500MB requires Enterprise)
  • Advanced rate limiting rules
  • Custom error pages and WAF rule sets

For a homelab running cloudflare tunnel self hosted services for personal or small-team use, the free tier is free in practice. The 100MB upload cap is the only limit that realistically affects a normal homelab setup, and the Immich mobile app’s chunked upload avoids it for daily photo syncing.

One thing Cloudflare’s terms of service do say: you can’t use their network as a CDN for large-scale video delivery or commercial content distribution. Running Home Assistant, n8n, Immich, and Vaultwarden through a tunnel for personal use is squarely in scope. A public video streaming site hosted through a Cloudflare Tunnel is not.

Is Cloudflare Tunnel Worth It for Your Homelab?

Yes. With a real qualifier.

If your self-hosted services are already running in Docker, adding Cloudflare Tunnel takes about 20 minutes and the security improvement over port forwarding is not incremental. It’s categorical. No exposed home IP, no inbound firewall rules to manage, optional authentication layer in front of everything.

The qualifier: Cloudflare Tunnel doesn’t replace Tailscale. They’re complementary. Use Tailscale for private SSH access and server administration. Use Cloudflare Tunnel for whatever actually needs a public URL. Trying to use Cloudflare Tunnel for everything, including private admin access, means every admin task depends on Cloudflare’s network being available. Tailscale for private, Cloudflare for public, is the reliable split.

For what it’s worth, I run Cloudflare for DNS on this blog’s domain and the admin experience there has gotten noticeably better over the past 12 months. On the Zero Trust side, by most accounts the dashboard has gotten less confusing than the early versions, which is saying something. The free tier for tunnels has been stable for years at this point.

If you’re wondering which to set up first, start with Tailscale for private access, then add Cloudflare Tunnel when you have a specific public-facing need. Doing it in that order means you have a working private access solution while you’re learning the Cloudflare side.

The real pro tip from this guide: set up the combination from day one, not as an afterthought. Retrofitting your cloudflare tunnel self hosted services config after the fact is more painful than doing it right the first time, mostly because you have to reconcile two different mental models of how your services are reachable. Start with the full picture.

Sources

Your Turn

If you’ve been putting off remote access for your homelab because port forwarding felt risky (it is) and setting up a full VPN felt like overkill (it is), cloudflare tunnel self hosted services is the path that handles both concerns without a monthly bill or a complicated config.

Drop a comment below: which service are you tunneling first? Home Assistant and Immich generate the most setup questions, so if you’re stuck, describe what you’re seeing and I’ll try to help sort it out.

And if this saved you the frustrating hour of diagnosing why your HA dashboard freezes after connecting, share it with whoever in your group chat is about to forward port 8123 to the open internet. Do it as a public service.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top