Migrating from HomeAssistant OS to HomeAssistant Container

We use HomeAssistant to help control our home automation and have done so for some time now. For those not yet familiar with HomeAssistant, it's an open-source Home Automation suite which comes with a variety of installation options:

HomeAssistant has 4 installation options: HassOS, HA Container, HA Core and HA Supervised

When first setting everything up, I chose to use HomeAssistant OS (HAOS) because it looked like the path of least resistance: It's HomeAssistant's recommended route and provides a low maintenance system with access to the Addons ecosystem.

Over time, however, I've increasingly found that HAOS isn't really a good fit for my needs and so I looked at migrating to using HomeAssistant Container instead (removing HAOS, Supervisor and their dependencies from the equation).

This post talks about how I completed the migration as well as a little more on why I decided to migrate between the two. The data move steps should also work for anyone looking at installing HomeAssistant locally rather than via a container (i.e. those using the "Home Assistant Core" option listed above).


Background

I really like HomeAssistant: We use HA to control a wide range of things, from our aquarium lighting to our heating and I've even figured out how to have Kapacitor alerts trigger HomeAssistant Automations.

However, whilst I'm happy with the application, I've had a number of issues with the other components that make up the HomeAssistant OS stack.

A little while ago, an update changed the way that HomeAssistant uses DNS, introducing a container running coredns (more usually found in Kubernetes deployments). Lookups are sent via this container with the effect of forcing name resolution to go via CloudFlare's 1.1.1.1 DNS service, the consequences of which irked me somewhat.

I wasn't alone in feeling this irritation, nor was it particularly unfounded: As well as breaking local name resolution, the deployed coredns config caused packet storms and caused some users hard to troubleshoot issues. It's also arguable whether it was right (or even legally possible) to have a software update effectively sign users up to the terms of Cloudflare's Privacy Policy without their knowledge (even before you get onto considering the implications of Cloudflare being a moral shitheap of a company).

It's not just the coredns issue, I've also had issues where a HAOS component automatically updated (there isn't a supported way to turn these updates off), leaving it incompatible with the remaining components and rendering my home automation setup broken and in need of manual intervention: automation or not, I need my lightswitches to just work.

These things are annoying, but it's not my intention to rag on the HomeAssistant team (who, I later found out, had had to endure some fairly toxic and unacceptable conversations on the DNS topic, behaviour that's sadly not unknown in software communities).

The underlying problem, really, is that HAOS isn't built with people like me in mind and some of the design decisions were made in order to reduce support overhead on the developers:

We make the decisions we do because with Home Assistant OS we are offering a solutions to users that works, for users that want to focus on about home automation, not system administration.

We used to get a ton of issues with people incorrectly configuring their DNS and then Home Assistant stops working. All those issues disappeared when we did this.

If you're interested in doing DNS stuff, probably Home Assistant OS is not for you. Consider using a Supervised or Container installation.

I might not agree with the outcome, but I can totally understand the desire to lower the support burden.

Although I understand their motives, it doesn't make sense for me to continue using a stack that doesn't meet my needs, so a migration away from HomeAssistant OS has been on the books for some time, carefully filed in the "must get around to that" section of my brain.

Recently, I was prompted to find time for it: HomeAssistant disclosed an authentication bypass vulnerability in Supervisor's API, given the level of control and access that Supervisor has it's a pretty serious hole.

The vulnerability is unfortunate, but these things happen in complex software. It did, however, bring my attention back to the fact that I was relying on a stack that I didn't particularly like and that I was only really still exposed to this vulnerability because I hadn't got around to doing the migration yet.

So, I upgraded (to fix the vulnerability) and set about exploring the migration options.


Planning the Migration

My HAOS install was running on a dedicated Raspberry Pi 4, which meant I had a handful of options:

  1. Switch the SD card and set up on the same Pi, potentially meaning downtime (if I got stuck)
  2. Set the container up elsewhere, then move to the Pi
  3. Set the container up elsewhere and move my USB Zigbee dongle (a Texas Instruments TI CC2531 based thing) over

I was unsure how straightforward the migration was going to be, so discarded option 1. As the initial steps would be the same, I decided I'd choose between the remaining options once I'd done the initial setup and had an idea of the work involved.


Running the Docker Container

