Running the daemon in a container¶
This page is about running the daemon and its admin page in Docker, on a machine that is not your desk (a NAS, a server, a small box under the TV). The daemon reads the providers' usage data and serves it to the Presto display and to the admin page.
Nothing here changes the daemon's rules: the app only observes provider accounts, it never spends or resets anything, and it never reads a credential it was not given.
What is in the box¶
| File | What it is |
|---|---|
Dockerfile |
The image: the daemon, its admin page and one dependency. |
compose.yaml |
The daemon, its ports, its data volume and its settings. |
.dockerignore |
The list of files that may not go into the image. |
.env.example |
A template for the tokens. Copy it to .env. |
The image is built on python:3.13-alpine3.24, pinned to one digest, and it
runs as an unprivileged user (dash, uid 10001). It is about 95 MB.
Quick start¶
First get the two files the steps below use, compose.yaml and
.env.example, into an empty folder on the machine that will run Docker. Each
release carries them, so no checkout is needed:
mkdir presto-ai-dash && cd presto-ai-dash
curl -fLO https://github.com/jake1164/presto-ai-dash/releases/latest/download/compose.yaml
curl -fL -o .env.example https://github.com/jake1164/presto-ai-dash/releases/latest/download/default.env.example
The -f makes curl stop with an error instead of saving a "Not Found" page as
your file. (GitHub drops the leading dot of an attached file's name, so the
template is published as default.env.example. The command saves it as
.env.example.)
# 1. Two tokens: one for the admin page, one for the Presto device. Both are
# required, and they must be different. Generate each (run it twice):
openssl rand -hex 24
# or: python -c "import secrets; print(secrets.token_urlsafe(24))"
# 2. Put them in .env next to compose.yaml.
cp .env.example .env
# then edit .env and paste one token after DASH_ADMIN_TOKEN= and the other
# after DASH_SHARED_TOKEN=. Neither may be left empty. The same file sets
# the addresses and ports the two services are published on (see below).
# 3. Pull the published image and start.
docker compose up -d
# 4. Open the admin page on this machine and unlock it.
# http://127.0.0.1:8788
The admin token you generated in step 1 is the token for the first login. It is the permanent one: it does not change when the container restarts. The daemon never prints it.
You cannot leave DASH_ADMIN_TOKEN empty in a container. A local install
(not Docker) generates a temporary token at every start when you give it none,
and prints it in its log, because its admin page is only reachable from the
machine itself. In a container the admin page has to listen on all
interfaces, because that is how the published port reaches it, and a
non-loopback address needs a token you chose. A generated one would also sit in
Docker's log after the container is gone. So Compose refuses to start with an
empty token:
required variable DASH_ADMIN_TOKEN is missing a value: DASH_ADMIN_TOKEN is empty
in .env: put a token you generate there (openssl rand -hex 24)
If a token reaches the daemon some other way and is still missing, the daemon stops with:
daemon startup failed: the admin page would listen on 0.0.0.0, which is not a
loopback address; set an explicit admin token (admin.token in dash.toml or
DASH_ADMIN_TOKEN) before listening on a non-loopback address
A second message, of the same kind, appears when the list of names the page
answers to (DASH_ADMIN_ALLOWED_HOSTS) is empty.
The device token is separate. The Presto display holds it, and the compose
file refuses to start without DASH_SHARED_TOKEN in .env. That is on purpose:
without one the daemon would print a temporary token in its log, and Docker
keeps that log after the container is gone. Put the same value in the Presto's
settings (presto/config.py).
The ports¶
| Port | Published as | Who uses it |
|---|---|---|
| 8787 | 0.0.0.0:8787 (every address) |
The Presto display polls this. |
| 8788 | 127.0.0.1:8788 (this machine only) |
The admin page. |
Both are set in .env (the template lists all four with their defaults), so you
never edit compose.yaml:
| Variable | Default | What it sets |
|---|---|---|
DASH_PUBLISH_ADDRESS |
0.0.0.0 |
The host address the device port listens on |
DASH_DEVICE_PORT |
8787 |
The host port for the device |
DASH_ADMIN_PUBLISH_ADDRESS |
127.0.0.1 |
The host address the admin page listens on |
DASH_ADMIN_PUBLISH_PORT |
8788 |
The host port for the admin page |
Only the host side changes: inside the container the daemon keeps 8787 and 8788.
After editing .env, run docker compose up -d again.
The admin page is published on the loopback address of the host on purpose. To reach it from another machine without changing that, use an SSH port forward:
and open http://127.0.0.1:8788 on your own machine. The browser then uses
127.0.0.1:8788, which the admin page always accepts.
To reach the admin page from another machine on the LAN, put two lines in .env
(the second keeps the page's own names in the list):
DASH_ADMIN_PUBLISH_ADDRESS=0.0.0.0
DASH_ADMIN_ALLOWED_HOSTS=localhost:8788,127.0.0.1:8788,toontown-int-srv1:8788
Use the name (or address) and port the browser will type. Read the security notes below before you do that.
The health check¶
The image's health check asks one question:
That is the whole answer. The route needs no token and returns nothing else: no version, no source names, no counts, no timings. Every other route still needs its token.
Data and backups¶
Everything that has to survive an upgrade lives in one named volume,
dash-data, mounted at /data:
| File | What it is |
|---|---|
/data/dash.db |
The SQLite database: the accounts, the history, the encrypted provider keys, the browser sessions. |
/data/dash.db.key |
The encryption master key, in an owner-only file. |
Both files belong to the container's dash user and are readable by that user
only. To read or back them up from the host, use docker cp or a container
running as the same user; a bind mount owned by another host user will not
let the daemon create them.
Losing the key file loses the stored provider keys. The database still opens and every other setting is still there, but the encrypted keys cannot be read any more, and they are never restored by guessing: the daemon refuses to start with a wrong or missing key instead of overwriting anything. Back up the database and the key file, together:
docker run --rm -v presto-ai-dash_dash-data:/data -v "$PWD":/backup \
presto-ai-dash:local tar czf /backup/dash-backup.tar.gz -C /data .
To move to another machine, copy both files and set DASH_ENCRYPTION_KEY
instead (see below).
The encryption master key can also live in the environment, which is the portable choice: the key file of a Windows install is protected with DPAPI and cannot be used in a Linux container.
Put it in .env as DASH_ENCRYPTION_KEY=.... Keep it with the database
backup: the same rule applies, the key and the data belong together.
Upgrades¶
The image is ghcr.io/jake1164/presto-ai-dash. Its latest tag moves to every
release, so the two commands above are the whole upgrade. Each release also keeps an exact tag, for
example 0.50.0. To stay on one version, set DASH_IMAGE in .env, for example
DASH_IMAGE=ghcr.io/jake1164/presto-ai-dash:0.50.0.
The daemon tells you when a newer release is out (the admin page's Status tab, and a blue
arrow on the Presto). It asks only the GitHub Container Registry, compares against the exact
release your running image carries, and never pulls or restarts anything. If you pinned
DASH_IMAGE, change it to the version the page names first; a pull on its own only downloads the
image, and the container keeps running the old one until docker compose up -d dash recreates it.
A release candidate image is never what latest points to.
To build the image from a checkout instead of pulling it:
The data volume is not touched by an upgrade: the database and the key file stay where they are. The daemon migrates its own schema when it starts.
Take a backup first if the version jumps a long way. The daemon refuses to open a database that is newer than it is, and it never downgrades one.
The time zone¶
A container starts in UTC. The quiet hours and the weekly reset estimate
use the process's local time, so set TZ in your .env to your own zone,
for example America/New_York or Europe/London. Left alone it is UTC,
and quiet hours such as 22:00 to 07:00 will then be in UTC:
The admin page's Settings tab shows the zone in the effective configuration, next to the other read-only values, so you can see what the process is using.
A reverse proxy in front¶
The daemon speaks plain HTTP for everything but one thing: the Presto downloads app updates over TLS, on the same device port, with a certificate the daemon makes for itself (see below). A reverse proxy can put TLS in front of the admin page:
- point the proxy at the container's port 8787 (the device port) or 8788 (the admin page),
- terminate TLS at the proxy,
- and add the name the browser will use to the admin page's allowed hosts, because the admin page only answers to the names it knows:
With that name in the list, the admin page answers to
dash.example.com over HTTP and over HTTPS, and to nothing else. A
request with another Host header is refused, exactly as before.
The device port needs no such list: the Presto sends the shared token and
the daemon does not look at the Host header there.
Keep the Presto on the daemon's own network, not behind the proxy. The Presto is supported talking to the device port directly. Behind a proxy:
- App updates over the air stop working when the proxy terminates TLS or speaks HTTP on the device port. The Presto trusts only the certificate authority its daemon gave it at setup, never the proxy's certificate.
- The Presto's status polls are plain HTTP, so reaching the daemon through a proxy across the internet would send the device token and your quota readings unencrypted.
Passing the device port through as raw TCP might work, but it is not tested or supported.
The update certificate¶
On first start the daemon makes a private certificate authority and keeps it
in dash.db.ota.pem, beside the database (in the container, in the /data
volume). It is private to the account that runs the daemon, like dash.db.key.
Each start signs a fresh server certificate from it. The setup utility gives
each Presto the authority's public certificate. Deleting the file makes a new
authority, and every Presto then needs one run of Set up a Presto before it
can update over the air again.
Security notes¶
Securing a deployment covers these in full, including publishing the status port on one LAN address only.
- The admin page is on the loopback address of the host by default.
127.0.0.1:8788is reachable from the machine and from nowhere else. - The shared token is plain HTTP on the LAN. The Presto device and the device port send it unencrypted. Anyone who can watch the LAN can read it, and with it the status payload. Keep the LAN trusted, or put the device port behind a proxy with TLS, or use a private network such as Tailscale.
- The admin token is separate from the device token and the daemon refuses to use the same value for both.
- Limits are per address. The daemon limits how many connections, and how many alert writes, one client address may use. Where every client appears to come from one address (Docker Desktop on Windows or macOS, or a proxy in front), they share one allowance.
- The container never sees your Claude credentials. Do not mount
~/.claudeor~/.claude.jsoninto it. Those files hold credentials, and the daemon has no reason to read them in a container. The image's build context excludes them, and the test suite fails if it ever stops doing so. - The container runs as an unprivileged user and owns only
/data. The compose file also drops every Linux capability, forbids gaining new privileges, and mounts the root filesystem read-only (with a scratch/tmp). - Tokens can come from files instead of the environment. Anything in
environment:shows indocker inspect. For the two tokens and the master key, setDASH_ADMIN_TOKEN_FILE,DASH_SHARED_TOKEN_FILEorDASH_ENCRYPTION_KEY_FILEto a path, and mount the file there (Composesecrets:appear under/run/secrets/). Setting both a variable and its_FILEform is an error, so incompose.yamlreplace theDASH_ADMIN_TOKENandDASH_SHARED_TOKENlines rather than adding to them:
environment:
DASH_ADMIN_TOKEN_FILE: /run/secrets/admin_token
DASH_SHARED_TOKEN_FILE: /run/secrets/shared_token
secrets:
- admin_token
- shared_token
with a top-level secrets: block that points each name at a file outside the
repository. The daemon reads the whole file (up to 4 KB) and drops the
trailing newline.
- The image carries no secrets. The configuration, the tokens and the
database all come from the environment and the volume.
What works in a container, and what does not¶
| Source | In a container? | Why |
|---|---|---|
| OpenCode Go | Yes | It is an HTTP API on the internet. |
| Ollama Cloud | Yes | It is an HTTP API on the internet. |
| LiteLLM | Yes | Point it at the gateway: http://host.docker.internal:4000 or the gateway's LAN address. |
| Ollama (a server on your machine) | Yes | Same: http://host.docker.internal:11434 or the LAN address. |
| Claude Code quota (the terminal export) | Through a collector | The export runs on the machine where Claude Code runs and reads files there. Use the remote collector below. |
| Claude Code activity (the local session logs) | Through a collector | The reader opens the session files on the machine where Claude Code runs. A container has none; the collector's watch mode pushes the summary. |
Claude Code usage cache (~/.claude.json) |
Through a collector | Same: the file is on the Claude machine, and it must not be mounted into the container. watch pushes the numbers and nothing else from it. |
| Codex | Through a collector | The source needs a local codex app-server and the local login, which a container does not have. The collector's watch mode reads Codex on the machine where it is signed in and pushes the limits. |
A source whose files are missing does not break anything: it says plainly that it is unavailable, and why, in its status. The container does not crash over it.
host.docker.internal is the name Docker Desktop gives the machine that
runs Docker. Inside the container localhost is the container itself, so the
Add source form fills in host.docker.internal for Ollama and LiteLLM and
says why. On Linux, add it yourself in the compose file:
The LAN address of the machine works everywhere and needs nothing.
Feeding Claude data to a containerised daemon¶
The way in is the remote collector, the same one the local install uses:
- Turn the feature on: Settings -> Remote reports -> Accept remote
reports in the admin page, or
DASH_INGEST_ENABLED=1in.env. The.envvalue locks the checkbox, so use one or the other. - On the same panel, download the collector
presto_collector.py. It is a single file, Python 3.8 or newer, and it runs on the Claude machine. The panel shows its SHA-256 to compare after you copy it. - Under Accounts that report remotely, press Create remote token for the Claude account. The token is shown once. The daemon stores only its hash.
- On the Claude machine, set that profile's
statusLinecommand to the snippet the page gives you, and point the URL at the container's ingest address (http://server:8787/api/v1/ingest) and the token file at the token you copied.
Claude Code then runs the collector after each response, and the reading arrives in the container. The body carries quota numbers, hashes and a short host label. No credentials, no prompts, no transcripts.
That is the terminal's status line. Two things have no status line: the
VS Code panel and Codex. For those the collector has a watch mode,
which reads on a timer and pushes with the account's own token, from a folder
of its own (~/.presto-ai-dash/collector.json). You do not write it: when you
create the token, press Download setup file (check the address first),
download presto_collector.py beside it, and on that computer run
python presto_collector.py setup. It finds the download, saves your settings
and the token, deletes the download and checks once. Read
Remote collectors for it, and for what the
collector says when the container is not accepting reports yet.
Two warnings:
- The ingest route is plain HTTP. The token travels in the clear, like the device token. Never expose the device port to an untrusted network; use a reverse proxy with TLS or a private network.
- A remote token can only write readings for one account. It cannot read the status payload, list your accounts, or touch anything else.
The activity logs and the usage cache are pushed by the collector's watch
mode, not by the status line, and so is Codex: the Codex source needs the
Codex app-server and its login on the machine it runs on, and a container has
neither. Run watch on the machine where Codex is signed in, and the
container shows what it reads. The collector never puts a login in the
container: it reads the limits there and sends only the numbers.
None of this has been run against a real second computer yet; see Remote collectors for exactly what has been tested.
Seeing the Presto screen without a Presto¶
The image carries the Presto app and an emulated 240x240 screen, so you can
try the container, and click through the cards, without a physical device.
It is off by default. To turn it on, add this to .env and recreate the
container (docker compose up -d):
Then open http://127.0.0.1:8790/. The screen shows your real accounts, polled
from this daemon as a device named psl, which also appears on the Status
tab. Tick Demo on the page for sample data instead.
It is a test aid, not a feature to leave on. The page has no sign-in and shows
live account names and quotas, so Compose publishes its port on 127.0.0.1
only. If you change the published port, set DASH_PRESTO_PREVIEW_ALLOWED_HOSTS
in .env to the new host:port names (for example
localhost:18790,127.0.0.1:18790), as the page refuses any other address. On a
server, reach it with an SSH tunnel rather than by publishing it.
The environment variables¶
The compose file passes the documented variables through, so .env can
hold any of them:
| Variable | What it does |
|---|---|
DASH_SHARED_TOKEN |
The token the Presto device holds. Required by the compose file. |
DASH_IMAGE |
The image to run. ghcr.io/jake1164/presto-ai-dash:latest (the newest release) unless you set it, for example to a version. |
DASH_PUBLISH_ADDRESS |
The host address the device port is published on. Every address by default; set your LAN address on a host with a public interface. |
DASH_DEVICE_PORT |
The host port for the device. 8787. |
DASH_ADMIN_PUBLISH_ADDRESS, DASH_ADMIN_PUBLISH_PORT |
The host address and port the admin page is published on. 127.0.0.1 and 8788. |
DASH_ADMIN_TOKEN_FILE, DASH_SHARED_TOKEN_FILE, DASH_ENCRYPTION_KEY_FILE |
A path to a file holding the value, instead of the variable. Not both. |
DASH_HOST / DASH_PORT |
The device port. 0.0.0.0:8787 inside the container. |
DASH_ADMIN_HOST |
The admin page's bind address. 0.0.0.0 inside the container. |
DASH_ADMIN_ALLOWED_HOSTS |
The names the admin page answers to, comma separated. localhost and 127.0.0.1 on DASH_ADMIN_PUBLISH_PORT unless you set it in .env. |
DASH_ADMIN_TOKEN |
The admin page's token. Required for a non-loopback bind address. |
DASH_ADMIN_SECURE_COOKIE |
true when a TLS proxy fronts the admin page, so the session cookie is marked Secure. |
DASH_DATABASE |
The database. /data/dash.db. |
DASH_KEY_FILE |
The key file. /data/dash.db.key. |
DASH_ENCRYPTION_KEY |
The encryption master key, instead of the key file. |
DASH_INGEST_ENABLED |
The remote reports switch. Unset by default, so you can tick Accept remote reports in the admin page. Set to 1 or 0 in .env to fix the choice; the checkbox is then locked. |
DASH_PRESTO_PREVIEW |
1 serves the Presto screen in a browser at port 8790, on this machine only. Off unless set. |
DASH_PRESTO_PREVIEW_ALLOWED_HOSTS |
The host:port names the preview answers. Change it only with the published port. |
DASH_DEMO |
Demo mode: sample data instead of the real accounts. Off unless set to 1. |
DASH_LOG_LEVEL |
INFO by default. |
DASH_OVERFLOW_ROWS |
The Home overflow strip. 1 by default. |
TZ |
The time zone of the process. |
Troubleshooting¶
The container will not start. Read its log:
The two messages you are most likely to see are the two refusals above: no admin token, or no allowed hosts.
The admin page answers 403. The name in the browser's address bar is not
one the page knows. Add it to DASH_ADMIN_ALLOWED_HOSTS, including the
port the browser used (127.0.0.1:8788), and restart the container.
The Presto says "Daemon offline". The device port must be reachable
from the device: it is published on the LAN, so the Presto needs the
address of the machine that runs the container, and the firewall must let
8787 through. curl http://<the machine>:8787/healthz from another
machine on the LAN is the quickest check.
A source says it is unavailable. In a container that is expected for the Claude readers and for Codex. The reason is in the account's status in the admin page, and in the daemon's log.