Skip to content
SolutionmaxPublic

About

Self-hosted status page that runs its own HTTP, TCP, DNS and heartbeat checks. Probe locations, several status pages per install, monthly reports, passkeys. PHP 8.3, shared hosting or Docker.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

249 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pharos: self-hosted status pages with automatic monitoring

CI Licence: AGPL-3.0 PHP 8.3+ Laravel 12 Releases: signed zips, changelog, pinned installers Cachet-shaped API Runs on cPanel, DirectAdmin or Docker Buy me a coffee

Website · Documentation · Install · API & integrations · Licence

Pharos

A self-hosted status page that runs its own checks.

Pharos polls HTTP endpoints, TCP ports and DNS records, watches certificate expiry, listens for heartbeats from jobs it cannot see from outside, and sets component status without anyone pressing a button. A failing check opens an incident and posts the first update; a recovered check closes it and posts the closing update.

1.0 adds DNS, keyword and certificate checks, remote probe locations that vote on a result, latency charts, badges, RSS, a page per incident and an embeddable widget, subscriptions per service, monthly CSV and PDF reports, Prometheus metrics, a Cachet 2.x importer, four interface languages, remote backups and passkeys. The changelog has the details.

It is a PHP 8.3 application with SQLite or MySQL. Run it on compatible cPanel, DirectAdmin or Plesk hosting, another PHP host that meets the requirements, or in Docker. No daemon, no worker queue, no VPS.


Why it exists

Pharos brings automated incident publishing to compatible PHP shared hosting. It runs HTTP, TCP and heartbeat checks, groups affected components into one incident, and notifies customers. Host it separately from the services it watches so the status page survives their outage.

The API follows parts of Cachet 2.x's shape. Component updates accept the same status integers and token header; incident creation needs changes. See the compatibility guide before migrating.


What is in the box

Checks HTTP (with optional expected text and a warning before the certificate expires), TCP, DNS (A, AAAA, CNAME, MX, TXT) and heartbeat. Two failures turn a component red, three healthy checks in a row close the incident. Optional remote probe locations vote on the result, and each service keeps a 24 hour latency chart.
Incidents Opened and closed by the checks themselves, or by hand. One incident can span several components, each with its own status. Templates with {{variables}} for the API.
Uptime Daily roll-ups into a 90-day bar and a percentage. Pharos measures the services it checks itself and counts the status that the API or Uptime Kuma reports for the others (marked as reported); services set by hand are not measured. Days without data are grey and left out of the average, never counted as green.
Public page Every section is a switch (banner, uptime bar, services, per-component bars, incident history, empty days, API link), per-service visibility, light and dark theme, and a live preview in the admin that renders the real page from values you have not saved yet.
Sharing SVG status badges per service or group, an RSS feed, a page per incident and a status widget you embed with one script tag.
Subscribers A Get notified button, double opt-in, one e-mail per incident update, one-click unsubscribe. Visitors follow every service or pick the ones they care about. The four mails are editable Markdown templates.
Reports Monthly uptime per service as a page, CSV or PDF, public or for the team.
Integrations Cachet-shaped REST API for components and incidents, Uptime Kuma through a separately configured API workflow or heartbeat adapter, n8n in both directions with an HMAC-signed outgoing webhook, Zabbix and Grafana through the API, Slack and Discord webhooks, Telegram bot notifications, Signal through your own secured bridge, Prometheus metrics, and an importer for Cachet 2.x history.
Updates Signed release manifests (Ed25519), one-click install from the admin with an automatic backup, rollback, download and retention. Docker hosts pull the image instead.
Users Per-user TOTP two-factor with recovery codes, passkeys, OpenID Connect single sign-on, roles.
Languages English, Dutch, German and Spanish, chosen per account and per public page, including mail and dates.
Backups A backup before every update, and optional daily copies to your own S3 or SFTP storage with encrypted credentials.
Audit log Who changed what and when, filterable, exportable as CSV, with a configurable retention.
Time zone Everything stored in UTC, shown in the zone you pick; change it any time.

What it looks like

The public status page: overall headline, a 90 day uptime bar, an upcoming maintenance window, services grouped into rows, and incidents listed per day.

The public page. Every section on it is a switch on the Status page screen.

