cachethq/cachet
Cachet is a beautiful, self-hosted status page system built with Laravel. Create incidents, track component status, schedule maintenance, notify subscribers, and expose a JSON API to keep users informed during outages.
Read this first: Cachet is an application, not a library. You do not composer require cachethq/cachet into an existing Laravel app. It ships its own routes, models, migrations, and config, and it assumes it owns the whole Laravel bootstrapping. Treat it as a service you deploy next to your product — a Docker image, a separate host, or a subdomain behind your ingress.
Minimal path (Docker Compose, MySQL, queue worker included):
services:
cachet:
image: cachethq/docker:2.4 # pin the tag; see Gotchas
env_file: .env
ports: ["8080:8000"]
depends_on: [mysql, redis]
queue:
image: cachethq/docker:2.4
command: php artisan queue:work --tries=3
env_file: .env
depends_on: [mysql, redis]
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: cachet
MYSQL_ROOT_PASSWORD: secret
redis:
image: redis:7-alpine
.env essentials: APP_URL, APP_KEY (php artisan key:generate), APP_ENV=production, APP_DEBUG=false, DB_*, CACHE_DRIVER=redis, SESSION_DRIVER=redis, QUEUE_DRIVER=redis, and a working MAIL_* block. Then run migrations, seed the admin user, and change the default credentials immediately:
docker compose run --rm cachet php artisan migrate --force
docker compose run --rm cachet php artisan db:seed --force
Where to look first, in this order:
/api/v1/components and /api/v1/incidents — this is where 90% of your integration work happens.config/ (cachet.php, mail.php, database.php) and routes/api.php in the source tree.resources/views/ for branding, app/Http/Controllers/Api/ for the request/response contract.First use case — prove the credential, then open an incident:
curl -s -H "X-Cachet-Token: $CACHET_TOKEN" \
"$CACHET_URL/api/v1/components" | jq '.[] | {id, name, status}'
curl -s -X POST "$CACHET_URL/api/v1/incidents" \
-H "X-Cachet-Token: $CACHET_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "Elevated error rate on checkout",
"message": "We are investigating elevated 5xx responses.",
"status": 1,
"visible": 1,
"component_id": 3,
"component_status": 3,
"notify": true
}'
Public GET endpoints (components, incidents, metrics) work without a token, which is what makes the status page and any custom embed possible. Writes always need X-Cachet-Token.
1. CI/CD → incident lifecycle. The highest-value integration: your deploy pipeline opens an incident on failure and resolves it on success. Keep the incident ID so the resolution is deterministic.
# open (only if one isn't already open for this component)
ID=$(curl -s -X POST "$CACHET_URL/api/v1/incidents" \
-H "X-Cachet-Token: $CACHET_TOKEN" -H "Content-Type: application/json" \
-d "{\"name\":\"Deploy $GITHUB_SHA failed\",\"message\":\"$SUMMARY\",
\"status\":1,\"visible\":1,\"component_id\":$COMPONENT_ID,
\"component_status\":4,\"notify\":true}" | jq -r '.data.id')
echo "CACHET_INCIDENT_ID=$ID" >> "$GITHUB_ENV"
# resolve on the success path
curl -s -X PUT "$CACHET_URL/api/v1/incidents/$CACHET_INCIDENT_ID" \
-H "X-Cachet-Token: $CACHET_TOKEN" -H "Content-Type: application/json" \
-d '{"status":4,"component_status":1,"message":"Deploy recovered. All systems operational."}'
2. Monitoring/alerting → your enum mapping table. Cachet's numeric enums are the single biggest source of wrong-looking status pages. Put the mapping in one shared constant file and never inline integers again.
Incident status |
Meaning | Component status |
Meaning |
|---|---|---|---|
| 1 | Investigating | 1 | Operational |
| 2 | Identified | 2 | Performance Issues |
| 3 | Watching | 3 | Partial Outage |
| 4 | Fixed | 4 | Major Outage |
Scheduled-maintenance statuses are a different enum (0 Scheduled, 1 In Progress, 2 Complete) — don't reuse the incident one.
3. Alertmanager/webhook bridge. Cachet has no inbound webhook endpoint, so put a thin relay in front of it (a 20-line Lambda, a Laravel route in your app, or a Node worker). The relay owns the mapping "alertname → component_id", dedupes by fingerprint, and calls Cachet. Alertmanager's group_interval plus Cachet's create-on-every-POST means without dedupe you will spam subscribers.
4. Component status as a health mirror — but explicitly. A cron that runs GET /api/v1/components, compares against your own health checks, and PUTs changed components is a great lightweight uptime signal. Remember that component status and incident status are independent columns: closing an incident does not reset the component. Always send component_status: 1 on resolve.
5. Maintenance windows. POST /api/v1/schedules with name, message, scheduled_at, completed_at, status, and the component list. Create the schedule when you book the change, then PUT it to 1 (In Progress) at start and 2 (Complete) at end. This keeps planned downtime off the incident feed, which is exactly what on-call people want.
6. Metrics for uptime graphs. Create a metric once (POST /api/v1/metrics, {"name":"API Uptime","suffix":"%","default_view":1}), then push points from cron:
curl -s -X POST "$CACHET_URL/api/v1/metrics/$METRIC_ID/points" \
-H "X-Cachet-Token: $CACHET_TOKEN" -H "Content-Type: application/json" \
-d "{\"value\": 99.94, \"timestamp\": $(date +%s)}"
Points are append-only; there is no update endpoint, so compute correctly before pushing.
7. Subscribers and email. POST /api/v1/subscribers with {"email":"[email protected]"} triggers double opt-in. Keep the queue worker running and mail configured before you flip notify: true on any incident — synchronous mail on a large subscriber list will hang the request.
8. Custom embedding. No official badge widget, but unauthenticated GET /api/v1/components plus GET /api/v1/incidents?sort=created_at&order=desc gives you everything needed for a status pill in your own app, refreshed by a queued job with a short cache TTL.
9. Branding. Fork resources/views/ for layout changes and drop static assets in public/. Prefer CSS overrides in a custom view layer; patching PHP directly makes every upgrade a merge conflict.
10. Config as code. Bootstrap components via API in a seed script rather than clicking them in, so a rebuilt instance converges to the same state.
NOASSERTION. Read LICENSE in the repo before bundling or redistributing, especially for a commercial white-label status page. Don't assume MIT because it's a Laravel app.latest is how you get a Friday-night migration. Back up the database before any upgrade and read the changelog for migrations.APP_DEBUG=false in production. Cachet stack traces will happily print DB credentials and env vars on the public status page..env edits. php artisan config:clear && php artisan cache:clear (or rebuild the container) — otherwise mail/DB changes silently don't apply.MAIL_* is not an error, it's a silent drop. Incidents resolve fine, subscribers get nothing. Add a smoke test that subscribes a real address and checks delivery.Accept: application/json. Without it you can get HTML error pages back and your jq pipeline reports nulls instead of the actual validation message.visible is a separate flag from status. A resolved incident with visible: 0 disappears from the public page but stays in the DB — handy for internal-only blips, confusing when someone forgets to set it to 1.subscribers and metric points grow unboundedly. Prune stale unverified subscribers and roll up old points; a four-second status page is usually an unindexed table, not a PHP problem.storage/logs/laravel.log inside the container, php artisan route:list | grep api/v1 to confirm the endpoint actually exists in your pinned version, and a GET /api/v1/components with the token to separate auth failures from request-shape failures.How can I help you explore Laravel packages today?