Analytics role
Theanalytics role runs Plausible Community Edition for the fleet. An Instance sends visits to it through a tracking host. The Gateway reads the visits back for the Orbit web Instance page. analytics lists the role commands. Instance analytics stats owns the read.
Prepare the database Processes
The role keeps its data in two Docker Processes. Create them first on an active Node with thedatabase role.
A Process needs a command. For PostgreSQL, use
postgres. The ClickHouse image passes its own configuration file, so give it a server argument after two dashes, such as -- --logger.level=warning.
Publish PostgreSQL’s container port 5432 and ClickHouse’s HTTP port 8123 on the database Node’s WireGuard address only. Orbit refuses either storage Process if its published port binds to any other address, including an empty bind address (0.0.0.0). The WireGuard boundary is the real threat boundary; do not expose analytics storage on a public or LAN address.
Plausible connects with the credentials in each Process’s environment: POSTGRES_USER (default postgres) and POSTGRES_PASSWORD, and CLICKHOUSE_USER, CLICKHOUSE_PASSWORD, and CLICKHOUSE_DB. The ClickHouse container creates that database and user. Plausible connects to PostgreSQL as that Process’s own user, so give analytics its own PostgreSQL Process. Plan about 2 GB of memory for the three services, and disk that grows with traffic.
Assign the role
Assign the role withorbit node:role:add NODE analytics --postgres-process=ID --clickhouse-process=ID. In a terminal, the CLI asks for a missing Process. The role is a singleton. It conflicts with gateway and combines with database, so one services Node can hold both. It cannot be assigned during node:add, and it cannot be relocated.
Assignment refuses a storage Process that does not fit:
The role then converges in this order.
Plausible creates and migrates its PostgreSQL database each time it starts. The storage passwords, the two connection URLs, and
SECRET_KEY_BASE reach Plausible through the Process environment. The API hides and log reads redact that environment, but the Gateway database stores it unencrypted. The plausible container reaches a storage Process on its own Node through the Docker bridge, not through WireGuard. That is why the local rule exists.
You can read the logs of plausible and restart it like any other Process. You cannot remove it, or either storage Process, while the role is assigned (process.required_by_analytics).
https://analytics.orbit has no Gateway check in front of Plausible. Every WireGuard peer can reach it, because WireGuard membership is the security boundary. The first person to open it registers the Plausible owner account and owns the dashboard. Open it yourself right after you assign the role. Orbit does not create Plausible accounts, sites, or API tokens. To show visits on an Instance page, create a Stats API key in Plausible and store it with orbit analytics:credentials --set.
ClickHouse configuration
ClickHouse’s defaults assume a large server. On a small Node, its system log tables grow and its background merges run out of memory. So each role convergence applies the four ClickHouse files that Plausible Community Edition ships, unchanged. The role writes each file on the Node that runs the ClickHouse Process, owned by root with mode0644. It writes a file only when the content differs. It mounts each file read-only into the ClickHouse Process at the path that Plausible’s own setup uses.
logs.xml keeps only query_log, for 30 days, and turns off ClickHouse’s other system log tables. To diagnose ClickHouse itself, use its console log and query_log. Tables that ClickHouse wrote earlier keep their data. Orbit does not drop them.
The role adds only the mounts that the Process lacks. It keeps the Process’s ID, name, image, command, environment, ports, and other volumes. When another volume uses one of the four container paths, convergence fails with analytics.clickhouse_mount_conflict before it changes anything. Remove that volume first.
Each ClickHouse restart drops Plausible’s ClickHouse connection until ClickHouse is back.
To apply the configuration to an existing install, converge the role again with the same two Processes:
clickhouse-config step, before Plausible runs, and marks the ClickHouse Process failed. The next convergence repeats the step and restarts that Process. Removing the role leaves the files and mounts in place.
Update and remove the role
orbit analytics:update VERSION changes the pinned Plausible version and replaces the plausible Process. A new role starts at the version in the Gateway’s ORBIT_ANALYTICS_PLAUSIBLE_VERSION, which defaults to 3.2.1.
Dashboard publication first runs the shared Caddy package step. It leaves an installed Caddy at or above the floor in place.
orbit node:role:remove NODE analytics removes the plausible Process, the Caddy site, the certificate, the DNS record, and the role’s firewall rules. It deletes the stored SECRET_KEY_BASE and the role settings. It never touches the two databases or their Processes. Remove those Processes yourself to remove the data. Orbit does not back up or prune Plausible’s event data. Removal fails with analytics.tracking_hosts_exist while any Instance has a tracking host.
Publish a tracking host
A tracking host is a Route of kindanalytics_tracking that belongs to one Instance. It proxies only Plausible’s script and event paths.
orbit instance:analytics:enable INSTANCE publishes analytics.<instance domain>. The analytics role must be active (analytics.role_missing). The Instance must already serve a domain (analytics.domain_required). --host=HOST names another host. You can repeat it up to ten times. The command sets the exact host set, so it removes a host that you leave out. A host that another Route owns fails with analytics.host_taken.
A tracking Route copies the Node, Cluster, and publication of the Instance’s own Route when you create it.
When a Cluster attach, detach, activation, or deactivation moves the Instance, the tracking host moves with it. Both placements serve the host until private DNS answers for the old placement can have expired. Then the old placement stops. A move that fails before private DNS moves leaves the host on its old placement, and a retry moves it again.
When another edge terminates public TLS for the Instance, point the tracking host at that same edge.
The host answers two paths and nothing else:
The serving Node reaches Plausible over WireGuard. While the analytics role converges, the host keeps its Caddy site and its private DNS record. A tracking Route has no target and no upstream of its own.
route:update, route:target:set, and route:target:unset refuse it with route.kind_unsupported, and route:create cannot create one. route:destroy refuses direct removal with route.tracking_managed; disable analytics on the Instance instead. If the Route kind is not valid for the analytics removal path, the Gateway returns route.kind_invalid.
orbit instance:analytics:show INSTANCE returns each host with its Route, script URL, event URL, and the DNS record to create. The Gateway knows no public address. So the record is a CNAME from the tracking host to the Instance’s own domain. The answer also carries the script tag for the first host. Its data-domain is the Instance’s own domain. orbit instance:analytics:disable INSTANCE removes the hosts. You still create the site in Plausible and add the script tag to the Project.
An Instance with a tracking host cannot be removed (analytics.tracking_hosts_exist). Disable its analytics first.
Instance page panel
The Orbit web Instance page shows live visitors, visitors for Plausible’s day, 7 days, and 30 days, and the top ten pages. It shows the panel when the analytics role is active and the Instance has a tracking host. The Plausible site is the Instance’s own domain. When the Gateway cannot read the Stats API, the panel says so and shows no counts. See Instance analytics stats.Why it works this way
These reasons explain the design. Check them before you propose a change.Storage on database Nodes
Database servers belong ondatabase Nodes, where the operator sizes, inspects, and backs them up. So the role names two Processes that the operator created, by ID. A Node can run several PostgreSQL Processes, so naming only a Node would be ambiguous. The role never creates a database server itself.
No database provisioners
Plausible creates and migrates its own PostgreSQL database. The ClickHouse image creates its own database and user from its environment. So Orbit adds no PostgreSQL or ClickHouse administration.Plausible as a Process
The Process runtime already converges a Docker container on a Node, with logs, restarts, and Doctor checks. A second container runtime, Compose, or an Instance would duplicate that work.Plausible’s own ClickHouse files
Without them, ClickHouse ran a merge loop on a small Node. Orbit uses Plausible’s tested files unchanged, instead of its own variant. It mounts single files, because the ClickHouse image keeps its own files inconfig.d and users.d.
A dedicated Route kind with two paths
Browsers must reach Plausible’s script and event endpoints from the public internet, but they do not need the dashboard or the rest of its API. A dedicated Route kind derives its upstream from the active analytics role and exposes only/js/* and /api/event. Every other path returns 404. This fixed surface is small to verify and keeps the dashboard private.
General path proxying on any Route is rejected, because nothing else needs it and it would broaden the custom proxy upstream rules. A reserved prefix on the Instance’s own site is also rejected, because it puts fleet infrastructure inside every Project’s site, can collide with the Project’s paths, and makes tracking depend on the Instance’s runtime being awake. Publishing analytics.orbit is rejected, because it would expose the whole fleet’s dashboard and login.