The Overview of one status page: a partial outage banner with 11 of 12 components operational, tiles for 90 day uptime, open incidents, the last 30 days and subscribers, the 90 day availability bar, services and the open incident with its timeline.

Overview. The first screen after sign in: what the public page says right now, the key figures, 90 days of availability and the open incident with its updates.

The Components screen grouped by service, with tiles showing what is down right now, average uptime, how many components Pharos checks and how many are set from outside.

Components. The tiles answer “what is wrong right now” before the table does, including how many components still rely on someone noticing.

The Layout screen with one switch per section on the left and a live preview of the public page on the right, with a desktop and phone toggle.

Status page. Tick a section off and it disappears from the preview beside it: the real page, rendered from values you have not saved yet.

The Incidents screen: open now, the last 30 days, typical time to resolve, the open incident with its timeline, and the history of resolved incidents.

Incidents. Each row says whether a check, the API or a person opened it.

The Scheduled maintenance screen: a window for web-03 and web-04 starting in one day, announced 24 hours before, and a history with a completed nameserver update.

Scheduled maintenance. Pick the components and the window; Pharos announces it ahead, sets them to Under maintenance at the start and puts them back at the end.

The Send out screen: add a destination in numbered steps (Slack, Microsoft Teams, Discord, Telegram, Signal or generic JSON), then the moments it receives, beside the list of destinations with their health.

Send out. Add a destination in a few steps, choose which moments it receives, and see at a glance which one is working, failing or paused.

The search palette opened over the admin with the query web: six components, one incident and a maintenance window, each with its page and status.

Search. Ctrl K or Cmd K finds pages, components, incidents, maintenance and admin screens, limited to the pages you hold a role on.

Several status pages

One installation can run more than one status page, each with its own services, subscribers, branding, email and integrations, and a role per page for every user.

The Status pages screen with three cards: Harbor Logistics and Northwind Hosting published, Northwind Internal unpublished, each with its address, 90 day uptime, open incidents, subscribers and assigned users.

Status pages. Each card shows the live status, 90 day uptime, open incidents and subscribers.

The public page of Harbor Logistics with its own logo and green accent: all systems operational, a scheduled driver app API migration, two services and the incident history.

A second public page with its own brand, next to the Northwind one above.

The Updates screen: installed version, available release, how this install updates, and the backups kept.

Updates. Signed releases, one click, a backup before anything is written, and roll back if you change your mind.


Install

Environment Guide Verify before use
DirectAdmin Web installation Web PHP, CLI PHP, permissions and cron
cPanel / CloudLinux Web installation PHP selector versus CLI binary and cron
Plesk (documented, not yet verified on a live Plesk server) Web installation Document root and scheduled task permissions
Docker Docker guide App and scheduler containers, volumes and proxy

cPanel and DirectAdmin are verified on live hosts. The Plesk paths are documented and still need a first confirmed run.

Discord setup · Telegram setup · Signal bridge setup · Update recovery

cPanel, DirectAdmin, Plesk, or compatible PHP 8.3 hosting

Requires PHP 8.3 or later with the usual Laravel extensions, and either SQLite or MySQL. No daemon, no worker queue, no root. Two installers, one result: the application lives outside the web root, only the public files are served by the web server, and one cron line runs every minute. Both download the same signed release and verify the Ed25519 manifest and the SHA-256 before unpacking. Rather do it yourself? See by hand.

1. Without SSH: the web installer

Download pharos-install.php and upload it into the domain's document root (File Manager → public_html, or the subdomain's folder). Open https://status.example.com/pharos-install.php. First, unlock it with the private installation key: the screen shows the file to open in your hosting File Manager. Keep the key for the administrator form. The installer contains its own logo and needs no external image files.

Step 1 of the web installer: where to find the private installation key, and the field to paste it.

After unlocking, the requirements screen checks whether the host can run Pharos:

Step 2, the server check: all 17 checks passed for PHP, the folders, the extensions, the release server and PHP for cron.

Download fetches the release, verifies the signed manifest and the checksum, unpacks the application into a private pharos-app-<domain-id> folder (next to the web root, never inside it) and copies public/ into the document root with an index.php that points at the app. No document-root change needed, and .env stays out of reach:

