Deployment
Meta runs each Muse in a per-user secure VM. Locally, a container is the nearest equivalent: the agent sees only its data directory and its workspace.
This page is about the Python runtime — the web app's agent — in Docker. Running the relay (your own sign-in, hub and sync) and pointing the apps at it is self-hosting.md, which also puts the three ways to run nanoMuse yourself in order.
Docker
cp .env.example .env && $EDITOR .env # API key
nanomuse config init # or copy config/config.example.toml to config/config.toml
docker compose up -d app # the phone app on http://<host>:8787
docker compose logs app # shows the URL with the access tokenOther entry points:
docker compose run --rm muse # terminal chat
docker compose run --rm muse run "plan my week" # one task
docker compose up -d daemon # advance goals on a timer, no UIThe image runs as a non-root user with all capabilities dropped. Two volumes hold state: nanomuse-data (memory, goals, vault, audit log, threads) and ./workspace (the agent's files). config/config.toml is mounted read-only. Set NANOMUSE_SERVER_TOKEN in .env so the token is stable across restarts, and open the printed URL from your phone using the host's address.
Without cloning — the published image (linux/amd64 and linux/arm64, so a Raspberry Pi or an Apple-silicon Mac works):
docker run -d --name muse -p 8787:8787 --env-file .env \
-v nanomuse-data:/data -v "$PWD/workspace:/workspace" \
ghcr.io/nano-muse/nanomuse:latest
docker logs muse # the URL with the access token:latest and :X.Y.Z follow releases; :edge follows main. Without a mounted config.toml the image starts from config.example.toml, so the model is set from .env (NANOMUSE_LLM_*) or in the app's Connections screen.
With a browser. The -browser tags (:latest-browser, :X.Y.Z-browser, :edge-browser; linux/amd64) bundle Chromium and Playwright and start with the browser tool on, so the agent can use websites and you can watch and take over from the phone (see the app → browser view). It is about 400 MB larger. NANOMUSE_BROWSER_ENABLED=1 is what switches the tool on in that image; the same variable works anywhere Playwright and Chromium are installed.
Building by hand:
docker build -t nanomuse .
docker build --build-arg WITH_BROWSER=1 -t nanomuse:browser . # with Chromium
docker run -d --name muse -p 8787:8787 --env-file .env \
-v nanomuse-data:/data -v "$PWD/workspace:/workspace" \
-v "$PWD/config/config.toml:/app/config/config.toml:ro" nanomuseKeeping it running without Docker
A user-level systemd unit is enough:
# ~/.config/systemd/user/nanomuse.service
[Unit]
Description=nanoMuse
After=network-online.target
[Service]
WorkingDirectory=%h/nanoMuse
ExecStart=%h/nanoMuse/.venv/bin/nanomuse serve --host 0.0.0.0 --no-qr
Restart=on-failure
EnvironmentFile=%h/nanoMuse/.env
[Install]
WantedBy=default.targetsystemctl --user enable --now nanomuse
loginctl enable-linger $USER # keep it running after logout
journalctl --user -u nanomuse -fReaching it from outside your network
Do not expose port 8787 to the internet directly. Options that keep the token scheme intact:
- Tailscale / WireGuard: bind to
0.0.0.0, open the tailnet address on your phone. - Reverse proxy with TLS (Caddy, nginx): proxy
/and/ws(WebSocket upgrade) to127.0.0.1:8787. The token still applies.
TLS is also what turns on push notifications on the phone: browsers only allow a service worker and Web Push in a secure context (https:// or localhost). A Tailscale address with tailscale serve or a Caddy block like the one below is enough:
muse.example.com {
reverse_proxy 127.0.0.1:8787
}Updating
uv tool upgrade nanomuse # PyPI install (or: pip install -U nanomuse)
git pull --ff-only && uv pip install -e ".[dev]" # source install
docker compose build && docker compose up -d app # DockerData formats (SQLite, JSONL, JSON) are kept backward compatible within a minor version.