← Architecture
architecture

All I Wanted Was to Reach Home Assistant from the Internet

Remote access to Home Assistant sounded simple. Then came domains, HTTPS, Caddy, split DNS, AdGuard Home and the important question of who is actually in charge of the network.

All I wanted was…

to reach Home Assistant from the internet.

Then it escalated quickly.

The requirement sounded completely normal

Home Assistant is useful at home.

It is considerably more useful when I am not at home.

I wanted to be able to open it from my phone, see what the house was doing, check an alert or change something without first connecting to a VPN or remembering an IP address.

That was the requirement.

Give Home Assistant a proper internet address and make it work.

The obvious answer was a domain name, HTTPS and a reverse proxy.

So I gave the house a public name, put Caddy in front of Home Assistant, and got encrypted remote access working.

Job done.

Well, nearly.

The same name should work inside the house too

Once a service has a proper hostname, typing an IP address on the local network feels ridiculous.

If Home Assistant has a sensible address from a hotel room, it should have a sensible address from the sofa as well.

That is where the network started becoming more interesting.

From outside the house, a hostname such as home.odegaard.eu needs to find the public edge of the network and arrive at the reverse proxy.

Inside the house, there is no good reason to send that traffic on an unnecessary trip out toward the internet just to find a service sitting a few metres away.

What I wanted was:

public DNS outside, local knowledge inside.

That is split DNS.

The name can remain friendly and predictable while the answer changes depending on where the request comes from.

Then I wanted names for everything else

Home Assistant was only the beginning.

Once you have stopped remembering one IP address, remembering the other ones starts to feel equally silly.

The local network gained names for infrastructure and services that should never need to be exposed publicly. A local hostname such as ha.odegaard.eu can point directly at Home Assistant inside the house. A service such as climate.odegaard.eu can exist only on the LAN.

That gives me two useful patterns:

Need DNS behaviour
Service should work from anywhere Public name externally, sensible local path internally
Service should stay inside the house Local DNS only; no public route required

The names are not the clever part.

The useful bit is that I no longer need to care which machine or container currently owns the address behind them.

Services can move. The name stays useful.

AdGuard Home earned a permanent job

At roughly the same time, AdGuard Home became useful enough to keep.

It filters DNS for the house, removes a fair amount of advertising and tracking traffic before it reaches clients, and gives me a useful view of what devices are actually asking for.

For a smart home, that last part is surprisingly interesting.

A laptop making lots of DNS requests is normal.

A cheap IoT device trying to contact a collection of unexpected endpoints every few minutes is worth noticing.

So AdGuard became the resolver most clients use.

And that created the first architecture rule I actually care about:

Useful does not mean authoritative for everything.

I did not want the DNS filter to slowly become the router by accident.

One job, one owner

The router remains responsible for the basic network truth:

  • gateway and routing
  • firewalling
  • DHCP
  • WAN selection
  • which addresses belong to clients

AdGuard Home is responsible for DNS filtering and the local answers I deliberately give it.

Caddy is responsible for getting web traffic to the right internal service and handling HTTPS.

Home Assistant is responsible for being Home Assistant.

That sounds almost comically obvious when everything is working.

It becomes much less obvious at 2 AM when one box is down and half the house suddenly cannot resolve a hostname.

Having one clear owner for each function means I can ask a much better question:

Which layer is actually broken?

The failure mode I did not want

There is a particularly easy mistake to make in a home lab.

DNS stops working.

You log into another box.

You turn on DHCP there because it seems helpful.

Congratulations: you now have two DHCP servers, and the original DNS fault has been upgraded into a network-wide guessing game.

I wanted recovery to be boring instead.

If a hostname fails, test DNS.

If DNS looks right, test the service directly.

If the service works by address but not by name, stay in the DNS layer.

If the service itself is down, fixing DHCP will not improve matters.

The rule became simple:

Do not create a second authority just because the first one is having a bad day.

That principle has survived every later network experiment.

Fallback is not real until it has failed

Another lesson was that a configuration file saying there is a fallback DNS server does not prove clients will actually behave the way you expect.

Different devices cache DNS differently.

Some retry quickly.

Some hold on stubbornly.

Some appear to have accepted a secondary resolver and then behave rather creatively when the primary disappears.

So I distinguish between two things:

Configured fallback — I can see the setting.

Proven fallback — I have actually taken the preferred path away and watched the network recover.

The second one is the only version I trust.

It is the same philosophy I later applied to the backup internet connection: a standby path is not really part of the design until it has carried traffic during a test or a genuine outage.

Where OpenWrt fitted in

At this stage of the project, OpenWrt was the network authority.

That made it a good place for DHCP, routing policy and the local network view, while AdGuard remained a separate filtering service.

I liked that split because replacing or rebooting AdGuard did not require reinventing the LAN.

Equally, changing router hardware did not require throwing away the DNS policy.

That separation turned out to be useful later when the router side of the project became considerably more adventurous.

The router implementation can evolve.

The rule does not:

the router owns the network; the DNS service improves it.

The result in daily use

Most of this disappears when it is working properly.

That is the point.

I open Home Assistant using a name rather than an address.

The same mental model works whether I am in the house or away from it.

Internal-only tools get useful names without being exposed to the internet.

Ads and a chunk of tracking disappear at the DNS layer.

If I move a service to another VM or container, I change the network plumbing once rather than teaching everyone in the house a new address.

And if something stops working, the architecture gives me a sensible order to troubleshoot it.

That is a much better outcome than the original collection of bookmarks containing IP addresses.

Was all of this necessary just to reach Home Assistant?

Strictly speaking, no.

I could have exposed one port, remembered one address and stopped there.

But that would have solved the first request rather than the underlying problem.

The useful version was not simply:

make Home Assistant reachable from the internet.

It became:

give services stable names, keep local traffic local, expose only what needs exposing, filter DNS without making it the network authority, and make recovery understandable.

Which is quite a lot of architecture for wanting to check Home Assistant from my phone.

But at least I do not have to remember the IP address.