
EchoMuse, Part 1: Echo Dot without Alexa - the Infrastructure
EchoMuse, Part 1: Echo Dot without Alexa - the Infrastructure
The Starting Point
I have two Echo Dots from the 2nd generation lying around, manufactured in 2016, codename “biscuit”. The microphones, speakers, LED ring, and buttons work perfectly - I just don’t want Alexa and an Amazon account for them anymore. The goal: repurpose the Dots as voice satellites for Home Assistant.
That’s exactly what EchoMuse by wilbowes is for. The unlocking of the Dots is based on amonet-biscuit by R0rt1z2.
Here’s how the pieces fit together:
- Echo Dot with emOS: its own lightweight Linux instead of Amazon’s Android. The Dot provides microphones, speakers, LED ring, and buttons.
- EchoMuse Controller as a Docker container on my NUC: wake word detection with openWakeWord (“Hey Jarvis”), device management, and a dashboard.
- Home Assistant on a Raspberry Pi 4: sees each Dot as a normal ESPHome voice satellite and handles speech recognition, understanding, and voice output via the Assist pipeline.
- Traefik provides the dashboard via HTTPS, only in the LAN.
Important note upfront: nothing additional needs to be installed in Home Assistant. The built-in ESPHome integration is sufficient.
This article is split into two parts. This one covers everything that needs to be in place before touching the first Dot at all. Part 2 covers unlocking, flashing, and provisioning - with lessons learned from two attempts.
Implementation
Home Assistant: Checking the Assist Pipeline
The only real prerequisite on the HA side is a working Assist pipeline. I checked this in three steps.
View the pipeline (Settings → Voice assistants): My pipeline is called “Local Assistant”, language German, conversation agent Home Assistant. Speech recognition and speech output run via Home Assistant Cloud (Austrian German, voice Jonas). Despite the name, speech goes through Nabu Casa. For the initial setup, I deliberately left it this way because the cloud pipeline is a known quantity. What this means for cloud independence is covered at the end of Part 2.

Expose entities: Assist showed “0 entities exposed”. Without exposed entities, Assist understands but can’t control anything. I started by exposing just one or two test lights.

Test: first via text in the Assist dialog (“decke aus” turns off the light), then via voice through the microphone. The browser microphone only works over HTTPS or in the Companion app.

Learned along the way: short commands like “decke ein” are sometimes misinterpreted. Full sentences (“Schalte die Decke ein”) work more reliably.
EchoMuse Controller on the NUC
The controller runs as a ready-made image from the GitHub Container Registry. I placed it alongside my other stacks under ~/docker/echomuse, with the data/ folder in .gitignore.
curl -O https://raw.githubusercontent.com/wilbowes/EchoMuse/main/controller/docker-compose.deploy.yml
curl -o .env https://raw.githubusercontent.com/wilbowes/EchoMuse/main/controller/.env.example
sed -i 's/^SERVER_IP=.*/SERVER_IP=192.168.1.10/' .env
docker compose -f docker-compose.deploy.yml up -d
Three things are critical here:
network_mode: hostis mandatory. The controller announces itself via mDNS and opens its own ESPHome port for each Dot. Neither works behind Docker port mapping.- Set
SERVER_IPexplicitly. My NUC has many interfaces (Docker networks, WireGuard). Without a fixed IP, the controller might announce the wrong one. - Know the ports: 8768 for the dashboard, 8767 and 8770 for Dot connections (plain and TLS), starting at 16001 ESPHome per Dot, starting at 17001 the Bluetooth proxy.
On first startup, the controller writes a setup token once to the log:
docker logs echomuse-controller
On my first check, it was missing because the container was still running database migrations. Wait a few seconds, read the log again, and there it is.
One warning in the log is harmless: zeroconf reports an error on the WireGuard interface (Required key not available). mDNS on the LAN interface still works.
Dashboard via Traefik, LAN Only
Why Traefik at all? The provisioning wizard in the dashboard speaks to the Dot via WebUSB, and Chrome only allows WebUSB in a secure context - that is, HTTPS or localhost. That’s why the dashboard went behind Traefik before the first Dot was even plugged in.
Since the container runs in the host network, Docker labels don’t work the usual way here. Instead, I created an entry in the existing file provider (dynamic.yml):
http:
routers:
echomuse:
rule: "Host(`echomuse.home.example.com`)"
entryPoints: [websecure]
tls:
certResolver: letsencrypt
service: echomuse
middlewares: [lan-only]
services:
echomuse:
loadBalancer:
servers:
- url: "http://192.168.1.10:8768"
middlewares:
lan-only:
ipAllowList:
sourceRange: ["192.168.1.0/24", "10.0.0.0/24"]
- Only the dashboard runs through Traefik. Dots and Home Assistant connect directly to the controller.
lan-onlyis mandatory: The dashboard contains a root shell to every Dot. It shouldn’t be on the internet.- The certificate comes via Let’s Encrypt DNS challenge at Hostinger, DNS via my existing wildcard entry
*.home. - When editing a single mounted file, restart Traefik for safety:
docker restart traefik.
I tested from the laptop. Requests from the NUC itself arrive at Traefik with a Docker IP and are correctly rejected by lan-only - a 403 there is not an error, but proof that the middleware works.

Result
The Assist pipeline switches via text and voice, the controller runs in host network mode on the NUC, and the dashboard is accessible via HTTPS from the LAN only - nowhere else. All prerequisites for the provisioning wizard are met.
Pitfalls
| Pitfall | Symptom | Solution |
|---|---|---|
| No entities exposed | Assist understands but controls nothing | Expose entities in Assist, start small |
| Setup token missing from log | Log ends mid-migrations | wait a few seconds, read log again |
| Labels on host network container | Traefik can’t reach the service reliably | file provider with host IP |
| Test from NUC itself | 403 from Traefik | test from laptop, lan-only works correctly |
Conclusion
The real effort in this part comes down to three details: exposed entities in Home Assistant, network_mode: host with fixed SERVER_IP, and HTTPS for the dashboard because of WebUSB. Set these up cleanly beforehand, and you can focus entirely on the Dot during flashing.
Part 2 is all about the device: unlock with amonet-biscuit, FireOS 6 into both slots, the wizard on emOS, and integration into Home Assistant.