- 👉 Start Here: Quick Start Guide - Main setup path for new users
- 📚 Full Documentation - Comprehensive reference
- 🏗️ Architecture Details - Technical deep dive
- 🐧 Running with Podman - Use Podman instead of Docker (engine selection, rootless/GPU)
Chronicle uses a unified initialization system with clean separation of concerns:
- Configuration (
wizard.py) - Set up service configurations, API keys, and .env files - Service Management (
services.py) - Start, stop, and manage running services
The root orchestrator handles service selection and delegates configuration to individual service scripts. In general, setup scripts only configure and do not start services automatically. Exception: extras/asr-services is a startup script. This prevents unnecessary resource usage and gives you control over when services actually run.
New to Chronicle? Most users should start with the Quick Start Guide instead of this detailed reference.
- Location:
/wizard.py - Purpose: Service selection and delegation only
- Does NOT: Handle service-specific configuration or duplicate setup logic
- Backend:
backend/init.py- Complete Python-based interactive setup - Speaker Recognition:
extras/speaker-recognition/init.py- Python-based interactive setup - ASR Services:
extras/asr-services/setup.sh- Service startup script
Set up multiple services together with automatic URL coordination:
# From project root (using convenience script)
./wizard.sh
# Or use direct command:
uv run --with-requirements setup-requirements.txt python wizard.pyThe orchestrator will:
- Show service status and availability
- Let you select which services to configure
- Automatically pass service URLs between services
- Display next steps for starting services
Each service can be configured independently:
All setup commands run from the repository root — setup-requirements.txt
lists the shared chronicle-setup package as a relative path, and uv resolves
that against the working directory. Each init.py finds its own files from
__file__, so this does not change which .env it writes.
# Chronicle Backend only
uv run --with-requirements setup-requirements.txt python backend/init.py
# Speaker Recognition only
uv run --with-requirements setup-requirements.txt python extras/speaker-recognition/init.py
# ASR Services only
uv run --with-requirements setup-requirements.txt python extras/asr-services/init.py- Interactive setup for authentication, LLM, and transcription (memory is the agentic Markdown vault — no provider choice)
- Accepts arguments:
--speaker-service-url,--parakeet-asr-url - Generates: Complete
.envfile with all required configuration - Default ports: Backend (8000), WebUI (5173)
- Prompts for: Hugging Face token, compute mode (cpu/gpu)
- Service port: 8085
- WebUI port: 5173
- Requires: HF_TOKEN for pyannote models
- Starts: Parakeet ASR service via Docker Compose
- Service port: 8767
- Purpose: Offline speech-to-text processing
- No configuration required
When using the orchestrated setup, service URLs are automatically configured:
| Service Selected | Backend Gets Configured With |
|---|---|
| Speaker Recognition | SPEAKER_SERVICE_URL=http://host.docker.internal:8085 |
| ASR Services | PARAKEET_ASR_URL=http://host.docker.internal:8767 |
This eliminates the need to manually configure service URLs when running services on the same machine.
Note (Linux): If host.docker.internal is unavailable, add extra_hosts: - "host.docker.internal:host-gateway" to the relevant services in docker-compose.yml.
✅ No Unnecessary Building - Services are only started when you explicitly request them ✅ Resource Efficient - Parakeet ASR won't start if you're using cloud transcription ✅ Clean Separation - Configuration vs service management are separate concerns ✅ Unified Control - Single command to start/stop all services ✅ Selective Starting - Choose which services to run based on your current needs
| Service | API Port | Web UI Port | Access URL |
|---|---|---|---|
| Chronicle Backend | 8000 | 5173 | http://localhost:8000 (API), http://localhost:5173 (Dashboard) |
| Speaker Recognition | 8085 | 5175* | http://localhost:8085 (API), http://localhost:5175 (WebUI) |
| Langfuse | 3002 | 3002 | http://localhost:3002 (WebUI/API) |
| Parakeet ASR | 8767 | - | http://localhost:8767 (API) |
*Speaker Recognition WebUI port is configurable via REACT_UI_PORT
Note: Browsers require HTTPS for microphone access over network.
| Service | HTTP Port | HTTPS Port | Access URL |
|---|---|---|---|
| Chronicle Backend | 80->443 | 443 | https://localhost/ (Main), https://localhost/api/ (API) |
| Speaker Recognition | 8081->8444 | 8444 | https://localhost:8444/ (Main), https://localhost:8444/api/ (API) |
| Langfuse | 3002 (direct fallback) | 3443 | https://localhost:3443/ (WebUI/API) |
Caddy services start automatically with the standard compose command when HTTPS is configured.
See ssl-certificates.md for HTTPS/SSL setup details.
Services use host.docker.internal for inter-container communication:
http://127.0.0.1:8085- Speaker Recognitionhttp://host.docker.internal:8767- Parakeet ASR
The node agent (edge/service_manager.py) is a small host-side HTTP API that does
two jobs for one machine:
- Control — lets the WebUI System page start/stop/restart services and switch the
active ASR/TTS provider (it wraps
services.py). - Advertise — announces this node's services (and itself, as
chronicle-node) on the Tailnet via minidisc, so the backend/other nodes discover them. The advertised labels carry live state —runningandhealth— refreshed on a timer (ADVERTISE_REFRESH_SECS, default 30s), not just what's enabled. This folds in the old standalone discovery agent, which has been removed.
It must run natively on the host: docker compose needs host bind-mount paths, and (on Docker Desktop/WSL2) a container can't bind the Tailscale interface to advertise.
- Started automatically by
./services start --all/./services restart --allon any start (so a service-only node with no backend still advertises).stop --allleaves the agent running;./services manager stopstops it explicitly - Manual control:
./services manager start|stop|restart - Identity / cluster:
GET /node(host, Tailscale name/IP, arch, GPU) andGET /cluster(live tailnet view); both token-gated.GET /healthis unauthed. - Port: 8775 (override with
SERVICE_MANAGER_PORT) - Auth: bearer token, auto-generated into
backend/.envasSERVICE_MANAGER_TOKENon first start; the backend reads the same value and proxies admin-only requests (/api/admin/services/*) to the agent - Distributed setups: run the agent on the machine that hosts the services and point the backend at it via
SERVICE_MANAGER_URL(e.g.http://gpu-box.ts.net:8775); copy the token into both machines' configuration - Logs / PID:
edge/service-manager.log,edge/.service-manager.pid - Remote service nodes: a GPU box / RPi that runs a single service joins the cluster either via the wizard (Setup type → Join a cluster) or the one-liner
edge/install.sh <service>— both default to the node agent (advertise + control + reboot persistence) and need noSERVICE_MANAGER_URLwiring. The legacy advertise-onlyedge-agentsidecar (--profile edge) is the secondary fallback viaedge/install.sh --advertise-only, for boxes where you don't want a host process (no control, no WSL2). See edge/README.md.
manager install installs two systemd user services (with linger, so they
start without an interactive login):
chronicle-service-manager— the node agent itself. It runs natively on the host, not in Docker, so it does not come back after a reboot on its own; a fresh boot otherwise leaves the WebUI System page showing "Service manager not up, use ./services start --all".Type=exec, started immediately on install.chronicle-stack— aType=oneshotthat runs./services start --allon boot to bring the container stack back (enabled services fromconfig.ymlthat placement permits on this node, exactly what./services start --alldoes).ExecStopruns./services stop --all, so stopping or restarting this unit also stops the containers. Start and stop each have a 15-minute budget. Under Docker the containers'restart:policy revives them on boot, so this is belt-and-suspenders; under rootless Podman it's essential — Podman is daemonless and nothing re-appliesrestart:policies after a reboot (see podman.md). OrderedAfter=chronicle-service-manager.service; enabled for boot only (installing it does not kick off a fullstart --all—./services start --allowns the running stack).
The wizard offers both near the end of setup ("Auto-start on boot"); you can also do it manually:
# Install both units (~/.config/systemd/user/chronicle-{service-manager,stack}.service),
# enable linger, start the agent now and register the stack for boot
./services manager install
# Remove both
./services manager uninstallOnce installed, ./services start --all uses systemd to ensure the node agent is running;
the CLI submits container operations to that agent. ./services stop --all leaves the managed agent
running so it remains available for service controls. Inspect boot registration with
systemctl --user status chronicle-service-manager chronicle-stack.
Use systemctl --user start|stop|restart chronicle-stack to manage the stack through
systemd, or use ./services for operator commands. CLI operations do not
update the oneshot unit's active state; after a direct stop, use ./services start --all or
systemctl --user restart chronicle-stack to bring containers back. Inspect logs
with journalctl --user -u chronicle-stack or -u chronicle-service-manager.
Start, stop and restart return a nonzero exit status if any selected service fails. Bulk start/restart skips groups assigned only to other nodes, allows declared HA instances, and fails closed if the placement plan cannot be read. Each activation still acquires its normal reservation. Explicit service requests remain subject to admission; stop does not require the authority, including for excluded instances. Start/restart waits for selected services to become ready; see the completion rules below.
To refresh an older installed unit, rerun ./services manager install. Installing
the unit does not restart containers. If startup fails after partially starting
services, inspect the command output and use ./services stop --all to stop that partial stack.
Upgrading from the old two-agent layout: the standalone
chronicle-discoverysystemd unit is obsolete (the node agent advertises now)../services start --alland./services manager installauto-disable and remove a leftoverchronicle-discoveryunit, so no manual cleanup is needed.
Requires a systemd user instance. On a normal Linux host this is available out of the box. On WSL, enable systemd first: add a
[boot]section withsystemd=trueto/etc/wsl.conf, runwsl --shutdown, then reopen the terminal. If it's unavailable, the wizard/CLI prints this hint and skips installation.
From the WebUI (System page → External Services) you can start/stop/restart any
enabled service and switch ASR/TTS providers. Provider switches write the new
ASR_PROVIDER/TTS_PROVIDER to the service's .env, stop the old container, and
start the new one (they share a port). Tick "Build images" if the new provider's
image hasn't been built yet.
./services is the single operator entry point. It selects Docker or Podman from
configuration and uses the same lifecycle implementation as the WebUI. Run it from
the checkout; it resolves its own working directory. The old root start/stop/restart/
status scripts and Python CLI entry points have been removed.
./services status
./services status llm-services wakeword-service --detailed
./services status --json
./services start llm-services wakeword-service
./services restart backend
./services stop tts
./services start --all
./services stop --all
./services start backend --build --force-recreate
./services logs wakeword-service --tail 100
./services inspect llm-services
./services doctor
./services deployments --statusLifecycle commands require service names or explicit --all, never both. Names
refer to service groups from the registry, not container names; help lists the
available groups. ASR/TTS provider selection remains in setup and WebUI controls.
A normal restart recreates containers in place without rebuilding images;
restart --recreate performs down/up. Use --build for image changes. Mounted code
must be present in the actual runtime mount; a deployment using a reviewed source
snapshot needs that snapshot refreshed before restart.
The current node is the default. Remote operations require an explicit registered node ID; selection never silently moves to another owner:
./services status --node rainbow
./services restart tts --node rainbow
./services logs tts --node rainbow --tail 100--all start/restart selects enabled services allowed on that node by the placement
plan. Stop can clean up excluded instances even if the authority is unavailable.
Local inventory, endpoint readiness, and backend dependency health are separate
status fields: a stopped local service may be supplied by another node.
Normal lifecycle commands submit to the node manager, display an operation ID and
progress, and wait for completion. The local manager is started once if needed,
with a 30-second startup deadline. Read-only commands never start it. stop --all
leaves the manager running; use ./services manager stop to stop it explicitly.
Start/restart launches every selected group before checking readiness. It waits up
to 300 seconds after the container actions finish; change this with --timeout.
Success requires running containers and provider-aware endpoint readiness, including
configured identity checks. Stop verifies that containers stopped. A timeout reports
failure without rolling back or resubmitting. Exit codes are 0 for success/accepted,
1 for operational failure, and 2 for invalid usage.
./services start llm-services --timeout 600
./services start llm-services --no-wait
./services operation OPERATION_ID
./services operation OPERATION_ID --node rainbow --json--no-wait returns after acceptance; the manager continues readiness checks. Ctrl-C
stops waiting without cancelling the accepted action. Poll its ID instead of issuing
the action again. Operation polling is node-local and retained in manager memory;
a manager restart can lose that polling record. Consult logs/System Events rather
than assuming a lost record means the action did not happen.
If the manager cannot start, use explicit local recovery:
./services stop llm-services --direct--direct uses the same executor and host lock without the manager. It cannot be
combined with --node or --no-wait. Admission still applies to start/restart, so an
unavailable placement authority blocks activation. There is no automatic fallback.
Readiness is not a complete end-to-end test: verify generation for an LLM, fresh job activity for background work, and a spoken-device test for wakeword when those capabilities are the subject of a repair.
CLI and WebUI lifecycle actions share node-manager operation tracking and System Events → service reporting. Event reporting is best effort: if the backend is down, the operation reports that history could not be written and node-manager logs remain available. Direct recovery is explicitly marked as outside that ledger.
logs defaults to 100 lines per group container, with a maximum of 10,000; use
--container NAME to select a container within the group. Live following is not
implemented. inspect returns selected state, image, ports, mounts, and health timing
fields, excluding environment variables and health command credentials. Both support
--json and --node. Human operation timestamps are IST; JSON timestamps are Unix
seconds. JSON output contains no decorative text.
Use these commands before raw container-engine diagnostics. If a specific question
requires evidence they do not expose, state that gap and use a scoped read-only
engine command on the hosting node. Never dump full environments. Raw engine
lifecycle commands are reserved for isolated development, unmanaged services, or a
broken Chronicle CLI; explicit --direct is the first local recovery path.
journalctl --user -u chronicle-service-manager
journalctl --user -u chronicle-stackAn unmanaged manager writes edge/service-manager.log. For an unexpected restart,
check operation history first. Missing history does not prove a crash: direct
recovery, failed event reporting, and raw engine actions can leave no ledger entry.
backend/.env- Backend configuration with all servicesextras/speaker-recognition/.env- Speaker service configuration- All services backup existing
.envfiles automatically
- Root:
setup-requirements.txt(rich>=13.0.0) - Backend:
setup-requirements.txt(rich>=13.0.0, pyyaml>=6.0.0) - Extras: No additional setup dependencies required
setup-requirements.txt lists the shared package as -e ./extras/chronicle-setup.
The -e matters: uv run --with-requirements caches the environment it
builds and reuses it while this file's text is unchanged, so a non-editable path
dependency stays frozen at the sources it was first built from. Edits to
chronicle_setup/ are then silently ignored by the wizard, every init.py, and
./services doctor, which keep running a stale wheel from ~/.cache/uv.
Measured on uv 0.6.16: neither uv cache clean chronicle-setup nor a
[tool.uv] cache-keys entry invalidates it — only --reinstall-package, which
every invocation would have to remember. Editable installs link to the source
tree instead. If a change to this package appears to have no effect, check that
the -e is still there:
uv run --with-requirements setup-requirements.txt \
python -c "import chronicle_setup, sys; print(chronicle_setup.__file__)"
# want the repo path, not a ~/.cache/uv/archive-* pathStart with the service-management workflow: detailed status,
host diagnostics, placement checks, and then scoped logs. Check that the configured
container engine is available (Docker needs its daemon; rootless Podman does not).
For Podman host prerequisites and engine-specific faults, see podman.md.
Use the actual deployment address for endpoint checks; localhost applies only on the
hosting node. Prefer HTTPS and use curl -k for an internal/self-signed certificate.