Physical Smoker Device
The production smoker is a Raspberry Pi (armv7l, tailnet host smoker) running the
touchscreen kiosk, the device-service that reads the temperature probe, and Watchtower.
It is powered off most of the time, so it is not deployed to on a schedule. Watchtower on the device pulls new images whenever it is next online.
The operating rule
| What changed | How it reaches the device |
|---|---|
| Image content (app code) | Watchtower, on its next 300s poll after the device is online |
smoker.docker-compose.yml |
Dispatch Device Deploy — Watchtower recreates containers from the running container's config and never re-reads this file |
Watchtower only sees a new image when :latest moves, and :latest moves only when a
GitHub Release is published. Releases are published by merging the release PR that
release-please keeps open — see Release Process. Nightly builds
publish :nightly, which the device must never run (see the warning below). Merging a
feature PR to master alone never updates the smoker; it only refreshes the release PR.
What runs on the device
Compose lives at /opt/smoker-device/smoker.docker-compose.yml (project name
smoker-device, pinned in the file). Containers:
| Container | Image | Notes |
|---|---|---|
device_service |
benjr70/smart-smoker-device-service:latest |
Reads the Arduino on the GPIO UART /dev/ttyS0 |
frontend_smoker |
benjr70/smart-smoker-smoker:latest |
Touchscreen UI, port 8080 |
electron_shell |
benjr70/smart-smoker-electron-shell:latest |
X11 kiosk that displays the UI |
watchtower |
containrrr/watchtower:armhf-latest |
Scoped to the three above |
portainer_agent |
portainer/agent |
Standalone, not in compose, not managed by Watchtower |
Watchtower is deliberately scoped to the three application containers. Unscoped it
would also manage portainer_agent and itself, and an unattended self-update on armv7 is
the one failure that removes the device's ability to update anything else. Bump the
Watchtower image deliberately via a deploy.
Deploying a compose change
Dispatch the Device Deploy workflow on master:
| Input | Value |
|---|---|
device_host |
smoker |
version |
latest |
compose_file |
smoker.docker-compose.yml |
ssh_user |
smoker |
deploy_dir |
/opt/smoker-device |
environment |
prod |
cloud_backend_url |
https://smokecloud.tail74646.ts.net:8443 |
requires_arm_emulation |
false |
expected_containers |
device_service frontend_smoker electron_shell watchtower |
wait_seconds |
150 |
Three of those defaults are wrong for this device and must be overridden:
version: latest, nevernightly.publish.ymlbakes the cloud URL into the smoker bundle at build time —:nightlypoints at dev-cloud. A:nightlydevice looks completely healthy (green health check, live temperatures, working UI) while writing every production cook into the dev database.cloud_backend_urldefaults to dev-cloud. The check is advisory and always returns 0, so a wrong value produces a green run that proved nothing.requires_arm_emulation: false. The Pi is armv7;truewould runtonistiigi/binfmt --install armagainst live hardware.
wait_seconds must exceed the Electron kiosk's 60s start_period, hence 150.
Verifying a deploy
ssh smoker@smoker.tail74646.ts.net \
'sudo docker ps --format "{{.Names}}|{{.Image}}|{{.Status}}"'
Then, in order of how much they can hurt you:
- Serial.
sudo docker logs device_serviceshowsraw data {...}with plausible, moving values, and the service loggedRunning in production mode. If it saysemulator mode, every temperature on screen is fabricated. - Kiosk.
pgrep -x electronpassing proves nothing about what is on the screen — look at the display. No blank or error page, touch responds. - Cloud target. The served bundle must reference
smokecloud.tail74646.ts.netand neversmart-smoker-dev-cloud:curl -s http://smoker.tail74646.ts.net:8080/bundle.js \ | grep -o 'https://[a-z0-9.:-]*ts.net[^"]*' | sort -u - Watchtower armed.
sudo docker logs watchtowerreportsScanned=3,Failed=0. - Reboot test.
sudo reboot, then re-check 1 and 2. A kiosk that only survives until the next power cut is not fixed.
Rollback
- Automatic. A failed health check in
Device Deployrestores the previous compose from/opt/smoker-device/backups/and recreates the containers. - Re-pin to a known-good release. Re-dispatch with
version: v1.6.0. Because the images resolve through${VERSION:-latest}, this pins the device — and a scoped Watchtower on a pinned tag finds no update and leaves the rolled-back stack alone. ⚠️ A pinned device never auto-updates. Treat this as break-glass, and make "redeploylatest" the last step of any rollback. - Full revert. Older
benjr70/smart_smoker:*_V1.5.0images remain on Docker Hub. Rundocker compose downin/opt/smoker-devicefirst — the old stack used a different Compose project name, and two projects with the samecontainer_namevalues collide.
Gotchas worth knowing
- Device nodes are validated at container create time. Naming a node that does not
exist (the compose once mapped
/dev/ttyUSB0, which this Pi does not have) breaks every recreate — including Watchtower's, which would take the container down permanently. - The deploy user is not in the
dockergroup on this device; it has passwordlesssudoinstead.scripts/device-health-check.shfalls back tosudo docker inspectfor exactly this reason. - The device must never hold a git checkout. It previously ran its stack out of a
self-hosted runner's
_workdirectory, which froze the compose at whatever commit last deployed — that is how it ended up stranded on 2024 images for ~10 months.