HomeAssistant provide some documentation on running HomeAssistant Core with Docker, unfortunately the instructions provided are potentially dangerous:

  • Passing --privileged to Docker by default is extremely unwise. The option gives the container (more or less) the same privileges as the host system: When --privileged is used, you lose all benefits of containerisation (well, aside from ease of deployment), and as such is considered a security risk and is really not something to be taken lightly (i.e. almost certainly shouldn't be the default example in install instructions).
  • Passing --network=host to the container means that the container will bind to the host's network stack and any ports opened "in" the container are immediately available via the host's IP. Removing network isolation like this means that, if the containerised application has a vulnerability, an adversary may able to use it to expose additional services on the host (if the isolation were still in place, they wouldn't be able to reach them even if they could start them).

Essentially, with both options present, the containerised application is effectively no longer containerised: both privilege and network isolation have been removed. This is exacerbated by the fact that HomeAssistant runs as root within the container, so when following the published documentation you'll end up running an internet exposed application as root with no privilege or network isolation active... fun.

Thankfully, neither option is actually necessary (from talking to others, there's a little extra needed if you're using the HomeKit integration, there's a section in the HA documentation detailing what's needed).

To start with, I created a directory to use as the storage volume, this ensures that configuration and state will persist between container restarts.

mkdir -p docker_files/homeassistant/config

Then I started the docker container with a slightly amended command

docker run -d \
--name homeassistant \
-p 8123:8123 \
--restart=unless-stopped \
-e TZ=Europe/London \
-v /home/ben/docker_files/homeassistant/config:/config \
ghcr.io/home-assistant/home-assistant:stable

Note the use of -p 8123:8123 to expose the necessary port, this allows us to remove --network=host (We'll add something to replace --privileged shortly).


Migrating Data

When planning, I'd assumed that the data move would be quite easy: take a backup of the old system and import into the new . However, the current version of HomeAssistant Container no longer offers an option to restore a backup during setup.

In practice, the backup can still be used, it just needs to be restored manually.

I took a backup in the original HomeAssistant

  • Log into HomeAssistant
  • Go to Settings -> System -> Backups
  • Click the Create backup button
  • Provide a name
  • Choose full
  • Click Create
  • Once Complete, click the Download button

I copied the backup to my docker host and extracted it into a temporary directory

mkdir tmp && cd tmp
tar xf ~/cb6b4a6b.tar

The backup archive is actually a tarball of tarballs, covering a range of directories:

$ ls
93f0ddc5_coredns-fix.tar.gz  addons_local.tar.gz  core_ssh.tar.gz     homeassistant.tar.gz   media.tar.gz  ssl.tar.gz
a0d7b954_ssh.tar.gz          backup.json          homeassistant.json  local_telegraf.tar.gz  share.tar.gz

There's a tarball for each of the installed add-ons as well as specific config items. I only needed one tarball though: homeassistant.tar.gz contains the config structure for HomeAssistant.

tar xf homeassistant.tar.gz

This created a directory called data containing the full data and config structure from my original system.

$ ls data/
automations.yaml    custom_components      dns-override-template.main  home-assistant_v2.db-wal  q             secrets.yaml     telegraf.conf  zigbee.db-wal
blueprints          deps                   groups.yaml                 ip_bans.yaml              scenes.yaml   sonoff           tts
configuration.yaml  dns-override-template  home-assistant_v2.db        nest                      scripts.yaml  SonoffLAN-3.3.1  zigbee.db

So, I stopped the docker container, removed the new config directory and moved this directory into place

docker stop homeassistant
rm -r /home/ben/docker_files/homeassistant/config
mv data /home/ben/docker_files/homeassistant/config
docker start homeassistant

HomeAssistant came up with all of my data/configuration in place and actions like toggling Wifi switches worked just fine.


Connecting the Zigbee Stick

Although HomeAssistant was up, because I'd not yet moved the Zigbee dongle over, it obviously couldn't yet talk to any Zigbee devices.

As the initial steps had gone much more smoothly than expected, I decided to throw caution to the wind and move the stick over.

Earlier in the post, I mentioned that the install instructions recommend using --privileged, this is so that HomeAssistant has full access to hardware. It's much (much) safer to map the relevant device(s) through so that you're not throwing away the protections that containerisation can offer.

I connected the USB dongle to the host and ran sudo dmesg to check that it had been recognised, the following appeared at the end of the command's output:

[5619410.606742] usb 2-1: new full-speed USB device number 2 using xhci_hcd
[5619410.759135] usb 2-1: New USB device found, idVendor=0451, idProduct=16a8, bcdDevice= 0.09
[5619410.759141] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[5619410.759144] usb 2-1: Product: TI CC2531 USB CDC
[5619410.759146] usb 2-1: Manufacturer: Texas Instruments
[5619410.759149] usb 2-1: SerialNumber: __0X00124B001949B547
[5619410.771010] cdc_acm 2-1:1.0: ttyACM0: USB ACM device
[5619410.771412] usbcore: registered new interface driver cdc_acm
[5619410.771414] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters

Although it could technically be accessed at /dev/ttyACM0 I prefer to use a more deterministic path (so that devices don't switch around after reboots etc). So, knowing that the Zigbee dongle is serial device, I ran

ls /dev/serial/by-id/

Which gave me the name of my device:

usb-Texas_Instruments_TI_CC2531_USB_CDC___0X00124B001949B547-if00

Because this name is deterministic, the same path will have been used on the Raspberry pi (so none of my HomeAssistant config needs to change).

So, I just needed to stop the docker container and re-run it with the device passed in using --device

# Stop and remove the old container
docker stop homeassistant
docker rm homeassistant

# Start new container with device passed through
docker run -d \
--name homeassistant \
-p 8123:8123 \
--restart=unless-stopped \
-e TZ=Europe/London \
-v /home/ben/docker_files/homeassistant/config:/config \
--device /dev/serial/by-id/usb-Texas_Instruments_TI_CC2531_USB_CDC___0X00124B001949B547-if00 \
ghcr.io/home-assistant/home-assistant:stable

This time, when HomeAssistant came up, it was able to communicate with my Zigbee devices.

ZHA can see 31 Zigbee devices

Toggling a nearby light-bulb led to it flashing as it should.


Tidying Up

Although my automations and switches worked, the container's logs were a little noisy at startup

2023-03-10 11:53:02.409 ERROR (MainThread) [homeassistant.components.hassio] Missing SUPERVISOR environment variable
2023-03-10 11:53:02.411 ERROR (MainThread) [homeassistant.setup] Setup failed for hassio: Integration failed to initialize.
2023-03-10 11:53:04.547 ERROR (MainThread) [homeassistant.setup] Unable to set up dependencies of raspberry_pi. Setup failed for dependencies: hassio
2023-03-10 11:53:04.548 ERROR (MainThread) [homeassistant.setup] Setup failed for raspberry_pi: (DependencyError(...), 'Could not setup dependencies: hassio')
2023-03-10 11:53:04.996 ERROR (MainThread) [homeassistant.components.binary_sensor] rpi_power: Error on device update!

Moving the configuration over from the Raspberry Pi based install had obviously brought some stuff with it (it seems that that is a thing).

So, I edited the config

nano /home/ben/docker_files/homeassistant/config/.storage/core.config_entries
# ctrl+w Raspberry Pi

This led me to an entry

      {
        "entry_id": "20fbda82484a8eda7ec1ffeef9b7eda8",
        "version": 1,
        "domain": "raspberry_pi",
        "title": "Raspberry Pi",
        "data": {},
        "options": {},
        "pref_disable_new_entities": false,
        "pref_disable_polling": false,
        "source": "system",
        "unique_id": null,
        "disabled_by": null
      },

I removed that (if it's the last entry in the array, remove the preceding comma too) and then restarted HomeAssistant

docker restart homeassistant

No more log noise.


Trusted Proxies

Quite some time ago, I added trusted_proxies to my HA config file so that HomeAssistant's logs and notifications would show downstream IP's rather than that of my reverse proxy.

Because HomeAssistant was now running within a containerised network, it would no longer see my reverse proxy's IP, instead showing the SNAT address of the host. So, I needed to add the docker network to the list of trusted_proxies in the HomeAssistant configuration file.

If you don't already know what the IP will be, you can find out by inspecting the container:

docker inspect homeassistant | grep Gateway

The gateway IP then needs to be added to config

nano ~/docker_files/homeassistant/config/configuration.yaml

I already had a http section, and only added the host bridge's IP, but for completeness the section is

http:
   use_x_forwarded_for: True
   trusted_proxies:
     - 127.0.0.1
     - 172.17.0.1
   ip_ban_enabled: True
   login_attempts_threshold: 3

Having done this, I updated the configuration on my reverse proxy so that it would use the container rather than the Pi as an origin - I was fully cut-over.


Conclusion

Moving from HomeAssistant OS to HomeAssistant Core was much quicker and easier than I expected, even if it is no longer possible to simply import a backup.

As well as giving more control over when updates land, running Container also means that I've removed a huge amount of complexity from our Home Automation: no more coredns and no need to rely on a hack to prevent bypassing of local DNS infrastructure.

HomeAssistant is currently running on shared hardware, but if I ever wanted to, moving it back to the Pi would be fairly straightforward: I'd just need to install Raspbian and docker before copying files over from their current location and starting the container exactly as I have above.