Step 3, the download: the signed release fetched, signature and checksum verified, every file unpacked and the public folder copied.

Configure asks for the site address and the database. SQLite needs nothing from your host; MySQL or MariaDB gets a Test connection button. Saving writes .env and sets up the database:

Step 4, configure: the site address detected from the page, SQLite recommended or MySQL or MariaDB, and the button Save settings and set up the database.

Scheduler shows the cron line, with the PHP binary that matches the version the installer found, and where it goes in cPanel, DirectAdmin and Plesk; it notices the first run by itself. Continue to your account deletes the installer and opens the setup form:

Step 5, the scheduler: one cron line with a Copy button, a confirmation that the scheduler ran, and where to add it in cPanel, DirectAdmin and Plesk.

The installer keeps whatever your panel already put in .htaccess and adds Pharos's rules underneath; a half-finished install resumes where it stopped.

2. With SSH: one command

curl -fsSL https://pharos.solutionmax.net/get | sh -s -- --php --url https://status.example.com

Same release, same checks. It installs into a private pharos-app-<domain-id> folder (--dir to change that), writes .env, migrates, links storage and adds the cron line to your crontab, or prints it where the crontab is not writable, as in DirectAdmin's jailed shell:

A terminal running the get script: system detected, manifest signature valid, PHP checked, archive downloaded and verified, unpacked, installation key shown, database migrated, cron task configured, then the document root instructions per panel.

Then point the document root at ~/pharos-app/public, the one step the script leaves to you:

  • cPanel: Domains → Manage → Document Root, or over SSH uapi SubDomain changedocroot domain=status.example.com docroot=pharos-app/public
  • DirectAdmin: Domain Setup if your host allows it; otherwise cd ~/domains/status.example.com && mv public_html public_html.orig && ln -s ~/pharos-app/public public_html
  • Plesk: Hosting Settings → Document root → pharos-app/public

Updating is the same command again; it hands over to Pharos's own updater, which backs the current version up first.

The setup form

Either way it ends here. The first visit shows one screen: name the status page, create your administrator using the installation key, done. The form disappears the moment that account exists.

Step 6, inside Pharos: installation key, status page name, time zone, your name, email and password.

If you would rather not touch a browser, php artisan pharos:user you@example.com creates the first account from the command line, and gets you back in if you ever lock yourself out.

The one cron line

Both installers give you the exact line; by hand, add it yourself. This is the entire scheduler:

* * * * * cd ~/pharos-app && php artisan schedule:run >/dev/null 2>&1

The redirect matters on cPanel and DirectAdmin: without it the panel mails you the scheduler's output every minute.

In a control panel's cron form the first five stars go in the schedule fields and the rest in the command field. If the host's default php is not 8.3, use the full path (/opt/alt/php83/usr/bin/php on cPanel, /usr/local/php83/bin/php on DirectAdmin, /opt/plesk/php/8.3/bin/php on Plesk). Pharos shows a warning in the admin until it sees the first run.

A specific version

Both installers take the newest release unless you pin one: --version 1.0.0 on the command, or pharos-install.php?version=1.0.0 in the browser. Every release on the releases page also has a pre-pinned pharos-install-<version>.php. Pharos never downgrades by itself; use Roll back under Updates.

By hand

git clone https://github.com/solutionmax/pharos.git ~/pharos && cd ~/pharos
composer install --no-dev --optimize-autoloader
cp .env.example .env && php artisan key:generate
touch database/database.sqlite
php artisan migrate --force && php artisan storage:link
# then point the document root at ~/pharos/public, as above, and add the cron line

Docker

The image is ghcr.io/solutionmax/pharos: one tag per release (:1.0.0) plus :latest, built for linux/amd64 and linux/arm64 by GitHub Actions on every release tag.

git clone https://github.com/solutionmax/pharos.git
cd pharos
cp .env.docker.example .env

# an application key is required; generate one and put it in .env as APP_KEY=
docker run --rm php:8.3-cli php -r "echo 'base64:'.base64_encode(random_bytes(32)).PHP_EOL;"

docker compose up -d

up pulls the tagged image, or builds it from this checkout when the registry cannot be reached. Two containers from one image: the application on php:8.3-apache (port 8080 by default, change PHAROS_PORT in .env), and a second one running php artisan schedule:work. The database is SQLite on a volume; point DB_CONNECTION at MySQL if you prefer.

