Home Assistant - How I ditched the VM and embraced The Container

A while ago, I’ve set up Traefik to be able to publicly expose some of the services on my TrueNAS, and for the Crowdsec middleware to work with Home Assistant, I needed to feed it HA logs to detect bruteforce attempts. Problem? My Home Assistant was in a virtual machine, thus no direct access to the filesystem. I really did not want to set up a convoluted method of ssh-ing into it and tailing the logs to the host, so I switched to the container installation. I’ve been running it for a month or so and am quite happy with how the migration turned out.
Per @kris ’ suggestion on Youtube, here is a write up of how I did it.

Btw, since this was discussed on the T3 podcast - Home Assistant Supervised does not support LXC/Incus containers nor will it install inside one, it will just error out.

Prerequisites:

  1. Home Assistant Container
    I really did not want to run the container that will face the internet as privilleged and root with mounts to to system’s internals like the official guide wants you to (Linux - Home Assistant), nor did I really like the app from Truenas catalog as it’s also running as root, so I opted for using the image from linuxserver.io.
    I’m also used to editing Home Assistant config through a web editor, so I set it up together with a code server container, also from linuxserver.io.
    Here is the compose config that I used in Dockge:
services:
  homeassistant:
    image: linuxserver/homeassistant:latest
    container_name: homeassistant
    security_opt:
      - no-new-privileges:true
    volumes:
      - type: bind
        source: ${apps_storage}/homeassistant/config
        target: /config
      - type: bind
        source: ${apps_storage}/homeassistant/ssh
        target: /config/ssh
      - type: bind
        source: ${apps_storage}/homeassistant/secrets/secrets.yaml
        target: /config/secrets.yaml
      - type: bind
        source: ${apps_storage}/homeassistant/backups
        target: /config/backups
    network_mode: host
    environment:
      - PUID=${USER_ID}
      - PGID=${USER_ID}
      - TZ=${timezone}
    restart: unless-stopped
  code-server:
    image: linuxserver/code-server:latest
    container_name: code-server
    security_opt:
      - no-new-privileges:true
    environment:
      - PUID=${USER_ID}
      - PGID=${USER_ID}
      - HASHED_PASSWORD=${HASHED_PASSWORD}
      - TZ=${timezone}
      - DEFAULT_WORKSPACE=/homeassistant
    volumes:
      - type: bind
        source: ${apps_storage}/homeassistant/code-server
        target: /config
      - type: bind
        source: ${apps_storage}/homeassistant/config
        target: /homeassistant
      - type: bind
        source: ${apps_storage}/homeassistant/secrets/secrets.yaml
        target: /homeassistant/secrets.yaml
      - type: bind
        source: ${apps_storage}/homeassistant/backups
        target: /homeassistant/backups
    ports:
      - 8124:8443
    restart: unless-stopped
networks: {}

Pay attention to the ${variables}, you will want to specify them in .env in Dockge.

Note that I chose to use the bind mounts because I wanted each mount to be its own ZFS dataset, as volume mounts will happily create the source directory if it doesn’t already exist (in case you mess something up), which I didn’t really want.
Secrets file is in a separate secrets dataset because I had to mount whole config dataset for Crowdsec for it to read Home Assistant’s logs, and I didn’t want it to have access to the secrets file too.
Backup dataset is separate because that’s where my HAOS VM used to save its backups over SMB, so I decided to keep that dataset but just as a bind mount.
Also, if you want working mDNS discovery, in TrueNAS Web UI, you will need to go to Network - Global Configuration and uncheck the mDNS Service Announcement:

  1. Zigbee

For zigbee, I’m using zigbee2mqtt and mosquitto, which are both available in the TrueNAS Apps Catalog, as I didn’t want to give HA container privilleged access to USB devices.
I’ve been using zigbee2mqtt before I switched from the VM due to its better compatibility with zigbee devices, but if you are coming from ZHA (Home Assistant’s builtin Zigbee integration), last time I checked (which was a while ago), you basically need to start from scratch and manually re-add all your zigbee devices to a fresh zigbe2mqtt-created network.
Devices get exposed to Home Assistant via the “MQTT” integration.
I won’t go into detail on how to set those 2 containers up other than to suggest to read the tooltip for the port field in the zigbee2mqtt configuration, but if you have a question, leave a comment.

  1. Bluetooth

As there is, to me knowledge, no equivalent to zigbee2mqtt for bluetooth, I decided to instead opt for an ESP32 bluetooth proxy.
Setting those up is easy - buy a generic ESP-32 board, which cost a few dollars, plug it into your computer, flash it with Bluetooth Proxy firmware by visiting this page in a Chromium-based browser Ready-Made Projects — ESPHome, give it credentials to your 2.4GHz Wi-Fi network, add it to your Home Assistant via the ESPHome integration, and you are good to go.

  1. ESPHome Device Builder

Only needed if you have devices that require custom compiled ESPHome firmware. You don’t need this for bluetooth proxy or Voice PE, as they get their firmware updates from Home Assistant servers.
ESPHome is available as a docker container, which I’ve installed through Dockge, although it is available in the App Catalog. But again, I want to run it as non-root, so I’m going the compose route.

services:
  esphome:
    container_name: esphome
    image: ghcr.io/esphome/esphome
    security_opt:
      - no-new-privileges:true
    user: ${USER_ID}:${USER_ID}
    ports:
      - 6052:6052
    volumes:
      - type: bind
        source: ${apps_storage}/esphome/config
        target: /config
      - type: bind
        source: ${apps_storage}/esphome/cache
        target: /.cache
    restart: unless-stopped
    environment:
      - TZ=${timezone}
      - ESPHOME_DASHBOARD_USE_PING=true
      - USERNAME=admin
      - PASSWORD=${password}
      - PLATFORMIO_CORE_DIR=/config/.esphome/.platformio
      - PLATFORMIO_GLOBALLIB_DIR=/config/.esphome/piolibs
networks: {}

I couldn’t get mDNS discovery working with this one, so I had to assign static DHCP leases to my ESPHome devices in the router, as well add this to configuration for ESPHome devices:

  use_address: DEVICE_IP_HERE

which goes under the wi-fi section.
It will look something like this:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  use_address: 192.168.0.123

You also won’t get notifications in Home Assistant itself every time new ESPHome version comes out, but it’s not a big deal to me as it’s rare I need to update the firmware version at all.

  1. Wyoming Piper for Text to Speech

If you use Home Assistant’s voice assistant satellites, such as Voice PE, then Wyoming Piper for local TTS is available as a docker container: https://hub.docker.com/r/rhasspy/wyoming-piper

Again, it’s nothing hard to set up, you can just convert the example command from the docker hub page to compose on the home page of Dockge, although you may want to change the default voice option to something nicer, like en_GB-alba-medium.

  1. Whisper.cpp for local Speech to Text with GPU acceleration

I have an Intel Arc A380 in my TrueNAS box mainly for Jellyfin transcoding, but since that uses only Media Engine part of the chip, I’ve set it up to do some compute by running local speech to text.
Speed is quite good, accuracy too, especially in noisy environments, takes only a second to process a simple request with the large-v2 model, less than a second with large-v3-turbo.
Compose config and stuff is available here: GitHub - tannisroot/wyoming-whisper-cpp-intel-gpu-docker: Run an Intel GPU-accelerated Wyoming protocol speech-to-text service for your Home Asssistant in Docker

I wrote this somewhat in a haste, so if I missed something or you have any questions, feel free to leave a comment.

Update: I was able to bypass the need for running home assistant in host networking mode by using the macvlan network driver.
This allowed me to re-enable mDNS discovery in TrueNAS and still have auto-discovery in Home Assistant.
I removed:

network_mode: host

and added

services:
  homeassistant:
...
    networks:
      homeassistant:
        ipv4_address: 192.168.0.242
...
networks:
  homeassistant:
    driver: macvlan
    driver_opts:
      parent: br0
    ipam:
      config:
        - subnet: 192.168.0.0/24
          ip_range: 192.168.0.240/29
          gateway: 192.168.0.1
          aux_addresses:
            host: 192.168.0.241

(to use br0 you need to have a bridge interface configured on TrueNAS)
To be able to access services on the host, create and configure a macvlan by running these commands on the host:

ip link add ha-host-shim link br0 type macvlan mode bridge
ip addr add 192.168.0.241/32 dev ha-host-shim
ip link set ha-host-shim up
ip route add 192.168.0.240/29 dev ha-host-shim

Change the subnet to the one used in your local network of course.

1 Like

Thanks for the info.

I just installed HomeAssistant for the first time ever today as an appliance VM under Incus. I like how easily it found smartstuff around the house. It didn’t find my Tapo smart plugs initially, but I was pleased to see TP-Link seems to support HA very well.

Once I saw 3 different devices for my TV and 2 for the Shield, I just scratched my head and closed the page. I’ll mess with it more another day :slight_smile:

Time machine uses mDNS wont turning it off make timemachine not work?

Also durring onboarding when Im trying to follow what you did I get “Failed to save: Unknown command.”

If you meant to reply to me, I can’t say whether turning off mDNS discovery for TrueNAS itself will break timemachine. However on Linux and Windows machines I am able to just manually input local NAS IP.
As for the error you mentioned, I can’t help without logs. My guess is it’s probably permissions.

Thank you for sharing your install logic!

I have a question, why did you choose for using the image from linuxserver.io and not the one from the Home Assistant project?

I don’t understand the differences between the two. Linuxserver git page and doc is not explicit on this point (or I’am reading at the wrong place).

Unlike the Home Assistant docker image, you can run the Linux Server image as a non-root user with PUID/PGID envars, it’s what linuxserver.io images are known for.
My Home Asssistant instance is publicly exposed and it’s unacceptable for me to run something that is internet-facing as a privilleged root container like HA docs suggest.

1 Like

Thx.

That’s probably why my attempts to run official docker image with PUID/PGID were a huge fail :sweat:

I think you were right. I got the Homeassistant docker to load with no errors after I recreated the datasets. now to try to follow the rest of it thanks.

I need to run Zwave JS for my Zooz 700 controller Here is the docker compose example for anyone interested.
https://zwave-js.github.io/zwave-js-ui/#/getting-started/docker?id=run-as-a-service

Update: I was able to bypass the need for running home assistant in host networking mode by using the macvlan network driver.
This allowed me to re-enable mDNS discovery in TrueNAS and still have auto-discovery in Home Assistant.
I removed:

network_mode: host

and added

services:
  homeassistant:
...
    networks:
      homeassistant:
        ipv4_address: 192.168.0.242
...
networks:
  homeassistant:
    driver: macvlan
    driver_opts:
      parent: br0
    ipam:
      config:
        - subnet: 192.168.0.0/24
          ip_range: 192.168.0.240/29
          gateway: 192.168.0.1
          aux_addresses:
            host: 192.168.0.241

(to use br0 you need to have a bridge interface configured on TrueNAS)
To be able to access services on the host, create and configure a macvlan by running these commands on the host:

ip link add ha-host-shim link br0 type macvlan mode bridge
ip addr add 192.168.0.241/32 dev ha-host-shim
ip link set ha-host-shim up
ip route add 192.168.0.240/29 dev ha-host-shim

Change the subnet to the one used in your local network of course.
And don’t forget to make the last series of command persistent by adding it to Postinit scripts.

1 Like