Documentation
Install the server, add monitors and connect agents.
Quick Start
This sets up the central server on ordinary PHP web hosting. It is the same guide as on the download page.
Measured install time: 1 min (from the release ZIP to the first collector run on a clean PHP 8.2 and MySQL server, following this guide; uploading over FTP and clicking through your hosting panel come on top)
Check what the hosting offers
- PHP 8.2 or newer with the PDO MySQL, cURL, mbstring and OpenSSL extensions
- A MySQL or MariaDB database
- A cron job every minute (cPanel's Cron Jobs is enough)
- Apache with .htaccess and mod_rewrite, as on most shared hosting
- One domain or subdomain whose root holds both /status and /app: the app talks to /status on its own host
- Outbound HTTP and HTTPS from the hosting, for the checks
Put it outside the network you watch. A server at home goes down with the home line, and then nothing is left to tell you.
Download and upload
Latest release: —
Unzip it and upload status/ into public_html/status/ and app/ into public_html/app/. Keep app/.htaccess: without it, reloading a page inside the app answers 404. To check the download against the published checksum, save SHA256SUMS next to the ZIP and run:
sha256sum -c SHA256SUMS --ignore-missingNo release yet? Build the same two folders from the source
With git and Node.js 22 on your computer:
git clone --recursive https://github.com/BKPepe/monitoring.git cd monitoring npm ci npm run build -w apps/monitor cp agents/vps-agent/agent.sh agents/vps-agent/agent.py agents/vps-agent/agent.ps1 \ agents/vps-agent/agent_openwrt.sh agents/vps-agent/docker-compose.agent.yml apps/status/Then upload apps/status/ (without its tests/ folder) as status/ and apps/monitor/dist/ (with its .htaccess) as app/. The agents belong next to agent_api.php: that is where they download from and update themselves from.
Create an empty database
Create a MySQL or MariaDB database and a user with all privileges on it (in cPanel: MySQL Databases), and note the name, the user and the password. Leave it empty: the installer creates the tables. Only if you prefer to do everything by hand, import status/schema.sql yourself:
mysql -u YOUR_DB_USER -p YOUR_DB_NAME < schema.sqlRun the installer
Open https://YOUR-DOMAIN/app/setup. It checks PHP, tests the database login, writes status/config.php (or shows the file to upload when PHP may not write there), creates the tables and then the first administrator. There is no default account or password, and the installer refuses to run again once an account exists. Until then it is open to anyone who finds the address, so create the database before the upload and run the installer right after it. By hand instead: copy config.sample.php to config.php in status/ and fill in the connection:
define('DB_HOST', 'localhost'); define('DB_PORT', 3306); define('DB_NAME', 'YOUR_DB_NAME'); define('DB_USER', 'YOUR_DB_USER'); define('DB_PASS', 'YOUR_DB_PASSWORD');Optional keys go into the same file as constants, in capitals, or later into the app under Settings: CRON_KEY, AGENT_REGISTRATION_TOKEN and METRICS_TOKEN. The installer gives every new install its own CRON_KEY. On the manual path it stays empty until you set one, and while it is empty anyone who knows the address of cron.php can start a collection run.
config.php holds the database password. It is not in the ZIP, and uploading a new release does not overwrite it.
Run the collector every minute
One cron job runs every check. The installer's last step shows this line with your server's real path to PHP and to cron.php. In cPanel open Cron Jobs, choose Once Per Minute (* * * * *) and put the command into the Command field. USER is your hosting account; cPanel shows the home directory on its main page.
php -q /home/USER/public_html/status/cron.php >/dev/null 2>&1Without cPanel, the same job as a line for crontab -e:
* * * * * php -q /home/USER/public_html/status/cron.php >/dev/null 2>&1Log in
Log in at https://YOUR-DOMAIN/app/ with the account the installer created. After the manual path, /app/setup creates that first account instead.
Check that it runs
The installer waits for the first collector run and turns green when it lands, within a minute of adding the cron job. To check it later, or after the manual path (the address also opens in a browser):
curl -s "https://YOUR-DOMAIN/status/api.php?action=collection_health"Look for a lastRunAt time and "stale": false. "stale": true with lastRunAt null means the collector has never run: check the cron line and its path.
Next: add monitors in the app, and agents on the machines and routers you want to see inside. Install an agent →
Then: your first monitor
In the app, open Infrastructure → Add New Monitor. You can track websites (HTTP/HTTPS), TCP ports, Minecraft, TeamSpeak and Discord servers, servers and OpenWrt routers with an agent, or heartbeats from jobs that report in on their own. The legacy PHP admin (admin.php) is mostly Czech; the /app dashboard is in English and Czech.
System Requirements
The central server is plain PHP and runs on ordinary shared web hosting; a VPS works too. What the hosting must offer is step 1 of the Quick Start.
Central Server:
- Storage: the application plus a MySQL/MariaDB database that grows with your check history
- Software: PHP 8.2+ (CI runs 8.4), Apache with
.htaccessandmod_rewrite, and MySQL or MariaDB. The server runs without Docker; only the Python agent has a Docker option.
Remote Agent:
- OS: Linux (any system with Bash, or Python 3), Windows 10/11/Server, OpenWrt/Turris OS, or Raspberry Pi OS
- Network: Outgoing HTTPS access (port 443) to your central server; nothing inbound
Architecture Overview
Blood Kings combines active checks run by the server and reports that agents push to it.
Passive Telemetry (Agents): The agent is a script that cron (or the Task Scheduler) starts on the monitored machine: every minute on OpenWrt, every 5 minutes by default elsewhere. Each run reads system resources (CPU, memory, disks, network), reports them to the central server via secure POST requests. This means you do not have to expose open incoming firewall ports on the monitored hosts.
Active Probing (Pinger): The central server's cron run checks targets directly: HTTP/HTTPS requests (with an optional keyword and certificate expiry) and TCP connection checks. There is no ICMP ping and no UDP check; a "ping" is a TCP connect.
Central Server Database Config
The server connects to MySQL or MariaDB through PDO. The installer at /app/setup writes the connection into status/config.php (step 4 of the Quick Start, which also shows the manual way). config.sample.php explains every other constant; the one most installs change is the time zone, which PHP and the database session then share:
define('TIMEZONE', 'Europe/Prague');Keys & Tokens
Three optional keys. Each is read as a constant in config.php (in capitals), then as an environment variable, then from the app's Settings; the first one found wins.
CRON_KEY: the keycron.phpasks for when it is called over HTTP (?key=...). While it is empty, anyone who knows the address can start a collection run; the command-line cron needs no key. The installer gives every new install its own.AGENT_REGISTRATION_TOKEN: lets the Bash and OpenWrt agents register themselves (--register). While it is empty, the server acceptsCRON_KEYin its place.METRICS_TOKEN: the bearer token for the Prometheus endpointmetrics.php. While it is empty, the endpoint is off.
define('CRON_KEY', 'a-long-random-string');Public Status Pages
Create status pages in the app under Status Pages: each has its own title, address (/app/public?page=…) and set of monitors, and can be hidden from the public. Visitors can subscribe to incident updates by e-mail there. An SVG badge of the public state, or of one monitor (api.php?action=badge&monitor_id=…), can be embedded anywhere.
Heartbeat Monitors
Active checks only reach what is reachable over the network. A backup that runs at 3 AM and fails silently is invisible to them - there is nothing to ping. Heartbeat monitors invert the direction: the job reports in, and the monitor goes down when it stays silent.
Create a monitor of type Heartbeat, set how often the job should report, then call the URL it gives you at the end of your job:
curl -fsS -m 10 "https://your-server/status/heartbeat.php?token=YOUR_TOKEN"If the job itself fails, say so explicitly - the monitor then goes down immediately instead of waiting out the interval:
curl -fsS -m 10 "https://your-server/status/heartbeat.php?token=YOUR_TOKEN&status=fail&msg=tar%20exited%202"A monitor that has never received a signal reports unknown, not down. It is not failing - nothing is known about it yet, and alerting on an outage that never happened is its own kind of lie.
The token is the only thing authorising the endpoint. Keep it out of shared logs; if it leaks, anyone can report in on your behalf and the monitor will stay green while the backup is long dead.
Watchdog: who watches the collector
Everything is collected by one cron job. If that job dies, the application does not crash - it keeps serving the last known states and looks perfectly healthy. Of all the ways monitoring can fail, this is the worst, because it never announces itself.
The server therefore records every completed collection run and exposes it:
curl -s "https://your-server/status/api.php?action=collection_health"The response carries stale: true once the last run is older than the configured limit - and also when the collector has never run at all, because "no data" is not the same as "all good".
A Cloudflare Worker checks that endpoint every five minutes. It runs outside the monitored host on purpose: a watchdog sharing a machine with the thing it watches dies with it.
The Worker is not in the release ZIP: it lives in apps/worker of the repository, and you deploy it to your own Cloudflare account. Without a target it watches bloodkings.eu, the author's server, so give it yours first:
cd apps/worker && npx wrangler deploy
npx wrangler secret put WATCHDOG_TARGET_URL # https://your-server/status/api.php?action=collection_healthPoint it at a Discord webhook once and it will speak up on its own:
cd apps/worker && npx wrangler secret put WATCHDOG_DISCORD_WEBHOOKWithout that secret the watchdog still checks, but it can only log - and it admits as much on /api/watchdog, rather than looking like it is guarding something.
Agent Registration & Setup
Agents are self-contained scripts for Bash, Python, PowerShell and OpenWrt routers. The download page has the steps for every platform: the same ones the app prints when you add a monitor, there with your address and key filled in.
For many machines, the Bash and OpenWrt agents can register themselves with the registration token (see Keys & Tokens). Both arguments are positional, and the second one is your server's agent_api.php:
/opt/bk-agent/agent.sh --register YOUR_REGISTRATION_TOKEN https://YOUR-DOMAIN/status/agent_api.phpWithout the address the agent registers against its localhost default. It stores the key it receives in agent.cfg next to the script (agent_openwrt.cfg on a router). The Python and PowerShell agents have no register command: they take API_URL and the AGENT_KEY from the monitor's settings in agent.cfg. More on registration.
Agent Intervals & Remote Actions
Agents run from cron: every 5 minutes on servers, every minute on OpenWrt routers. For OpenWrt routers, opt-in HMAC-signed Remote Actions can be enabled by setting REMOTE_ACTIONS_ENABLED=1 in agent_openwrt.cfg next to the script.
Agent Troubleshooting
If an agent stops reporting telemetry, verify outgoing HTTPS access to port 443, then run it by hand and read its log (it sits next to the script):
/opt/bk-agent/agent.sh --verbose
tail -n 50 /opt/bk-agent/agent.logThe download page has the same checks for OpenWrt, Windows and Docker.