Skip to content

docker and stacks

the pi is a standalone docker host. it has its own environment in portainer, and its stacks deploy from git like the swarm's.

docker

Docker CE comes from Docker's own apt repository, the same setup get.docker.com creates:

/etc/apt/keyrings/docker.asc
/etc/apt/sources.list.d/docker.list:
  deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable

there is no /etc/docker/daemon.json, and containerd's config is the package default (disabled_plugins = ["cri"]). nothing is tuned.

portainer agent

the agent is started by hand, not from git, because it is what lets portainer deploy everything else here. the command is on add a host.

radios

the USB radios are passed to containers by their /dev/serial/by-id/ names, so they land on the right device whatever order they enumerate in.

radio by-id name starts used by
Zooz 800 Z-Wave stick usb-Zooz_800_Z-Wave_Stick_ zwave-js-ui
Nabu Casa SkyConnect usb-Nabu_Casa_SkyConnect_v1.0_ ser2net
Sonoff Zigbee 3.0 USB Dongle Plus usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_ zigbee2mqtt

containers

each stack polls its own deploy/pi-zwave01/<stack> branch, see stacks in git.

zwave-js-ui

zwave-js-ui runs the Z-Wave network on the Zooz 800 stick. Home Assistant's Z-Wave JS integration connects to it.

compose.yml, 23 lines

download compose.yml

services:
  zwave-js-ui:
    container_name: zwave-js-ui
    image: zwavejs/zwave-js-ui:11.22.3
    restart: always
    stop_signal: SIGINT
    stop_grace_period: 10s
    networks:
      - zwave
    devices:
      - "/dev/serial/by-id/usb-Zooz_800_Z-Wave_Stick_533D004242-if00:/dev/ttyUSB0"
    volumes:
      - /docker-data/zwavejs2mqtt/store:/usr/src/app/store
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    environment:
      - TZ=America/Los_Angeles
    ports:
      - "80:8091"
      - "3000:3000"

networks:
  zwave:
network its own bridge
ports 80 to the UI on 8091, 3000 for the Z-Wave JS websocket Home Assistant uses
data /docker-data/zwavejs2mqtt/store: settings, network keys, node cache
  • renovate never bumps it on its own. it has to stay compatible with Home Assistant's integration, and an upgrade migrates the node database one way. i approve each one when i can watch Home Assistant afterwards
  • the store directory keeps its old zwavejs2mqtt name. renaming it means changing the stack too

zigbee2mqtt

zigbee2mqtt runs the Zigbee network on the Sonoff dongle and publishes its devices to mosquitto on the swarm.

compose.yml, 14 lines

download compose.yml

services:
  zigbee2mqtt:
    container_name: zigbee2mqtt
    image: koenkk/zigbee2mqtt:2.14.1@sha256:fef0de769dcd04c27b3a6d277b61046eb96284bdd4198dcb1687c3a01b3020f3
    restart: unless-stopped
    volumes:
      - /docker-data/zigbee2mqtt/data:/app/data
      - /run/udev:/run/udev:ro
    ports:
      - 8080:8080
    environment:
      - TZ=America/Los_Angeles
    devices:
      - /dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_e0a81881b1e7ec1182f87a60e89bdf6f-if00-port0:/dev/ttyUSB1
network its own bridge
ports 8080 to the UI
data /docker-data/zigbee2mqtt/data: configuration.yaml, the device database and the coordinator backup
  • configuration.yaml names the broker, mqtt://mqtt.mydomain.com:1883, and the adapter, zstack. the network itself (channel, PAN ID and keys) lives on the dongle and in coordinator_backup.json
  • keep coordinator_backup.json. with it, a new container resumes the network. without it, and a config that doesn't match the dongle, zigbee2mqtt forms a new network and every device has to be paired again

ser2net

ser2net shares the SkyConnect over the network on TCP port 8000, so Home Assistant's OpenThread Border Router app can use the radio from its VM. thread radio has the config and why each line is there.

compose.yml, 19 lines

download compose.yml

version: '3.4'
services:
  ser2net:
    container_name: ser2net
    image: ghcr.io/jippi/docker-ser2net
    environment:
      - TZ=America/Los_Angeles
    restart: unless-stopped
    network_mode: host
    volumes:
      - /docker-data/ser2net/data/ser2net.yaml:/etc/ser2net/ser2net.yaml
    devices:
      - /dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_008ecfc2048dec119f3674e883c5466d-if00-port0:/dev/ttyUSB1
    healthcheck:
      test: ["CMD-SHELL", "grep -qE '^ *[0-9]+: [0-9A-F]+:1F40 [0-9A-F]+:[0-9A-F]{4} 01 ' /proc/net/tcp /proc/net/tcp6"]
      interval: 60s
      timeout: 5s
      retries: 3
      start_period: 120s
network host
ports 8000
data /docker-data/ser2net/data/ser2net.yaml

its healthcheck looks for an established connection on port 8000, so healthy means a client is attached. unhealthy usually means the app is down, but ser2net failing to start, read its config or open the radio looks the same. check the connection first, then its logs.

dozzle agent

the dozzle agent serves this host's container logs on port 7007 to the dozzle hub on the swarm. the certificate pair it authenticates with is in /docker-data/dozzle.

glances

glances serves host metrics for the dashboard on port 61208. it uses host networking to see the pi's real interfaces, and mounts an empty directory from the root filesystem read-only to report the pi's own disk.

/docker-data

every stack's state lives under one directory, so one directory is what gets backed up:

path holds
/docker-data/zwavejs2mqtt/store the Z-Wave network: settings, keys, node cache, logs
/docker-data/ser2net/data ser2net.yaml
/docker-data/dozzle the dozzle agent's certificate pair
/docker-data/zigbee2mqtt/data zigbee2mqtt config and database