Install Tesserae via Docker¶
The official Docker image is the fastest install if you'd rather not touch Python. It ships Tesserae plus a known-good Playwright Chromium so the webpage / dashboard renderer works out of the box.
The image is hosted on GitHub Container Registry as
ghcr.io/dmellok/tesserae.
Tags follow Tesserae versions (e.g. :0.14.0, :0.14), plus a
:latest pointing at the most recent release tag.
Pin the tag in setups you care about
:latest is convenient for kicking the tyres but moves whenever
a new release is published. Pin to a specific version (e.g.
:0.14.0) so docker compose pull && up -d is the deliberate
upgrade step.
Quick start¶
mkdir ~/tesserae && cd ~/tesserae
curl -fsSLO https://raw.githubusercontent.com/dmellok/tesserae/main/docker-compose.yml
docker compose up -d
That's it. Open http://<host-ip>:8765 (or
http://tesserae.local:8765 once mDNS comes up), the first request
walks through password setup and the onboarding wizard.
The default docker-compose.yml uses host networking, which is
the right choice for a self-hosted Pi / mini-PC / NAS appliance:
- The render-frame URL Tesserae embeds in every MQTT push points at the host's real LAN IP, so your panels can actually fetch frames.
- The built-in MQTT broker is reachable on the host's port 1883
without you publishing it from a
ports:block. - mDNS works, so
tesserae.localresolves on the LAN.
Linux only, though. Docker Desktop on Mac / Windows handles host networking differently, see Bridge networking below if you're testing there.
What's in the image¶
- Tesserae, installed from the repo at the tag.
- Playwright Chromium, preinstalled from the
official Playwright Python base image
so the webpage renderer works without
playwright install. - Waitress as the WSGI server. Production-tuned by default; no nginx required for a single-user install.
- Non-root user (
tesserae, uid 1001) for defence in depth. This is not a widget sandbox, widgets still execute in the same Python process and can read anything this user can read (see issue #3).
Configuring the MQTT broker¶
Tesserae publishes frames over MQTT. Two ways to configure it:
Use the built-in broker¶
After first-run, go to Settings → Server → MQTT and toggle the built-in broker on. Then expose port 1883 from the container so your panels can reach it:
services:
tesserae:
image: ghcr.io/dmellok/tesserae:latest
ports:
- "8765:8765"
- "1883:1883" # built-in broker
volumes:
- ./data:/app/data
The built-in broker is amqtt, which speaks MQTT v3.1.1 only. Tesserae's own Pi / ESP32 clients are fine; if you connect with MQTT Explorer / MQTTX / Home Assistant / Node-RED you'll need to set their protocol version to 3.1.1, v5 clients get rejected.
Point at Mosquitto (or HA's broker)¶
If you already run Mosquitto, point Tesserae at it via Settings → Server → MQTT (host, port, username, password). The compose file doesn't need any extra ports for this path.
For a "full MQTT v5 with a Mosquitto sidecar" setup, add the sidecar yourself:
services:
tesserae:
image: ghcr.io/dmellok/tesserae:latest
ports: ["8765:8765"]
volumes: ["./data:/app/data"]
depends_on: [mosquitto]
mosquitto:
image: eclipse-mosquitto:2
restart: unless-stopped
ports: ["1883:1883"]
volumes:
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf
Then set the broker host to mosquitto in Tesserae's settings (the
service name is the resolvable hostname inside the compose network).
Bridge networking¶
If you can't use host networking, typically Docker Desktop on Mac / Windows, or a setup with port conflicts on the host, switch to bridge networking. Two things break under bridge that host networking got for free, and both need fixing before your panels can talk to Tesserae:
- The render-frame URL in every MQTT push and the MQTT broker
URL the wizard shows your panels both point at
detect_local_ip(). Under bridge networking that resolves to the container's internal172.x.x.xaddress, which LAN clients can't reach. The fix: setTESSERAE_HOST_IPto your host's real LAN IP. - mDNS multicast doesn't escape the bridge network, so
tesserae.localwon't resolve on the LAN. That one you can't easily fix on bridge, use the host's IP directly, or run a separate mDNS reflector (out of scope here).
The compose snippet:
services:
tesserae:
image: ghcr.io/dmellok/tesserae:latest
container_name: tesserae
restart: unless-stopped
ports:
- "8765:8765"
- "1883:1883" # only if using the built-in MQTT broker
volumes:
- ./data:/app/data
environment:
TESSERAE_IN_DOCKER: "1"
TESSERAE_HOST_IP: "192.168.1.50" # your host's actual LAN IP
Find your host's LAN IP with hostname -I (first address printed) or
ip addr show eth0. Without TESSERAE_HOST_IP set, Tesserae logs a
loud warning on startup and the admin UI flags the bad URL, but it
won't auto-fix itself, because the host's IP isn't introspectable
from inside a bridge-networked container.
Upgrading¶
Your ./data volume carries settings, pages, schedules, history, and
the render cache across the upgrade.
The in-app Settings → System → Updates card is hidden under
Docker, a git pull inside a layered filesystem would lose changes
on the next image rebuild, so upgrades go through docker compose
pull instead. The Settings page surfaces a docker compose hint
instead of the update form.
Backups¶
The Settings → System → Backups card still works, backups land
in ./data/core/backups/ on your host. Snapshotting ./data with
your normal backup tool (restic, borg, rsnapshot, or a plain cron'd
tarball) covers everything Tesserae has, including those backups.
Limits¶
- No self-update. Use
docker compose pull. The in-app update flow is gated off whenTESSERAE_IN_DOCKER=1(the image sets that for you). - mDNS needs host networking. See above.
- arm/v7 is not built. Pi 3 and below would need a different Playwright story; not currently in scope.
- The image is ~970 MB to pull, ~2.5 GB on disk uncompressed. Most of that is Chromium and its sandboxes. There's no smaller Tesserae image plan, the renderer fundamentally needs a real browser.