One web page for the Project Graphite VPS and its handbook. It runs at
https://console.project-graphite.com behind Cloudflare Access.
| Page | Shows |
|---|---|
| Overview | host CPU, memory, swap and disk; host usage charts; storage split into images, volumes, container layers and build cache; one row per project with status, CPU, memory and storage, with a filter and pins |
Project (#/project/<slug>) |
a usage chart with a line per container, the containers, and the images and volumes with their sizes |
| Container | a usage chart, details and networks, live logs, a shell, its ports |
| Images, Volumes | every image and volume on the VPS |
| Shell | a root shell on the host |
| Alerts | what needs attention now and what resolved in the last 7 days |
| Admin | every organisation member, when they last signed in, and what they can change |
| Docs | the handbook from the docs repository, with search |
Usage charts cover 1h, 5h, 1d, 7d or 30d. Hovering shows the time and every line's value, with the nearest line highlighted. On the Overview, hovering the VPS chart also breaks that moment's CPU and memory down by project, largest first, with the rest as system and other; CPU there is a share of the whole machine. The chosen range and the pins are kept in the browser.
Every organisation member can read everything except Admin. What they can change is set per member on the Admin page:
| Access | Lets them |
|---|---|
| a project | start, stop and restart its containers, wake it, open its containers' terminals and ports |
| images | remove dangling images |
| host shell | open the root shell on the host |
| everything | all of the above, for every project |
Organisation owners have everything, and only they see Admin. Access is kept in the console's
/data/access.db.
A Node 24 server talks to the Docker Engine API over /var/run/docker.sock, reads host usage from
/proc, samples every container every 5 seconds in the background, keeps 30 days of per-minute
history in node:sqlite (/data/metrics.db), reads disk usage every 5 minutes, and serves a React
page built with @project-graphite/ui. The handbook is read from GitHub every 5 minutes and
rendered with marked; raw HTML in it is shown as text. Terminals are xterm.js over WebSockets into Docker exec sessions. The host
shell is an exec into the console's own container running nsenter into the host's PID 1, so the
container runs privileged with pid: host.
Sign-in is two layers:
- Cloudflare Access admits only Project-Graphite members. The server verifies the
Cf-Access-Jwt-Assertionheader itself. - GitHub sign-in names the user. An organisation admin is an owner, and anyone else is a member.
Sessions last 1 hour, after which the page signs in again through GitHub without a prompt. Every change (start, stop, restart, wake, prune, a terminal, the host shell, an open port, saving access) asks GitHub for the user's role, at most once a minute, and reads their access before it runs. When they may no longer do it, the change is refused and their open terminals for it close. Removing access on the Admin page closes those terminals at once, and open terminals are rechecked every hour. The session cookie is encrypted and holds the user's GitHub token for these checks.
docker compose up --buildThen open http://localhost:3000. Locally there is no Access or GitHub sign-in: DEV_LOGIN names
the user and DEV_ROLE=member shows a member's view. The local stack talks to your own
Docker, so its shells and actions act on your machine.
For the page with hot reload, keep that container running and serve the page from your machine:
cd app
npm ci
npm run devVite proxies /api, /auth, /open and /ws to the container on port 3000, or to
CONSOLE_ORIGIN when it is set.
| Command | What it does |
|---|---|
npm run build |
Type-check, build the server to dist/server and the page to dist/web |
npm test |
Unit tests |
npm run lint |
oxlint |
platform/deploy/console/values.yaml deploys it, proxied through Cloudflare (proxied: true), with
the Docker socket and a data volume for the history. The values set NODE_ENV, APP_URL and
SSH_TARGET. It needs these values in the secrets repository:
| Name | Value |
|---|---|
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
a GitHub OAuth App owned by the organisation, callback https://console.project-graphite.com/auth/callback |
SESSION_SECRET |
32 or more random bytes, e.g. openssl rand -base64 48 |
ACCESS_TEAM |
the Zero Trust team name, the <team> in <team>.cloudflareaccess.com |
ACCESS_AUD |
the Access application's audience tag |
Every minute the console checks for a container that stopped, is unhealthy or restarted three times
in ten minutes (noting an out-of-memory kill), host memory over 90%, swap over 80% and disk over
85%, and a project's backup with no dump in 26 hours. A condition becomes an alert after two
checks in a row and resolves after two clear ones; finished one-off jobs and sleeping projects are
not alerts. Active alerts show on the Overview and the Alerts tab, and history is kept in
/data/alerts.db for 30 days.
With ALERTS_TOKEN set, each alert also opens an issue labelled alert in ALERTS_REPO
(project-graphite/platform) and closes it on recovery, so GitHub's notifications deliver it. The
token is a fine-grained personal access token with Issues read and write on that repository only.
platform's Uptime workflow raises the same kind of issue from outside the VPS.
Optional settings, with their defaults: ALERTS_TOKEN (none), ALERTS_REPO
(project-graphite/platform), GITHUB_ORG (Project-Graphite), DOCS_REPO
(project-graphite/docs), SABLIER_URL
(http://sablier:10000), DATA_DIR (/data), DOCKER_SOCKET (/var/run/docker.sock),
HOST_ROOT (/proc/1/root).
| Route | Who |
|---|---|
GET /api/me, /api/overview, /api/projects/<slug>, /api/history?target=<container>|host&range= or ?project=<slug>&range= (1h, 5h, 1d, 7d, 30d; default 1d), /api/images, /api/disk, /api/alerts, /api/docs, /ws/logs |
every member |
POST /api/containers/<id>/start|stop|restart, POST /api/projects/<slug>/wake, /open/<container>/<port>/, /ws/terminal |
access to the project |
POST /api/prune |
access to images |
/ws/host |
access to the host shell |
GET /api/admin/members, POST /api/admin/members/<login> with {"scopes": ["project:<slug>", "area:images", "area:shell", "*"]} |
owners |
/healthz |
anyone |
| Path | Purpose |
|---|---|
app/src/server/ |
HTTP and WebSocket server (main), Docker client, auth, sampling and disk usage (inventory), history, host usage from /proc, access (access), alerts (alerts, monitor), the handbook (docs), /open proxy, terminals and logs |
app/src/web/ |
the React page |
app/test/ |
unit tests |
compose.yaml |
local run against your own Docker |