Skip to content

Installation

Funkloch is one container image for amd64 and arm64: tristanteu/funkloch on Docker Hub. It keeps everything in one SQLite file in /data and listens on port 8090.

Podman Quadlet

~/.config/containers/systemd/funkloch.container (rootless) or /etc/containers/systemd/funkloch.container:

[Unit]
Description=Funkloch

[Container]
Image=docker.io/tristanteu/funkloch:latest
ContainerName=funkloch
Network=host
Volume=funkloch-data:/data
Environment=ZIGBEE2MQTT_MQTT_URL=mqtt://localhost:1883
Environment=ZIGBEE2MQTT_MQTT_USER=funkloch
Environment=ZIGBEE2MQTT_MQTT_PASSWORD=secret
Environment=OTBR_URL=http://localhost:8081
Environment=MATTER_SERVER_URL=ws://localhost:5580/ws
Environment=TZ=Europe/Berlin
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user start funkloch

podman auto-update (or its timer, systemctl --user enable --now podman-auto-update.timer) pulls new releases.

Docker Compose

services:
  funkloch:
    image: tristanteu/funkloch:latest
    container_name: funkloch
    network_mode: host
    volumes:
      - funkloch-data:/data
    environment:
      ZIGBEE2MQTT_MQTT_URL: mqtt://localhost:1883
      ZIGBEE2MQTT_MQTT_USER: funkloch
      ZIGBEE2MQTT_MQTT_PASSWORD: secret
      OTBR_URL: http://localhost:8081
      MATTER_SERVER_URL: ws://localhost:5580/ws
      TZ: Europe/Berlin
    restart: unless-stopped

volumes:
  funkloch-data:

With network_mode: host, localhost is the server itself. Without host networking, publish the port (ports: ["8090:8090"]) and use addresses the container can reach, e.g. the broker's container name in the same Compose network.

Bind mount instead of a volume

The container runs as user bun (uid 1000). A host directory needs to belong to it: chown 1000:1000 ./data, or with Podman Volume=./data:/data:U (add Z under SELinux: :U,Z). If it cannot write there, Funkloch stops at start with Funkloch (uid 1000) cannot write to /data (owned by …) and names the fix.

First start

Open http://<host>:8090. The first visit asks for the admin password (at least 10 characters); until then anyone who reaches the port could set it, so do this right after the start. Funkloch then signs you in for 30 days per browser. A page that stays open after its session ended (a logout elsewhere, a password change) goes to the login with its next click.

Forgot the password? Start once with FUNKLOCH_RESET_PASSWORD=1: it removes the password and every session, and the next visit sets a new one. Remove the variable again afterwards.

Environment variables

Each network is on when its variable is set; everything else has a default.

Variable Meaning Default
ZIGBEE2MQTT_MQTT_URL MQTT broker of Zigbee2MQTT, e.g. mqtt://localhost:1883 or mqtts://…; empty: Zigbee off
ZIGBEE2MQTT_MQTT_USER, ZIGBEE2MQTT_MQTT_PASSWORD broker login
ZIGBEE2MQTT_BASE_TOPIC Zigbee2MQTT base topic zigbee2mqtt
ZIGBEE2MQTT_NETWORKMAP_HOURS hours between two network maps until changed in the settings, at least 1 6
OTBR_URL REST API of the OpenThread Border Router, e.g. http://localhost:8081; empty: Thread off
MATTER_SERVER_URL WebSocket of matterjs-server, e.g. ws://localhost:5580/ws; empty: Matter off. See Matter
MATTER_POLL_MINUTES minutes between two reads of the neighbor tables and Wi-Fi signals, 5 to 1440 15
PORT port of the web UI 8090
DATA_DIR folder of the SQLite database /data
TZ time zone for day boundaries, the heatmap, the 3 a.m. jobs and every date and time on the pages, whatever the browser's zone UTC
LOG_LEVEL trace to fatal info
FUNKLOCH_RESET_PASSWORD 1: forget the admin password at start
FUNKLOCH_DEMO 1: a demo network instead of your own, no login
FUNKLOCH_DEMO_NOW with the demo: an ISO time it stands still at, without live readings (for the screenshots)

Thresholds, the network map interval, retention, notification rules and channels are set in the web UI (Settings and Events) and kept in the database.

Behind a reverse proxy

Funkloch works on a sub-domain (not under a sub-path). The proxy needs to pass the Host header (or X-Forwarded-Host) and X-Forwarded-Proto; with https the session cookie is marked Secure. Form posts from another origin are refused, so the browser must reach Funkloch under the address the proxy forwards.

After five wrong passwords within 15 minutes an address waits until the window has passed. The address is the last entry of X-Forwarded-For, the one the proxy appends; a client that reaches the port directly can set that header itself, so do not open the port itself to the internet.

The live updates are a server-sent event stream at /api/live; Funkloch already sends X-Accel-Buffering: no for nginx.

funkloch.example.com {
    reverse_proxy localhost:8090
}
server {
    server_name funkloch.example.com;
    location / {
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 1h;
    }
}

Set the public address under Events → Notifications as well, so notifications link to the device.

Health check

GET /healthz answers 200 with {"ok":true,…} when the database answers and every network is connected, otherwise 503 with the adapter that is not. The image runs it every 30 seconds.

Backup and data

The database is /data/funkloch.db (with -wal and -shm beside it while it runs). Stop the container and copy the three files, or back up the volume. Raw readings are kept 14 days, hourly values a year (both in Settings); for 100 devices the file stays at a few hundred MB.

Update

Pull the new image and restart: podman auto-update or docker compose pull && docker compose up -d. The database is migrated at start; a backup before a major release does no harm.