If HTTPS ends at a reverse proxy, set APP_URL to the public HTTPS address and TRUSTED_PROXIES to that proxy's IP address or CIDR in .env. The proxy must send X-Forwarded-Proto: https and preserve the public host. Recreate the app container after changing these values so generated links and redirects use HTTPS. Leave TRUSTED_PROXIES empty when connecting directly; do not trust arbitrary forwarded headers on a publicly reachable backend.

Pin a release with PHAROS_VERSION=1.0.0 in .env. To build the image from your own checkout instead of pulling it, docker compose up -d --build; the build: block in compose.yaml stamps the same version into the image.

Updating is docker compose pull && docker compose up -d. Inside a container the app does not replace its own files: the Updates screen says the install is managed from the host and shows that command. There is no bundled host updater; the web interface cannot request a container restart. If PHAROS_VERSION is pinned in your Compose environment or .env, change it to the target release before running the command from the directory containing compose.yaml. The curl … /get | sh installer pulls the same image and only builds it from the release archive when the registry cannot be reached.

Check it is alive

php artisan pharos:check --force

Runs every enabled check immediately and prints what it found, instead of waiting for the scheduler.


How checks work

Type What it does Good for
HTTP Requests a URL, expects a status code and, if you set one, a piece of text in the response. Warns before the certificate expires Websites, APIs, control panels
TCP Opens a socket to host:port Mail, databases, anything without HTTP
DNS Resolves an A, AAAA, CNAME, MX or TXT record and compares the answer Domains, mail routing, records someone else can change
Heartbeat Waits for your job to call in; silence is the failure Backups, cron scripts, anything you cannot poll from outside

A check has to fail twice before the component goes red, and has to succeed three times in a row before the incident closes. That is deliberate: one dropped packet should not publish an outage.

Checks run from wherever Pharos runs. Add probe locations to have other Pharos installs run the same checks: a strict majority of locations decides, so one broken route does not publish an outage. See docs/monitoring.md.


Connecting it to what you already run

The API is Cachet-shaped: components, groups, incidents, maintenance windows and subscribers under /api/v1, the same {"data": …} envelope and status integers, and X-Cachet-Token accepted alongside Authorization: Bearer. Reading components and incidents needs no token. Not there: version, and Cachet's schedules path (maintenance windows live under /maintenance). Fields and limits are in docs/public-features.md.

curl -X POST https://status.example.com/api/v1/incidents \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "template":   "server-unreachable",
        "vars":       { "server": "web-06.example.net" },
        "status":     "investigating",
        "components": { "7": "major_outage" }
      }'
  • Uptime Kuma: connect a separately configured API workflow or periodic heartbeat adapter
  • n8n: both directions, with an HMAC-SHA256 signed outgoing webhook on every incident
  • Zabbix and Grafana: through the same API, no plugin needed
  • Telegram: bot notifications to a chat, group or channel; see Telegram setup
  • Slack: an incoming webhook per incident update; see docs/notifications.md
  • Prometheus: authenticated /metrics with status and uptime per service
  • Cachet 2.x: import groups, components, incidents and subscribers from a JSON export, with a preview first
  • Anything else: a token and a POST is the whole contract
  • Your visitors: a Get notified button on the status page; confirmed addresses get an e-mail per incident update, with one-click unsubscribe. See docs/subscribers.md

Checks, probe locations, web cron, remote backups and passkeys are covered in docs/monitoring.md; badges, the feed, the widget, reports, the API and the Cachet importer in docs/public-features.md. Single sign-on and two-factor are covered in docs/sso.md; how licence keys are issued and verified in docs/licensing.md.


Updates

Pharos checks for a signed release manifest and shows what is available under Updates, on every installation, paid or not.

On a PHP host it downloads the release, verifies the SHA-256, backs up the current version and replaces its own files, keeping .env, storage/ and the database. Roll back from the same screen. On Docker the host pulls the new image: docker compose pull && docker compose up -d.

Manifests are signed with Ed25519 and verified locally. If the release server cannot be reached, that reads as no update available, never as an error.

