Michael Haren’s Wassupy Blog

Expose self-hosted Home Assistant to the internet (and companion app) with Cloudflare Tunnels and Docker Compose

in technology, code, and homelab

Prerequisites

This guide assumes you have the following:

  1. A Cloudflare account (free)
  2. A domain name (bought from Cloudflare or elsewhere; $10/yr), set up in Cloudflare
  3. A linux server with docker, which can be the same one where you run Home Assistant, but doesn’t need to be

The general idea

Web users connect to Cloudflare, and instead of Cloudflare reaching into our network, we run a service inside the network that reaches out to Cloudflare:

CF Tunnels Diagram of a client connecting to a Home Assistant instance inside a homelab via Cloudflare Tunnels. The diagram has three entities: client, Cloudflare, home lab. The client is connected to Cloudflare. The home lab has two docker containers. The first is labled "CF Tunnel" and it is connected to the Cloudflare entity, and the other container: Home Assistant Client Cloudflare Homelab Containers CF Tunnel Home Assistant

We are not opening any ports, e.g. http/https from our server to the local network or internet. We are not messing with dynamic dns stuff. We don’t need to set up TLS.

An example with Docker Compose

Create the tunnel config at Cloudflare

First create your tunnel in the Cloudflare dashboard > Zero Trust > Networks > Tunnels:

  • Choose cloudflared
  • Name it whatever you want, e.g. homelab
  • Click the “docker” installation instructions just to grab the token
  • Don’t actually install anything

Copy that token for the next section.

On your homelab server

Create docker-compose.yaml like this, with the token you copied above:

version: "3.3"

services:
    tunnel:
        image: cloudflare/cloudflared
        restart: unless-stopped
        command: tunnel run
        environment:
            - TUNNEL_TOKEN=***insert CF Tunnel token here***

Start up the containers:

docker compose up -d

Tail the logs:

docker compose logs -f

Back in the Cloudflare dashboard

At this point, the Cloudflare dashboard should show your tunnel as healthy. If not, read those logs carefully for errors and fix it before continuing.

Add a public hostname to your tunnel:

  • subdomain: anything you want (this DNS record will be added for you)
  • domain: pick the domain you already set up in Cloudflare
  • service: choose http, and set url to the internal IP and port of your Home Assistant inac name of your docker service. E.g. http://192.168.1.100:8123
  • additional application settings: none

With that done, you should be able to browse to the subdomain you set up, and the response will be tunneled over from your homelab server. You’ll probably get a 400 error. Don’t fret—you’re almost done.

Now you need to tell Home Assistant that the Cloudflare tunnel is allowed to proxy it. You can see the exact IP you need in the Home Assistant error log, e.g.:

“A request from a reverse proxy was received from 172.21.0.8, but your HTTP integration is not set-up for reverse proxies”

That exact IP could change when your cftunnel container restarts, but the first two parts of the network probably won’t, so you can just trust the whole thing like this, in HA configuration.yaml:

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 172.21.0.0/16 # add the actual IP of your cftunnels container, or the network it's on

(And restart your HA instance.)

You can also find the anticipated proxy IP by checking the network details, too. First, list your docker networks:

docker network ls

NETWORK ID     NAME                     DRIVER    SCOPE
37c19c0df841   bridge                   bridge    local
da9f58733dc9   home-assistant_default   bridge    local
587ff440738c   host                     host      local
30a44b8290f3   none                     null      local
0e1df72bc30c   sites_default            bridge    local

Then pick the one where you’re running your cftunnels container (not the one running your HA instance!). In my case, it’s in a folder called sites, so I need the last one. Inspect it:

// docker network inspect sites_default 

[
    {
        "Name": "sites_default",
        "IPAM": {
            "Config": [
                {
                    "Subnet": "172.21.0.0/16", // <-- this is what you want
                    "Gateway": "172.21.0.1"
                }
            ]
        },
        "Containers": {
            "6626c238...": {
                "Name": "sites-tunnel-1",
                "IPv4Address": "172.21.0.8/16"
            }
        }
    }
]

Do I need to do TLS, HTTP compression, etc.?

No. Cloudflare handles TLS and HTTP compression for you. The tunnel is encrypted so no additional TLS is necessary for that leg.

I’m still getting a 400 error, or something else isn’t working

Check the logs!