RxKite Status monitor: private admin, Gotify/email alerts, R2 checks. amd64/arm64.
247
An independent, self-hosted status page for RxKite. A Rust service on a Raspberry Pi checks application servers and storage, keeps history in SQLite, and publishes a public snapshot to Cloudflare. The public page continues serving the last snapshot if the Pi or RxKite goes offline.
Start here: step-by-step deployment guide.
Public Cloudflare deployment: https://rxkite-status.pages.dev. Pages, private R2 storage and the D1 queue are deployed. The page waits for the monitor's first snapshot; status.rxkite.in is pending DNS setup.
Docker Hub: shounak6942/rxkite-status. Compose pulls the versioned release without building or requiring a Dockerfile; run python3 scripts/init-env.py, docker compose pull, then docker compose up -d. To compile from source, use the separate build override.
For Portainer's stack web editor, use docker-compose.portainer.yml and the Portainer steps. It takes credentials from stack environment variables and needs no local files.
For Cloudflare dashboard drag-and-drop, use npm run build:pages and follow the Pages Direct Upload guide. The ZIP includes the API as a bundled _worker.js; configure its R2/D1 bindings and secrets before connecting the Pi.
flowchart LR
Pi[Home Pi: Rust monitor + private admin] -->|HTTP health checks| App[BOM-1 / AMS-1 / CCU-1 / public site]
Pi -->|Authenticated storage check| Worker
Pi --> SQLite[(SQLite volume)]
Pi -->|Authenticated outbound publish| Worker[Cloudflare Worker + React static assets]
Worker --> R2[(Private R2 snapshot)]
Visitors[Status visitors] --> Worker
Worker -->|Signup / explicit link action| D1[(D1 event queue)]
Pi -->|Pull + acknowledge| D1
Pi -->|Persistent delivery outbox| Email[SMTP / Resend]
Email --> Visitors
The Pi needs no inbound public port. The private admin is published at 127.0.0.1:8088 by Compose, and opens directly without a login. Access it through SSH forwarding; private Host and same-origin checks protect browser API access. Public publishing uses a separate token. There is no public admin route.
dallergy/prescribe: /ok, /database, /databaseMs, /version.Requires Rust 1.90.0, Node 22+, Python 3, and optionally Docker with Compose/Buildx.
npm ci
cargo test --locked
npm run build
npm test
npx playwright install chromium
npm run test:e2e
Render the public page using committed sample data (does not contact production):
VITE_SAMPLE=true npm run dev -w web
For a fully offline Docker test using mock health endpoints:
docker build -t rxkite-status:local .
RXKITE_IMAGE=rxkite-status:local python3 scripts/docker-smoke.py
# Exercise the Portainer deployment format with the same offline checks:
RXKITE_IMAGE=rxkite-status:local RXKITE_COMPOSE_FILE=docker-compose.portainer.yml python3 scripts/docker-smoke.py
The smoke test first starts the image as UID 10001 with its bundled INITIAL_CONFIG, a read-only filesystem and no outbound network. It then uses an isolated Compose project, random temporary secrets, a mock health service, a temporary volume, a one-off check cycle, and the actual service. It tests readiness, authentication, incident creation/resolution, disk publication and persistence after restart, then removes only its own test resources. It does not send emails or check production servers.
Run the native monitor by creating a private local configuration and writable data directory:
python3 scripts/init-env.py
mkdir -p data
set -a; . ./.env; set +a
DATABASE_URL=sqlite://data/status.sqlite STATUS_OUTPUT=data/status.json cargo run --locked
INITIAL_CONFIG seeds the settings until the first admin save; thereafter SQLite settings are authoritative. --once executes enabled checks and publishes one snapshot, then exits. Check failures are represented in the snapshot rather than causing a process exit; publishing/database failures cause a nonzero exit. --healthcheck calls the running service's liveness endpoint.
For a native monitor with local mock health checks and retained development data, run cargo build --locked then python3 scripts/dev-monitor.py. It generates private development credentials under the ignored data/ directory; the admin opens directly through its loopback address. It binds admin to loopback, writes a disk snapshot, and does not contact production services.
For the Worker locally, build the frontend, apply D1 migrations with --local, set private worker/.dev.vars (see deployment guide), and use npm run dev -w worker. Vite proxies /api to the local Worker. Real Pi publishing requires HTTPS; the offline smoke test uses disk mode.
config/rxkite.example.json is the complete seed example. Placeholder regional app monitors are disabled until you replace their URLs. The R2 monitor is disabled until publishing is connected; select R2 connected storage and enable it. The public RxKite monitor is enabled. The CCU-1 address is a sample LAN address, not an assertion about your network.
| Setting | Meaning / default |
|---|---|
monitors[].id | Stable letters/digits/hyphens ID; retained history is keyed by this ID |
name, group, url, enabled | Display labels, grouping and HTTP(S) health/test URL; use uncredentialed URLs |
check_type | http (default) for URL checks; r2 for private storage writes/reads using the connected Cloudflare backend |
interval_secs | 180 for app checks, 300 for R2; range 30–86400 |
timeout_secs | 10; range 1–60; limits the whole request/body |
retries, retry_delay_secs | 2 retries, 30 seconds apart; retries 0–2, delay 1–120 |
expected_status | Exact expected HTTP code, default 200; redirects are not followed |
status_path, status_value | JSON pointer and expected JSON value, e.g. /ok and true |
db_latency_path | Pointer to a nonnegative millisecond number, e.g. /databaseMs |
db_status_path, db_status_value | Pointer and healthy value, e.g. /database, "connected"; an unhealthy DB makes the check fail |
version_path | Pointer to version string; empty disables extraction |
branding | Name, HTTPS logo URL, accent #RRGGBB, HTTPS footer links |
templates | incident, recovery, maintenance; {name} expands for automatic incidents |
subscribers | enabled, public HTTPS URL, and sender |
sender.kind | disabled, smtp or resend |
| SMTP fields | from, smtp_host, smtp_port (587), smtp_username, smtp_password, smtp_starttls (true; false means implicit TLS, not plaintext) |
| HTTP fields | from, http_url (Resend API endpoint), api_key; speaks the Resend JSON API, not arbitrary providers or SES HTTP |
publish | mode: disk or worker; output_path, or HTTPS worker_url and token |
alerts | Optional gotify_url + gotify_token, gotify_priority (0–10, default 5); telegram_token + telegram_chat_id; email (requires sender) |
cache_ttl_secs | 300; range 30–3600; controls HTTP/CDN cache headers |
JSON pointers support nested fields and array indices. Escape ~ as ~0 and / as ~1. Empty paths disable the corresponding extraction. A monitor with no JSON paths is a plain HTTP/body check. Response bodies must be at most 64 KiB. Status values in the admin form are JSON, so a string requires quotes.
Uptime percentages are confirmed successful check samples / all confirmed check samples, not continuous wall-clock SLA measurements. Retries collapse into one final sample. Days without samples are grey and excluded. Check intervals resume after a cycle completes; a confirmed failure takes up to the original timeout plus retry delays/timeouts. A current component becomes unknown after its check freshness threshold (two intervals plus retry budget). The public page independently warns at 15 minutes without a new snapshot. Raw checks are retained for 100 days; hourly chart data is aggregated on publication. Incident history is retained until you remove the database. All dates in payloads use UTC; the public page shows incident times in the visitor's timezone.
| Environment variable | Purpose |
|---|---|
ADMIN_AUTH | private (default): direct admin access through loopback/private IP, with same-origin checks |
ADMIN_TOKEN | Optional 32+ character bearer credential for API automation; required only with ADMIN_AUTH=bearer |
SETTINGS_KEY | Required 64 hexadecimal characters; encrypts settings/outbox/link tokens. Back it up; changing it makes old data unreadable |
ADMIN_BIND | Native default 127.0.0.1:8088; container listens internally on 0.0.0.0:8088 |
ALLOW_LAN_ADMIN | Explicit true needed for non-loopback binding; firewall/network isolation remains required |
DATABASE_URL | Default sqlite:///data/status.sqlite |
INITIAL_CONFIG | Seed JSON path; container defaults /etc/rxkite-status/initial.json |
WORKER_URL, WORKER_TOKEN | Override saved publishing target/token; Worker URL selects worker mode |
SMTP_PASSWORD, EMAIL_API_KEY, TELEGRAM_TOKEN, GOTIFY_TOKEN | Override encrypted saved sender/alert secrets |
STATUS_OUTPUT | Override local disk output path |
BACKUP_DIR | Default /data/backups; SQLite backup destination |
RUST_LOG | Log filter, default rxkite_status=info |
SSL_CERT_FILE | Optional trusted PEM CA bundle for outbound HTTP; TLS verification stays enabled |
Environment overrides take precedence at runtime. The admin leaves saved credential inputs empty with a “Saved credential — unchanged” placeholder. Enter a new value to rotate it or use Clear saved credential to remove it; the API represents preservation as __unchanged__. To remove an environment override, edit .env and recreate the container. Never put real keys in the example or commit .env/.dev.vars.
The private Rust service opens directly through a loopback/private IP address or .local hostname. Browser API requests must use the same origin; public Host headers and cross-origin requests are rejected. Compose publishes only 127.0.0.1:8088; keep the SSH tunnel for remote access. Optional ADMIN_AUTH=bearer protects API automation with Authorization: Bearer $ADMIN_TOKEN and does not enable the direct dashboard. GET /healthz is unauthenticated and checks SQLite liveness. This is distinct from the public Worker's /api/status and subscriber routes.
curl --fail-with-body http://127.0.0.1:8088/api/status
curl --fail-with-body http://127.0.0.1:8088/api/incidents \
-H 'Content-Type: application/json' \
-d '{"title":"Mumbai service disruption","message":"We are investigating.","kind":"incident","component_ids":["bom-1"]}'
# Replace INCIDENT_ID with the returned id. Each update is retained.
curl --fail-with-body http://127.0.0.1:8088/api/incidents/INCIDENT_ID/updates \
-H 'Content-Type: application/json' \
-d '{"state":"resolved","message":"Service has recovered."}'
# Maintenance times are Unix seconds; substitute your future window.
curl --fail-with-body http://127.0.0.1:8088/api/incidents \
-H 'Content-Type: application/json' \
-d '{"title":"Scheduled maintenance","message":"A short interruption is possible.","kind":"maintenance","component_ids":["public"],"starts_at":1900000000,"ends_at":1900003600}'
| Method / path | Operation |
|---|---|
GET, PUT /api/config | Read redacted complete settings / validate and replace full settings |
GET /api/status | Generate public-shaped snapshot from SQLite |
POST /api/check | Test one monitor once without saving a sample, creating incidents or changing its schedule |
POST /api/publish | Publish immediately; returns the published snapshot |
GET /api/incidents?limit=100&offset=0 | Paginated full retained announcement history, maximum page 200 |
POST /api/incidents | Create incident or planned maintenance |
POST /api/incidents/:id/updates | Append message + state; optionally edit title or both maintenance start/end values |
GET /api/subscribers | Confirmed/pending counts, queued/retrying delivery counts; no subscriber addresses |
POST /api/backup | Consistent SQLite VACUUM INTO backup; returns its path |
States: investigating, identified, monitoring, resolved for incidents; scheduled, in_progress, resolved for maintenance. Resolved announcements are immutable; create a new one for a new event. Automatic incidents are owned by the monitor; recovery resolves them. Create a separate manual incident for broader operational context. Creation/update emails are queued transactionally, and publication runs at least once a minute. Scheduled windows affect the public banner while active; use the admin/API to report progress and completion.
Pending signup addresses and token digests in D1 are private behind the publish token. SQLite subscriber addresses are personal data; protect the volume, backups, and encryption key. Do not enable request-body/query logging in Worker observability: link URLs are bearer capabilities. Delivery is at least once after crashes; Resend receives an idempotency key and SMTP receives a stable Message-ID, but SMTP cannot guarantee deduplication. Delivery failures back off up to six hours and remain visible in admin counts. Set up/verify an email domain with SPF/DKIM/DMARC before enabling subscribers. Provider quotas and email delivery depend on your configured account.
R2 stores the snapshot because KV's free 1,000 writes/day would be exceeded by per-check publishing plus minute heartbeats. With five checks at 3/5-minute intervals plus heartbeats, approximately 3,648 writes/day (under 114,000/month) fit within R2's 1 million free monthly Class A operations; 5-minute private R2 checks add about 8,640 Class A writes and 8,640 Class B reads/month. Public-file probes only read an object. R2's free storage allowance is ample for one capped 1 MiB snapshot. The public frontend and private admin refresh every 30 seconds. Monitor intervals, retries, publishing/heartbeat cadence and cache TTL are independent and unchanged. The Worker caches status responses at the edge for 300 seconds by default and serves static assets through Workers Assets. D1 empty queue polling uses around 1,440 requests/day. No KV namespace or R2 access key is needed by the Pi: its Worker binding performs object access.
Cloudflare Workers Free has request/CPU limits and D1/R2 have usage limits. Stay on the Free Workers plan, monitor the Cloudflare dashboard, and set usage/billing alerts. R2 may require billing activation; usage beyond its free allowance can be charged. The architecture fits the expected small status-page workload; it does not guarantee unlimited traffic without cost. Large subscriber lists also depend on sender free quotas. The monitor limits concurrent checks to four and SQL connections to three, pages notification recipients, caps health bodies, and aggregates history instead of loading raw samples. The runtime RAM target is below 50 MB for the sample workload, not a hard guarantee for 50 monitors or a large outbox; Compose provides a 96 MB ceiling.
See docs/deployment.md for Pi provisioning, exact Cloudflare commands, custom domain status.rxkite.in, sender configuration, smoke checks, multi-platform images, updates and consistent backup/restore. See docs/validation.md for what was actually tested.
Portainer Compose: https://github.com/dallergy/rxkite-status/blob/main/docker-compose.portainer.yml Step-by-step guide: https://github.com/dallergy/rxkite-status/blob/main/docs/deployment.md
Content type
Image
Digest
sha256:eca489caa…
Size
31.2 MB
Last updated
12 minutes ago
docker pull shounak6942/rxkite-status