Every release (what changed, the signed zip, its SHA-256 and a copy of the web installer pinned to that version) is listed at pharos.solutionmax.net/releases. The GitHub Releases here carry the same files as a mirror.


Not implemented yet

Stated plainly rather than described as if it were finished:

  • Verified Plesk support. The procedure is written down in docs/hosting-package.md, but it has not been run on a live Plesk server yet. cPanel and DirectAdmin are the verified panels.
  • Packagist. Pharos is not published there. Install from the signed release zip, one of the installers or the Docker image.
  • Accounts for visitors. A status page is public or it is not; there is no sign in for the people who read it.

Licence

AGPL-3.0-only. See LICENSE.

In plain terms: anyone may use, modify and redistribute this, including commercially. If you modify it and run it as a service other people can reach, you have to publish your modified source. That is the difference between the AGPL and the GPL, and it is the point: it keeps a hosting company from taking this closed.

If that obligation does not work for your organisation, there is a commercial licence that lifts it. Buying one does not take Pharos away from anyone: every release stays published under the AGPL.

Contributions are welcome; see CONTRIBUTING.md, which includes the short contributor licence agreement that makes the dual licence possible.

Paid, and entirely optional

Free Every feature of the core, 1 status page, a "Powered by Pharos" footer credit.
Brand pack € 79, one time. Your own logo (light and dark), favicon, your logo in email and editable mail templates, the footer credit removed. Keeps working forever. 1 status page.
Supported € 149 per year. The Brand pack included and yours to keep, support by email, and up to 5 status pages, each with its own components, subscribers, branding, email, integrations and user roles per page.
Commercial licence From € 599 per year, quoted individually. Everything in Supported, unlimited status pages, and the AGPL publication requirement lifted.

If a yearly key lapses, the branding stays and existing status pages keep running, but no new or reactivated pages beyond 1 can be added.

None of it is required to run Pharos. Monitoring, incidents, notifications and updates work in the free version; the gated parts are the branding (logo, favicon, mail wording, credit) and status pages beyond the first. Prices are on pharos.solutionmax.net.

Licences are verified locally with an Ed25519 signature and may be tied to the domain of the installation given at checkout. Pharos never phones home to ask whether you are allowed to run it.


Pharos, a SolutionMAX product · pharos.solutionmax.net

Support the work

Pharos is built and maintained by SolutionMAX and given away. If it saved you an afternoon:

Buy me a coffee

Optional Signal bridge

docker/signal-compose.yaml provides a separate Signal bridge behind HTTPS and Bearer authentication. It exposes only POST /v2/send publicly; linking and account management remain on loopback port 58080. Use a dedicated host with ports 80/443 available, set its DNS, copy docker/signal.env.example to a private env file and set a random token (openssl rand -hex 32, protect the file with chmod 600). Then:

docker compose --env-file /private/path/signal.env -f docker/signal-compose.yaml up -d

Link your Signal device using the bridge's loopback API over an SSH tunnel, following the bridge's instructions. In Pharos Integrations choose Signal, enter https://your-bridge-host/v2/send, the linked sender number, recipient number or group ID, and the same Bearer token. Back up signal-state privately. This is an optional self-hosted bridge, not an integration provided or operated by Signal. Linking a real number and sending a test notification require your own account.

Hosting update and cron recovery

The installer can add cron through DirectAdmin or cPanel when your host permits API access. CloudLinux uses the surrounding hosting panel. Plesk users can use Scheduled Tasks, or execute pharos:cron --install with their CLI PHP if shell access permits it. A successful task save is followed by verification, but monitoring is only proven once the scheduler has actually run.

Updates track copied public files in the private storage manifest. Obsolete, unmodified release files are removed on update/rollback; local files and uploads remain. If an obsolete tracked file was edited, the update stops for review. Files copied before the first manifest are not guessed or deleted. PHP shell installs refuse pinned-version overwrites of existing installs; use Updates and its backups. MySQL backups remain external: take and verify a consistent database backup before updating, and restore it together with code when rolling back schema changes.

About

Self-hosted status page that runs its own HTTP, TCP, DNS and heartbeat checks. Probe locations, several status pages per install, monthly reports, passkeys. PHP 8.3, shared hosting or Docker.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages