How to Run Z-Wave JS UI via a Docker Image

I’m a relatively lightweight user of Home Assistant and Z-Wave devices, at least compared to some who have dozens, if not hundreds, of devices and automations. Regardless, I do own about half a dozen Z-Wave devices, and I need to control them somehow. I’ve chosen to control them via Home Assistant and Z-Wave JS UI.

Z-Wave JS UI logo

This is my thirteenth post documenting images I use at home. You can also read about how I run the Unifi controller, how I run Plex, how I update DuckDNS, how I run Duplicacy., how I run Heimdall, how I run Librespeed, how I run Home Assistant, how I run NetBox, how I run Scrutiny, how I run OpenVSCode Server, how I run QDirStat, and how I run WireGuard.

About Z-Wave JS UI

Z-Wave JS UI is a “full featured Z-Wave Control Panel and MQTT Gateway.” In my case, I run it alongside Home Assistant so I can manually or automatically control my Z-Wave devices. (In particular, I use a combination of controlling devices both via Home Assistant directly, as well as HomeKit via Home Assistant.)

It may be worth noting that this project was renamed Z-Wave JS UI the day before I started drafting this post. Apparently, the prior name was confusing and misleading to a lot of folks, myself included. So the first order of business was to update my docker-compose.yml file to swap out a few names. Anyway, I point this out in case you run across the old name (which I will not name here, in the interest of helping the old name die out sooner).

Running via docker-compose

I run all of my containers via docker-copmose. Here are the relevant sections of my docker-compose.yml file:


networks:
  home-assistant-zwave-js-ui:
    name: home-assistant-zwave-js-ui

services:
  home-assistant:
    container_name: home-assistant
    image: lscr.io/linuxserver/homeassistant:2022.9.6-ls97
    network_mode: host
    restart: unless-stopped

    depends_on:
      - home-assistant-zwave-js-ui
    env_file:
      - ./common.env
      - ./secret.env
    volumes:
      - ${SERVICE_DATA_DIR}/home-assistant:/config

  home-assistant-zwave-js-ui:
    container_name: home-assistant-zwave-js-ui
    image: zwavejs/zwave-js-ui:8.0.0
    restart: unless-stopped
    user: ${PUID}:${PGID}

    devices:
      - /dev/aeotec-zwave-stick:/dev/zwave
    env_file:
      - ./common.env
      - ./secret.env
    networks:
      - home-assistant-zwave-js-ui
    ports:
      - 3002:3000
      - 8091:8091
    volumes:
      - ${SERVICE_DATA_DIR}/home-assistant-zwave-js-ui:/usr/src/app/store

First, I create an explicitly-named network for Z-Wave JS UI to use. This helps ensure services are isolated and avoids the automatic names docker-compose generates. The names aren’t bad, but I like the explicit names better. Note that I don’t create an explicitly named network for Home Assistant as it uses host networking instead of a bridge.

Then, the first stanza of the services key specifies the basic container configuration: a name, image to use, and restart policy. It also includes a user configuration to set the container’s user as the non-root user I use for all my containers. This isn’t strictly necessary, but I like how it runs the container as a regular user and not root. It also helps keep file ownership sane.

Next, the devices section exposes my Aeotec Z-Wave stick into the container as /dev/zwave. This gets Z-Wave JS UI access to the physical hardware it needs to communicate on the Z-Wave network.

In my prior post about Home Assistant, the device was passed directly to the Home Assistant container. In the intervening time since I made that post, Z-Wave functionality was split out of Home Assistant proper; the current recommendation is to use a separate container like Z-Wave JS UI. Hence the device section moving from one container to another.

The env_file section brings in environment variables used commonly across my containers. This includes the SERVICE_DATA_DIR variable used further down the configuration.

Two external ports are mapped into the container: 3002 is mapped to 3000 and 8091 is mapped to 8091. Port 8091 is used to connect to the web application from a browser. There’s no difference in port numbers here, so it isn’t particularly interesting.

Port 3000, on the other hand, is more interesting. Because I have other containers running on my system, ports 3000 and 3001 are already in use. Thus I needed to present port 3000 as port 3002 on the host. This isn’t hard, but it’s mildly annoying, as Home Assistant then needs its Z-Wave JS configuration updated to also use port 3002. Thankfully that’s pretty easy to configure within Home Assistant.

Lastly, I have one volume mapping: /usr/src/app/store within the container is mapped to the directory on my host filesystem with all of my persistent data across all containers. I use an environment variable SERVICE_DATA_DIR to specify where that persistent configuration lives. The environment variable helps by cutting down on duplication in my broader docker-compose.yml file.

The container is now ready for you to boot it up via docker-compose up.

Final setup steps

Once you’ve booted the container, you can connect to its web interface in a browser on port 8091. From there, you can configure your particular Z-Wave network and devices.

You’ll also need to go into Z-Wave JS configuration within Home Assistant and update it to connect to port 3002 instead of 3000, or whatever the port number on your host is.

I can’t get more detailed in terms of configuration at this point, since every network is unique. Good luck!

Run Z-Wave JS User Interface via Docker Image

Though it was mildly obnoxious when Home Assistant deprecated the built-in Z-Wave support, I’m glad it was. Z-Wave JS UI has been a much friendlier and more effective interface to the Z-Wave network than the old support was. Thanks to the Z-Wave JS UI team for publishing such a handy tool.