github 0xfurai/peekaping 0.0.48

4 hours ago

Added

  • feat: NOTIFICATION_HISTORY_RETENTION_DAYS and TLS_INFO_RETENTION_DAYS set how many days the hourly cleanup keeps notification history (90 by default, at least 30, so the default certificate expiry notifications at 21 days aren't sent twice) and monitors' TLS information (30 by default, at least 1). They apply to the api and to peekaping, and a value below its minimum stops startup. Unset or 0, nothing changes. See Cleanup Configuration (#75)

  • feat: bun db import-mongo --mongo-uri <uri> [--mongo-db <name>] (or MONGO_URI and MONGO_DB in the environment, which keeps a password out of the shell history) moves a MongoDB install to SQLite or PostgreSQL. It applies the SQL migrations to the new database that DB_TYPE and the other DB_* variables name, then copies users (with their passwords and two-factor secrets), settings (including the keys that sign login tokens), API keys, proxies, monitors, notification channels, tags, maintenance windows, status pages with their domains, the links between them, heartbeats, stats, TLS information and notification history, in one transaction. It refuses a database that holds users, monitors or history of its own, and prints what it did with every collection, including what it skipped (such as links to deleted monitors) and what SQL has no place for (which monitors a status page hides). The monitors of a status page keep their order. It's in the single-mode image and in the migrate image. See Move to SQLite or PostgreSQL for the steps (#227)

    • Upgrade note: every row gets a UUID in place of its ObjectID. API key tokens and badge URLs that carry an old ID keep working, and so do logins and two-factor codes. Browsers that were signed in sign in once more, and scripts that put a monitor's ID in an API URL need the new ID.
    • Upgrade note: the login lockout counters (login_state) are not copied.
    • Upgrade note: open incidents in PagerDuty, PagerTree and Grafana OnCall won't resolve after the move, because their deduplication keys and fields carry the old monitor ID. Resolve them by hand. Webhook receivers that key on the monitor's id see the new ID, and links to /monitors/<id> in earlier Slack, Microsoft Teams and Google Chat messages no longer open the monitor.
    • Upgrade note: the source MongoDB must be 4.4 or later. On PostgreSQL, text wider than its column is cut and listed in the report, and two names that differ only after the cut get ~2, ~3 at the end, so no row is lost. SQLite keeps text whole. The escape \u0000 is removed from monitor and channel configs, which PostgreSQL rejects.
    • The target can be a database that Peekaping has started on once. It holds only the default settings that the first start writes, and the import replaces them with the install's settings, the login keys included. So a PostgreSQL that peekaping has started on is a target too.
  • docs: pages for Deployment modes, Upgrading to single mode and Kubernetes (Helm). They say what single mode and cluster mode run, how to take a 0.0.44 to 0.0.47 bundle to single mode and back, and how to install the chart, from the published chart or from a repository checkout. The Docker + SQLite and Docker + PostgreSQL pages link to the upgrade page instead of each keeping its own copy of the upgrade notes

  • feat: each release publishes the Helm chart at oci://ghcr.io/0xfurai/charts/peekaping, with the chart's version and appVersion set to the release, and every image tag defaults to the appVersion. So a chart version installs one consistent release, and you need no checkout and no helm dependency build: helm install peekaping oci://ghcr.io/0xfurai/charts/peekaping --version <X.Y.Z>. A component's image.tag still overrides the default. See Kubernetes (Helm) (#221)

    • Upgrade note: an install from a checkout still runs latest, and an image.tag you set still wins. A values file that pins all six tags can drop them when you install the published chart, because the chart version pins them. A tag left in stays on its release when you upgrade.
  • feat: Peekaping sends anonymous usage statistics once a day and error reports when its own code crashes. Both are on by default, and an admin can turn each off on its own: the setup screen starts with both boxes ticked, and Settings has a switch for each. The usage ping holds the version, mode, storage backend, operating system, a range for the number of monitors and the kinds of monitors and notification channels in use. A report holds the error type, the stack frames, the version, mode, storage backend and operating system. Neither holds names, URLs, hosts, settings, error messages or IP addresses. Every field is listed on the page above, and the Settings card shows the exact JSON of the next usage ping.

    • Upgrade note: an install that upgrades starts sending each one for which no choice is stored. A choice an admin stored before, such as an opt-out, stays in effect: the new default applies only where nothing is stored. The admin sees a one-time notice in the app that says what is on, names what each sends and never sends, and links to the Settings cards. Dismissing it, or changing either card, stops it. It does not show when both are switched off by the environment.
    • To send neither, set both TELEMETRY=off and ERROR_REPORTING=off. TELEMETRY=off switches off only the usage statistics and ERROR_REPORTING=off only the error reports, each whatever an admin chose. In cluster mode, set ERROR_REPORTING on the API.
    • The dev compose files in this repository point both at an address nothing listens on, so a development stack sends nothing.
  • feat: remote probes run a monitor's checks from another machine. Create a probe on the new Probes page, start the peekaping-probe image (0xfurai/peekaping-probe, also on GHCR) with its token, and choose the probe in the monitor's form. The probe connects out to the API over HTTP(S), runs the checks assigned to it and sends the results back as heartbeats. A monitor can run on several probes: each probe's results are a location on the monitor page, with its own status, response time and heartbeat bar, and by default the monitor is Down while any online location is Down. A status page can show each location. Several instances can share one token, and the probe's monitors are dealt among them. A probe that goes silent is marked offline after PROBE_OFFLINE_AFTER (3 minutes by default), the probe's own notification channels are told, and its monitors are not marked Down. The Docker Compose nginx gateway, the single-mode bundles on port 8383 and the Helm chart's Ingress and Istio VirtualService route /probe/ to the API. See Remote probes (#31, #32, #33, #34, #252, #36, #257, #253, #452, #453, #456, #457, #458, #459, #465, #466, #469, #470, #471, #476, #477, #478, #479, #486, #487)

    • Upgrade note: nothing changes for an existing install. The migrations add the probe tables, put every monitor on the local probe, and record the probe on heartbeats and stats; the heartbeats and stats you have read as the local probe's.
    • Upgrade note: with ingress-nginx, set nginx.ingress.kubernetes.io/proxy-body-size: "2m" in ingress.annotations before you use a remote probe, because a probe's batch of results can pass the controller's default limit of 1 MiB. A reverse proxy of your own in front of port 8383 has to pass /probe/, and allow 2 MiB bodies.
  • feat: a monitor on several probes has a location rule that says when it goes Down: when any location is Down (the default, as before), when all of them are, or when at least N are. Locations whose probe is offline, or that haven't reported yet, don't count, so "all", or "at least" with N of 2 or more, keeps one probe's network trouble from raising an alarm while the other probes report. Choose it in the monitor's form once the monitor runs on more than one probe. Its Down and Up notifications name the locations, such as "Down at Frankfurt, Tokyo (2 of 3 locations)", and custom templates get locations_down and locations_total. See Remote probes (#35, #488, #489)

    • Upgrade note: every existing monitor gets the any rule, so it goes Down as it did before. A migration adds the rule's two columns to monitors.
  • feat: the remote probe has a slim image, 0xfurai/peekaping-probe:slim (or <version>-slim, also on GHCR), about 40% smaller than the full one. It leaves out the Docker, gRPC keyword, Kafka producer, MongoDB and SQL Server monitor types, tells Peekaping so when it connects, and so can't be given a monitor of one of those types. See The slim image (#264)

  • feat: the full and the slim image can run under one probe token. Peekaping keeps the monitor types of each probe instance: a monitor is dealt only to the online instances that run its type, so a Docker monitor goes to a full instance and never to a slim one, and an instance can't send results for a type it doesn't run. The probe's types, which the monitor form and the API check a monitor against, are those of its online instances together, so a slim instance saying hello no longer takes Docker away from the probe. The Probes API shows each instance's supported_types. See Full and slim instances under one token (#264)

    • Upgrade note: a migration adds supported_types to probe_instances and gives each existing instance the types of its probe, so nothing changes for a probe on one build.
  • feat: probe availability. The Probes page shows each probe's online and offline time over the last 24 hours as a bar with its uptime, and the probe's name opens its own page, with its history over 24 hours, 7 days or 30 days and a bar for each instance's connections over the last 24 hours. The API serves it at GET /api/v1/probes/availability and GET /api/v1/probes/{id}/availability. In single mode the local probe is the peekaping process itself and has no history (PP-330)

    • Upgrade note: migrations add the tables probe_status_changes and probe_instance_connections and start each existing probe's history with its status at the upgrade. The hourly cleanup deletes status changes older than the data retention period.
  • feat: Create probe has an "Assign to all monitors" box, ticked by default: when the probe's first instance connects, the probe is added to every monitor of a type it runs. The Probes page has the same action for a probe that exists. A new monitor now starts on every enabled probe that runs its type, not only on the local probe; a cloned or edited monitor keeps its probes. See Remote probes (PP-324)

    • Upgrade note: on an install with remote probes, a new monitor runs on several locations under the default "any" location rule, so one probe's network trouble takes it Down. Choose "at least 2" in the monitor's form for three or more probes. A migration adds assign_all_pending to probes, false for every existing probe.
  • feat: each heartbeat records the probe instance that ran the check, and the heartbeat tooltips and the important heartbeats list on the monitor page show "Instance:" and the instance's hostname. The heartbeat API, socket events and webhook payloads carry a nullable instance_id (PP-331)

    • Upgrade note: a migration adds the nullable column heartbeats.instance_id, empty for earlier heartbeats. It doesn't rewrite the table.
  • feat: the CA is optional for mTLS on HTTP monitors. Without one, the check still sends the client certificate and verifies the server's certificate against the system's trusted roots. With one, nothing changes (#217)

  • feat: every HTTP request gets an X-Request-ID response header (a valid one the client sends is kept) and one structured log line in every mode, not only with MODE=dev. The line names the route pattern, not the path, so push tokens no longer reach the log. Health and metrics requests log at debug level (#241)

  • feat: SQLITE_STARTUP_CHECK chooses the SQLite check at startup: quick (the default), full or off. Cluster mode on SQLite logs a warning that single mode or PostgreSQL fits better, and the Helm chart refuses DB_TYPE: sqlite, which gave each pod a database of its own (#240)

  • feat: the monitor list and the monitor page mark a probe that doesn't run the monitor's type, and the stale-monitor notification says why, such as "probe Frankfurt doesn't run docker monitors" (#263)

  • feat: the version in the sidebar links to its GitHub release (#269)

Changed

  • feat: the docs at docs.peekaping.com match the landing page: dark by default with violet glows and the Geist font, a frosted navbar with the mascot, restyled sidebar, code blocks, tables and admonitions, and a welcome page with a hero, a copyable docker run and link cards. Bash, nginx, HTTP, PHP and Dockerfile code blocks are now highlighted

  • feat: the landing page at peekaping.com has a new dark design: a hero with a live heartbeat card and a copyable docker run, a features grid, a quickstart terminal for SQLite and PostgreSQL, and monitor and alert channel lists that match what the server runs (MySQL/MariaDB, SendGrid, Twilio, LINE, PagerTree and Pushbullet were missing). The testimonials no longer repeat

  • feat: the single-mode image runs Peekaping as one process, peekaping, which serves the web app, the API and the websocket on port 8383, with no Redis, Caddy or supervisord. The image now runs on Alpine. Port 8383, the data volume and your environment variables stay the same. There is no PostgreSQL bundle any more: PostgreSQL runs in a container of its own (see below). peekaping runs as the non-root peekaping user, and PUID, PGID, the Docker socket's group and the ping_group_range warning work as before. The image can also start with docker run --user, and it has a Docker health check. There is no MongoDB bundle any more (see Removed). The image, measured against 0.0.47:

    0.0.47 Now
    Image size 417.5 MB 78.6 MB
    RAM after 2 minutes idle 92.7 MiB 19.2 MiB
    RAM with 100 HTTP monitors checked every 20 seconds, after 5 minutes 131.2 MiB 36.6 MiB

    See Upgrading to single mode, Docker + SQLite, Docker + PostgreSQL and Single-mode user and permissions (#163, #209, #376) (thanks @tafaust)

    • Upgrade note, SQLite bundle: pull the new tag and start it with the same volume. Migrations run at start, as before. Redis and its data are gone; Redis held only the schedule and queued checks, which peekaping rebuilds from the database at start.
    • Upgrade note: a custom Caddyfile mounted at /etc/caddy/Caddyfile no longer works, because the image has no Caddy. Put your own reverse proxy (Caddy, nginx, Traefik) in front of port 8383 instead. The login lockout counts failed logins per client address, and the Caddy inside the image used to set X-Forwarded-For for it. Behind your own proxy, make it put the client's address last in X-Forwarded-For (nginx: proxy_set_header X-Forwarded-For $remote_addr;, because plain proxy_pass forwards the client's own header) and set TRUSTED_PROXIES to the address it connects from, so the API believes that header. Until you do, every login counts as coming from the proxy.
    • Upgrade note: port 8383 serves everything, and the image ignores SERVER_PORT. A .env that sets it, such as SERVER_PORT=8034 from the examples, keeps working. To use another host port, change the published port, for example -p 8080:8383.
    • Upgrade note: MODE from your environment now applies, with prod as the default. Before, the bundles always ran with prod. With MODE=dev, peekaping logs in the development format and logs every request.
    • Upgrade note: API_URL from your environment now reaches the web app, in the /env.js that peekaping serves. Before, the bundles always gave the web app an empty API_URL, which means the API on the web app's own origin. If your environment or .env sets API_URL, unset it, unless the web app must call an API on another origin.
    • Upgrade note: peekaping takes up to 30 seconds to shut down cleanly. docker stop waits only 10 seconds by default, so use docker stop -t 35, or stop_grace_period: 35s in Docker Compose.
    • Upgrade note: with METRICS_ENABLED=true, port 8383 now serves /metrics, so anyone who can open the web app can read it. Before, Caddy didn't forward it.
    • Upgrade note: logs go to the container's output (docker logs). Nothing writes to /var/log/supervisor now, so a volume mounted there can go.
  • chore: for development, docker-compose.deps.yml with --profile postgres replaces docker-compose.sqlite.yml, docker-compose.postgres.yml and docker-compose.mongo.yml. make docker-sqlite and make docker-postgres keep working

  • chore: the Helm chart's bundled PostgreSQL and Redis (off by default) run pinned images, PostgreSQL 18.6 and Redis 8.10.2, instead of Bitnami's floating latest, so a rescheduled pod can't start a new major version on old data. The Bitnami subcharts move to postgresql 18.12.14 and redis 28.3.1, and scripts/update-chart-deps.sh checks and updates both. The migrate Job now tries up to 6 times (migrate.backoffLimit), enough to outlast a restart of the bundled PostgreSQL during an upgrade

    • Upgrade note: with the bundled databases on, the upgrade restarts PostgreSQL and Redis on the pinned images. The data stays, since the major versions stay 18 and 8.
  • feat: the images of cluster mode run as a non-root user. The api, producer, ingester and migrate images run as 65532:65532, like the worker, and the web image runs nginx as the user nginx (UID 101) on nginxinc/nginx-unprivileged, still on port 80. The SQLite compose files (docker-compose.prod.sqlite.yml, docker-compose.dev.sqlite.yml and the one in Docker + SQLite) get a one-shot data-permissions service that makes ./.data/sqlite writable by 65532 before migrate starts, on a fresh checkout and on a database that root created. The Helm chart's web pods set the sysctl net.ipv4.ip_unprivileged_port_start to 0 (web.sysctls), so nginx binds port 80 on any container runtime, and mount an emptyDir at /tmp instead of /var/cache/nginx and /var/run. Cluster user and permissions says what the SQLite folder, Docker monitors and ping monitors need (#175)

    • Upgrade note: if you run SQLite in cluster mode from your own compose file, the database in your data folder belongs to root, and the new images can't write to it: migrate stops with unable to open database file. Give the folder to 65532 once with sudo chown -R 65532:65532 .data/sqlite, or take the new compose file, or add its data-permissions service to yours and make migrate wait for it (mount a named volume in that service too). PostgreSQL installs need nothing.
    • Upgrade note: Docker lets the unprivileged web image bind port 80. On a runtime that doesn't, the web container stops with bind() to 0.0.0.0:80 failed (13: Permission denied): add sysctls: {net.ipv4.ip_unprivileged_port_start: "0"} to the service. The Helm chart sets it for you.
  • feat: cluster mode on SQLite and PostgreSQL runs the api and peekaping-probe instead of the api, producer, worker and ingester: two processes fewer, and Redis only between the api replicas. The local probe's monitors are checked by peekaping-probe instances that hold the local probe's token (PROBE_BOOTSTRAP_TOKEN on the api, PROBE_TOKEN with the same value on the probe, PEEKAPING_URL pointing at the api). The api stores what it takes, a push to a push URL or a probe's results, on the replica that took the request, one monitor's results at a time and in order, and the stats of a heartbeat are counted once, by the replica that stored it. A batch a probe retries on another replica within the hour is answered as a duplicate, through a window the replicas share in Redis. The replica that holds the leader lock (the Redis key peekaping:producer:leader) runs the stale-monitor watchdog, the probe offline sweep and the push monitors' checks, and another replica takes over within about 15 seconds after a hard kill, and at once after a graceful stop. In cluster mode the local probe is online while one of its instances is connected, like a remote probe, so a stopped probe deployment raises the probe offline notification; if a cluster runs with no instance at all, the leader reports the local probe offline once, after PROBE_OFFLINE_AFTER (3 minutes by default, now read by the api), and logs the local probe has N active monitors and no instance: deploy peekaping-probe with PROBE_TOKEN set to PROBE_BOOTSTRAP_TOKEN. To check more monitors, add probe replicas. The producer, worker and ingester are removed in this release (see Removed). Single mode doesn't change. MongoDB installs move to SQLite or PostgreSQL with bun db import-mongo first (see Removed). The Helm chart's producer, worker and ingester values become probe.*, with a Secret <release>-probe-token that the chart makes and keeps across upgrades, or probe.token.existingSecret and probe.token.existingSecretKey for one of your own. The prod and dev Docker Compose stacks for SQLite and PostgreSQL get a probe service in place of the producer, worker and ingester, and stop at start until PROBE_BOOTSTRAP_TOKEN is in .env. make dev runs the api, a probe and the docs watcher. One api stores about 2,000 probe results a second on PostgreSQL 17 and on SQLite, where the ingester queue stored about 1,350 a second on PostgreSQL; a soak of 3 probe instances and 1,000 monitors at 20 seconds, with one instance stopped, missed no check and kept the api's heap at 14 to 17 MiB. See Upgrading to probes, Probe, Deployment modes and ADR 0002 (#254)

    • Upgrade note, Docker Compose on SQLite or PostgreSQL: add PROBE_BOOTSTRAP_TOKEN=<the output of openssl rand -hex 32> to .env (at least 32 characters), take the new compose file from Docker + SQLite or Docker + PostgreSQL, and run docker compose pull and docker compose up -d --remove-orphans. The probe service replaces the producer, worker and ingester. If you keep your own compose file, delete those three services, add the probe service from Upgrading to probes, and move the Docker socket mount, group_add and ping sysctls of the worker to it. Run docker compose up -d --scale probe=3 to check more monitors.
    • Upgrade note, Helm: remove the producer, worker and ingester blocks from your values file and set what they held on probe (probe.replicaCount, probe.resources, and probe.sysctls in place of worker.sysctls); a leftover block is ignored. helm upgrade creates the <release>-probe-token Secret with a random token and keeps it from then on. If you render the chart without the cluster (helm template, Argo CD), make a Secret with a token of at least 32 characters and name it in probe.token.existingSecret, or each render makes a new token.
    • Upgrade note: the order is to set the token, start the probes, upgrade the api, and remove the producer, worker and ingester. The compose files and the chart do it in one command, so no monitor goes unchecked for longer than the probes take to connect. A monitor goes stale only after three missed intervals.
    • Upgrade note: a cluster that runs with no probe instance has no checker for the local probe's monitors, so the leader reports the local probe offline once after PROBE_OFFLINE_AFTER and says what to deploy. Push monitors need no instance.
    • Upgrade note: the watchdog can't make a probe check again, so a stale monitor in this layout gets its notification once the wait has passed, without the "checking again" step. A monitor whose probe is offline is skipped, so a stopped probe deployment gives one notification, not one for each monitor.
    • Upgrade note: set PROBE_OFFLINE_AFTER and ERROR_REPORTING on the api replicas. The producer isn't there to read them any more, and the probe sends no error reports. QUEUE_CONCURRENCY and PRODUCER_CONCURRENCY no longer size the checks: PROBE_CONCURRENCY (128 by default) does, for each probe instance.
    • Upgrade note: MongoDB installs don't upgrade this way. They move to SQLite or PostgreSQL with bun db import-mongo first (see Removed).
    • Rollback: put back the previous compose file or run helm rollback, with the previous image tags. The upgrade adds no migration, so nothing in the database changes and there is nothing to restore. The local probe's token stays in the database and is harmless. If you came from a release without remote probes, restore your backup, because that upgrade also ran the migrations that add them.
  • chore: the compose files, Dockerfiles and scripts in the repository root are named after the mode they run (PP-317). docker-compose.prod.<db>.yml is now docker-compose.cluster.<db>.yml, docker-compose.bundle.<db>.yml is docker-compose.single.<db>.yml, Dockerfile.bundle.sqlite is Dockerfile.single (the PostgreSQL one is gone, see below), startup.bundle.sqlite.sh and startup.bundle.user.sh are startup.single.sh and startup.single.user.sh, and scripts/test-bundle-user.sh is scripts/test-single-user.sh. The make targets follow: make docker-prod-postgres, make docker-prod-sqlite, make down-prod-postgres and make down-prod-sqlite are now make docker-cluster-postgres, make docker-cluster-sqlite, make down-cluster-postgres and make down-cluster-sqlite.

    • Upgrade note: this matters only if you run the compose files or make targets from a clone of the repository. After a git pull, use the new paths in your docker compose -f commands and scripts. A stack that you started from the old file keeps running, and the new file manages it, because Compose names the project after the directory, not the file.
  • feat: single mode's PostgreSQL runs in its own container (PP-317). There is one single-mode image, built from Dockerfile.single, and it runs peekaping alone. With DB_TYPE unset or sqlite it keeps its data in /app/data/peekaping.db, as before. With DB_TYPE=postgres it connects to the PostgreSQL that DB_HOST, DB_PORT, DB_USER, DB_PASS and DB_NAME name, and DB_HOST is required. Any other DB_TYPE stops the container with a message. docker-compose.single.postgres.yml runs the image beside the official postgres:17 image, with a healthcheck, depends_on with service_healthy and the data in ./.data/postgres17, a folder of its own, so that the repository's single PostgreSQL Compose file never starts postgres:17 on the old bundle's cluster in .data/postgres. docker-compose.single.sqlite.yml sets DB_TYPE=sqlite itself, so a .env shared with cluster mode can't switch it. Dockerfile.single.postgres and startup.single.postgres.sh are gone, and so is the peekaping-bundle-postgres image from the release workflows, the e2e jobs and the release candidate smoke test, which run the PostgreSQL case as the single image beside a postgres:17 container. See Docker + PostgreSQL and ADR 0002.

    • Upgrade note: peekaping-bundle-postgres gets no more tags. 0.0.47 is its last, and it stays on the registries, so a running install keeps working and a pull of that tag still works. To upgrade, move to the Compose layout with pg_dump and pg_restore, as Moving from the PostgreSQL bundle says. The new stack keeps its data in .data/postgres17 and leaves the bundle's .data/postgres as it is, so you can go back by starting the old container. Don't point the postgres image at the bundle's folder: its files belong to another user and were written on an older Debian release.
    • Upgrade note: a single-mode container now reads DB_TYPE. Unset or sqlite keeps SQLite, and DB_NAME is ignored, as before. postgres connects out to the server DB_HOST names. Any other value stops startup with a message, including postgresql, mysql and DB_TYPE=mongo.
    • Upgrade note: PostgreSQL's major upgrades go through the official image now, and single mode works with any PostgreSQL, a managed one included.
  • feat: the single-mode image is now published as 0xfurai/peekaping (and ghcr.io/0xfurai/peekaping), with the tags latest and <version>, for amd64 and arm64 (PP-321). It was peekaping-bundle-sqlite. DB_TYPE picks SQLite or PostgreSQL, as before.

    • Upgrade note: in docker run, replace 0xfurai/peekaping-bundle-sqlite:latest with 0xfurai/peekaping:latest. In a compose file, change the image: line the same way. The port, the data volume and the environment variables stay the same, so nothing else changes.
    • Upgrade note: peekaping-bundle-sqlite gets the same tags in 0.0.48 and 0.0.49, and none from 0.0.50. A setup that keeps the old name stops getting updates then.
  • feat: the SQLite startup check runs PRAGMA quick_check instead of the full integrity_check, about 2 seconds instead of 30 on a 1.5 GB database, and the shutdown no longer runs a check at all. When the check finds damage, the process logs the findings and stops, instead of writing to a damaged database. See SQLite recovery (#231, #416)

    • Upgrade note: an install that runs on a corrupt SQLite database today, logging Database integrity check FAILED and carrying on, stops at startup after the upgrade, and a container with a restart policy keeps restarting. Follow the recovery page, or set SQLITE_STARTUP_CHECK=off while you do.
  • feat: a probe with no notification channels of its own sends its offline and online notifications to the channels of its active monitors, each channel once. Before, it sent nothing, and its monitors went unchecked in silence. Create probe no longer asks for channels; Edit probe keeps them (PP-329, #549)

    • Upgrade note: installs whose probes have no channels start getting probe offline and online notifications on their monitors' channels. In cluster mode the local probe's offline notification reaches every channel of its monitors.
  • feat: a probe holds at most 64 instances. A new instance at the cap takes the place of the oldest one that isn't online; when all 64 are online, the API answers 429 and the probe tries again after a minute (#258)

  • feat: the setup screen asks for usage statistics and error reports with two ticked boxes and one short note that links to what's sent, instead of thirty lines of small print (PP-328)

  • feat: the Type select of a notification channel lists the channel types alphabetically (#268)

Removed

  • chore!: remove MongoDB as a storage backend (the database Peekaping keeps its data in). DB_TYPE=mongo and DB_TYPE=mongodb are gone, with the Mongo repositories, the peekaping-bundle-mongo image, the MongoDB Docker Compose files (docker-compose.bundle.mongo.yml, docker-compose.dev.mongo.yml and docker-compose.prod.mongo.yml), the mongo profile of docker-compose.deps.yml and the make targets for them. Move off MongoDB storage is now only the guide for moving to SQLite or PostgreSQL. The MongoDB monitor type, which checks other MongoDB servers, is not affected, and neither is bun db import-mongo (PP-277)

    • Upgrade note: a MongoDB install has to move to SQLite or PostgreSQL with bun db import-mongo from this release before it starts on SQL, and it stays on 0.0.47 until then. The command is in the migrate image and in the single-mode image, and the guide has the steps for the MongoDB bundle, the Docker Compose stack and a server of your own. 0.0.47 is the last release that runs on MongoDB. The import reads a copy of your MongoDB data folder and changes nothing in it, so keep the folder until you are sure you don't need it.
    • Upgrade note: with DB_TYPE=mongo or DB_TYPE=mongodb, every Peekaping binary (the api, peekaping and bun) stops at startup with an error that says MongoDB storage was removed and gives the address of the guide, and so does the migrate image. The Helm chart refuses to render with either value. None of them writes to the database. A Docker Compose stack for MongoDB that pulls latest stops at the next pull, so pin its image tags to 0.0.47 until you have moved.
    • Upgrade note: peekaping-bundle-mongo gets no new tags. The images already published stay where they are.
  • chore!: remove the producer, worker and ingester, which stayed only for MongoDB storage. On SQLite and PostgreSQL, cluster mode moves to probes in this release and they have nothing left to do (see the #254 entry above). The cmd/producer, cmd/worker and cmd/ingester programs, the peekaping-producer, peekaping-worker and peekaping-ingester images, the queue-based check and ingest dispatch (the monitor:healthcheck and monitor:ingest queues), their make targets and dev:* scripts and their three architecture pages are gone. Cluster mode is the api and peekaping-probe instances. The notification queue stays, and so does the leader lock key peekaping:producer:leader, so replicas of an earlier release and of this one still share one leader during a rolling upgrade. The api no longer copies the choice to send error reports to Redis, because nothing else reads it: each api replica follows the stored choice and applies a change made on another replica within a minute. See Upgrading to probes and ADR 0002 (PP-277)

    • Upgrade note: the peekaping-producer, peekaping-worker and peekaping-ingester images get no new tags. 0.0.47 is the last release that has them, and the images already published stay where they are. On SQLite and PostgreSQL their work moves to the probes (see the #254 entry).
    • Upgrade note: a cluster that still runs them removes those services. For Docker Compose, delete the producer, worker and ingester services and add the probe service. For Helm, delete the producer, worker and ingester blocks from your values file. Upgrading to probes has the steps. A stack that pulls latest keeps getting the last images published, not new ones.
    • Upgrade note: PRODUCER_CONCURRENCY is no longer read, so remove it from your .env. QUEUE_CONCURRENCY stays and now sizes only the api's notification queue. PROBE_CONCURRENCY sets how many checks a probe instance runs at once.

Fixed

  • fix: the monitor list, batch, create and update responses now include notification_ids and tag_ids, like GET /monitors/{id}, which now also returns push_token (#191)
  • fix: in cluster mode, the hourly cleanup (heartbeats past the data retention period, expired stats, old notification history, old TLS information) ran on every api replica at the same time. It now runs once an hour however many replicas run: the replicas claim the hour with a Redis key (peekaping:cleanup:<hour>, kept for two hours, where the hour is the time rounded to the nearest hour in UTC, so replicas whose clocks are less than an hour apart share one claim), and only the replica that wins logs Running the hourly cleanup. The others log at debug level that another replica has it. A replica that can't reach Redis when the hour starts logs an error and skips that hour, and the next hour tries again. In single mode, peekaping runs the cleanup every hour in its process, with no Redis. The four cleanups are one job now, so they run one after the other, and the cron fires at the top of the UTC hour, which only moves the time in time zones offset by part of an hour. A failure to schedule the cleanup used to be ignored; it now stops startup with an error (#75)
  • fix: security: the API lets a page read its responses only from the origins in the new CORS_ORIGINS setting. Before, it answered every origin with Access-Control-Allow-Origin: *, so a page on any site could read them. CORS_ORIGINS is a comma-separated list of origins, such as https://status.example.com,http://localhost:8383. Unset, it is the origin of CLIENT_URL, and * alone allows any origin. A request from another origin is still served; its browser just won't hand the response to the page. Responses no longer carry Access-Control-Allow-Credentials, which the API never needed because its token travels in the Authorization header. An entry that isn't an origin stops startup, and /metrics stays unreadable from another origin. See API Server (#55)
    • Upgrade note: a setup where a page on an origin other than CLIENT_URL's calls the API must set CORS_ORIGINS to that origin, or the browser refuses it. The web app the bundles, the cluster gateway and the Helm chart serve calls the API on its own origin and needs nothing. The dev Compose files set CORS_ORIGINS to http://localhost:8383 for the web container.
  • fix: security: the login lockout no longer trusts a client's X-Forwarded-For. It counted failed logins under the leftmost address in that header, so a client could send a different one with every request and never be locked out, in every deployment. The API now believes X-Forwarded-For and X-Real-IP only from the proxies in the new TRUSTED_PROXIES setting (comma-separated IP addresses and CIDR ranges; its own default is empty, which believes no header, as in the single-mode bundles, while the cluster compose files and the Helm chart set loopback and the private ranges when it is unset, and an explicitly empty value there trusts nobody). For a request from one of them, the client is the last X-Forwarded-For address that isn't a trusted proxy (the leftmost when all are), or the X-Real-IP when there is no X-Forwarded-For; an X-Forwarded-For it can't read makes the client the proxy's own address. A request from any other address counts as coming from the address it connected from. A second counter now locks an account whatever addresses the guesses come from: BRUTEFORCE_ACCOUNT_MAX_ATTEMPTS failed logins (100 by default) within BRUTEFORCE_ACCOUNT_WINDOW (10 minutes) lock the account for BRUTEFORCE_ACCOUNT_LOCKOUT (1 minute). The count of failures is kept until the window, measured from the first failure, has passed, so until then each further failure after a lock ends locks the account again: guessing is bounded to the limit per window plus one attempt each time a lock ends, and after an attack stops the owner is back in within the window at most. An attacker who keeps going can keep the account locked. A successful login resets both counters. A login request body over 64 KiB is refused with 413. The cluster compose files and the Helm chart set TRUSTED_PROXIES for their gateway, and the cluster nginx.conf now replaces the client's X-Forwarded-For with its address. See Reverse proxy and HTTPS (#50)
    • Upgrade note: if you run Peekaping behind your own proxy, make the proxy put the client's address last in X-Forwarded-For (Caddy and Traefik do; plain nginx proxy_pass forwards the client's own header, so add proxy_set_header X-Forwarded-For $remote_addr;) and set TRUSTED_PROXIES to the address the proxy connects from. For a proxy on the same Docker host, publish port 8383 on loopback (-p 127.0.0.1:8383:8383) and use the gateway of Docker's network, such as TRUSTED_PROXIES=172.17.0.1; don't trust that address while 8383 is published on all interfaces, because direct connections can arrive from it too. Until you do, the API sees every login as coming from the proxy, so one person's failed logins can lock an account for everyone. Installs with no proxy in front, and the cluster compose files and the Helm chart as shipped, need no change. A cluster install with its own nginx.conf needs proxy_set_header X-Forwarded-For $remote_addr; in the /api/ and /socket.io/ locations, as the new nginx.conf has. An Ingress that passes the client's X-Forwarded-For through unchanged needs config.env.TRUSTED_PROXIES: "" in the Helm values. A malformed TRUSTED_PROXIES stops the API at start.
  • fix: security: login tokens and API keys are verified more strictly (#54).
    • Login tokens are accepted only when signed with HS256. The signing secrets stay in memory and are read from the database again after a minute, instead of on every request, so a secret changed in the database is used within a minute.
    • API keys are checked with SHA-256 and a constant-time compare, which cost far less than bcrypt on every request. A key created before this change keeps working, and its stored hash is replaced on its first use.
    • A key with a usage limit is never accepted more often than its limit, also when requests arrive together: checking the limit and counting the use are one database update.
    • A refresh token issued before a password change no longer works, so the sessions signed in with the old password can't renew. An access token already issued stays valid until it expires, within 15 minutes by default.
    • Registrations that arrive together on a new install create one admin. The others fail with "admin already exists".
    • The websocket reads the access token from the socket.io auth payload. A token in the query string is no longer accepted, so it stops showing up in a proxy's access logs. The web app sends it that way.
    • Upgrade note: every user signs in again once, because refresh tokens issued by earlier releases are rejected.
    • Upgrade note: after its first use, an API key is stored as a SHA-256 hash, which earlier releases can't check. After a rollback, recreate the keys used since the upgrade.
    • Upgrade note: a script or tool that opens the websocket itself must pass the token as auth: { token } instead of query: { token }.
  • fix: creating a monitor, notification channel or maintenance window with active: false now stores and returns it as inactive. Bun replaced the false value with the column's true default on insert, so creates always came back active; editing afterwards worked. Omitting active when creating still produces an active resource (#394)
  • fix: the Helm chart's web pods copy the web app from the web image you set. Before, an init container always used 0xfurai/peekaping-web:latest, so pinning web.image.tag still served the latest web app. The chart's install notes now say it runs cluster mode and link to the Kubernetes (Helm) docs page
  • fix: on a fresh Helm install, the api, producer, worker and ingester pods no longer crash-loop until the migrate Job has created the database tables. They wait in an init container, wait-for-migrations, until the Job has applied every migration. On an upgrade, the new pods wait for the new release's migrations while the old pods keep serving
  • fix: in the PostgreSQL Docker Compose files (docker-compose.prod.postgres.yml, docker-compose.dev.postgres.yml and the one in Docker + PostgreSQL), the api, producer and ingester start after the migrate service has finished. Before, they waited only for the database and Redis, so they could start before the migrations had run. If you use your own compose file, add migrate with condition: service_completed_successfully to their depends_on
  • fix: helm upgrade no longer fails while the last migrate Job still exists. Each install, upgrade and rollback runs its own Job, <release>-migrate-<revision>, and Helm removes the previous one, so the Kubernetes (Helm) page drops the step that deleted the Job first
  • fix: a migration that is interrupted at startup or in the Helm chart's migrate Job (docker stop, an out-of-memory kill, a lost node) no longer leaves the database locked on PostgreSQL and SQLite. Before, it left a row in bun_migration_locks, and every later start failed with migrations table is already locked. On PostgreSQL the lock now disappears with the migration's database connection, and a migration that starts while another one runs waits for it, up to 10 minutes (MIGRATE_LOCK_TIMEOUT), instead of failing. SQLite takes no lock. A database that is already stuck on that error starts after the upgrade, because a leftover row is ignored, so bun db unlock is no longer needed there. MySQL doesn't change. See Migrate (#226)
    • Upgrade note: on PostgreSQL and SQLite, bun db lock no longer holds back migrate or rollback, because they don't read bun_migration_locks any more. If you ran bun db lock to freeze migrations during maintenance, stop the migrate container or Job instead. On MySQL, bun db lock still works as before.
  • fix: SQLite's connection settings (foreign keys and the busy timeout among them) no longer drop after a cancelled query, so deletes keep cascading and writes wait for the lock instead of failing with "database is locked". Transactions now take the write lock when they begin (#265, #485; also #234, #256, #261)
  • fix: in single mode, peekaping refuses a peekaping-probe that connects with the local probe's token, with a 401 whose message says why (the peekaping process checks the local probe's monitors, so use a remote probe's token). Before, such a probe was accepted and checked the local probe's monitors a second time. The probe logs the message, and peekaping logs a warning for each refused call. peekaping also no longer stores PROBE_BOOTSTRAP_TOKEN: it logs one warning at startup when it is set, and a value shorter than 32 characters no longer stops startup. See Deployment modes (PP-315)
    • Upgrade note: a single-mode install that ran a peekaping-probe with the local probe's token must give that probe a remote probe's token, or stop it. A token that PROBE_BOOTSTRAP_TOKEN stored earlier stays in the database, and cluster mode accepts it again, but single mode refuses it.
  • chore: make help and make docker-configs list only targets that exist: make docker-dev-postgres, make docker-dev-sqlite, make docker-prod-postgres and make docker-prod-sqlite, with the down-* targets, instead of dev-* and prod-* names and the missing docker-status and docker-ps. make switch-to-postgres and make switch-to-sqlite start the development environment instead of failing with "No rule to make target". Every Docker target in the Makefile, and the scripts under test_utils, run docker compose, so they work on a machine that has only the Compose plugin (PP-322)
  • fix: maintenance no longer lowers the uptime % read from stats. Each check in a maintenance window counted twice in the total, so one Up and one Maintenance check showed 66.67% uptime instead of 100%. Maintenance checks count as Up, never as Down. This fixes the uptime % on the monitor page (24h, 7d, 30d, 365d and under the chart), the badges and each location on a status page; monitors that had maintenance show higher, correct numbers. A monitor's own 24h uptime on a public status page is read from heartbeats and still counts Maintenance as Down (PP-343)
  • fix: security: a PostgreSQL monitor with sslmode=verify-ca or verify-full verifies the server's certificate. Before, it connected without checking any certificate. An unknown sslmode now fails the check, and a PostgreSQL or SQL Server query that isn't a SELECT is refused before connecting (PP-293, PP-294)
    • Upgrade note: a PostgreSQL monitor with verify-ca or verify-full against a server whose certificate doesn't verify goes Down. Use require to encrypt without verifying.
  • fix: security: the notification channel API returns each saved secret (bot tokens, passwords, API keys, secret webhook URLs) as ********. The edit form shows it as an empty field; saving or testing without retyping it keeps the saved secret, but only while the channel's type and endpoint stay the same, so a saved secret is never sent somewhere new (#124)
    • Upgrade note: a script that reads a channel's secret from the API gets ******** now. A script that sends a changed endpoint has to send the secrets again.
  • fix: security: secrets no longer reach the logs or the Test notification response. A failed send no longer shows its request URL, which holds the bot token for Telegram, and monitor configs and queue payloads are no longer logged (#52)
  • fix: security: a badge's color options take only hex colors and shields.io color names, so a crafted badge URL can't add markup to the SVG. Any other value draws the default color. color=blue on status, uptime and certificate expiry badges now draws blue (#49)
  • fix: security: a malformed socket.io message from a signed-in client no longer crashes the API (#51)
  • fix: security: the API closes slow clients (10 seconds for the request headers, 30 for the whole request, 120 for an idle connection) and answers 413 to a request body over 1 MiB (#53)
  • fix: in cluster mode each notification is sent once, by one api replica. Before, every replica sent it, so three replicas sent three copies (#29)
  • fix: a Down monitor stays Down until a check comes back Up. Before, it could slip back to Pending (a Docker container restarting, or max retries raised while Down), and then its Up went unannounced or its Down was sent twice (#133)
  • fix: the resend interval counts only Down heartbeats of a Down monitor, so a Pending monitor no longer resends a Down notification it never sent (#132)
  • fix: a monitor that is Down when its maintenance window ends sends a Down notification, and one that comes back Up after a window gets an event in its history (PP-131)
  • fix: switched-off notification channels no longer send notifications (#418)
  • fix: a probe's offline notification opens one incident on PagerDuty, Opsgenie, PagerTree and Grafana OnCall, keyed Peekaping/<probe id>, and its online notification closes it as the channel's auto-resolve setting says. The help under the auto-resolve settings of PagerDuty and PagerTree says so. Before, Opsgenie sent it as a test alert, PagerTree and PagerDuty opened a new one on each send, and Grafana OnCall titled it "General notification" (PP-274, PP-280, PP-282, PP-285)
    • Upgrade note: webhooks and custom templates see monitor.status: 1 on a probe's online notification, where it was always 0.
  • fix: a probe's notifications link to the Probes page, not to a monitor page that doesn't exist, and Slack's rich messages for them get a button. The webhook payload carries monitor.type: "probe" for them (#259)
  • fix: an HTTP check runs with its own monitor's timeout, TLS, proxy, mTLS and NTLM settings. Before, checks running at once could share one client and report another monitor's result (#57)
  • fix: an OAuth2 client credentials check with "Authorization Header" sends the client ID and secret only in the header, so strict servers such as Authelia accept it (#276) (thanks @slopgobblercobbler)
  • fix: HTTP monitors send Content-Type only with a request body, unless you set it in the headers. Some servers answered 400 to a bodyless GET with Content-Type: application/json (#415)
  • fix: TCP, DNS, MQTT, PostgreSQL and proxy addresses, and SMTP notification channels, work with IPv6 hosts. Before, every check failed with "too many colons in address" (#128)
  • fix: MongoDB, SQL Server and Kafka checks go Down within the monitor's timeout when the server never answers. Before, MongoDB took 30 seconds, Kafka about two minutes, and SQL Server hung. Kafka SCRAM-SHA-256 and SCRAM-SHA-512 work, and SQL Server passwords with ;, =, { or } are passed right (PP-291, PP-294, PP-327)
  • fix: creating or updating a monitor saves the monitor and its notification channel and tag links together, or nothing. Before, a failed link left the monitor saved and scheduled, and a monitor's first notification could find no channels (#86)
  • fix: the api shuts down cleanly on SIGTERM within 30 seconds: it finishes the requests in flight, then closes the websocket, the jobs, the event bus and the database. The cluster compose files and the Helm chart give it 35 seconds to stop (#84)
  • fix: bun db and the migrate image carry the SQL migrations in the binary, so they run without the source tree (#230)
  • fix: a period with no heartbeats shows a dash with a "No data" tooltip on the monitor page, the chart and status pages, and badges say "N/A" in grey, instead of 0% uptime. The uptime API answers null for it (PP-344)
    • Upgrade note: API clients that read stats/uptime or a status page's uptime_24h as a number have to handle null.
  • fix: the response time chart of a monitor checked less often than once a minute keeps its buckets in place between refreshes, and the first point of the 24-hour and 1-week charts is no longer empty (PP-345)
  • fix: in single mode the Probes page shows the local probe online (PP-325)
  • fix: the heartbeat bar fits its bars again when the sidebar opens or closes (#267)
  • fix: an empty password field shows the "Password is required" message in every language. Its translation key was missing (#357) (thanks @larswmh)

Raw Commit List

Release Statistics

  • 168 commits since 0.0.47
  • 3 contributors

Contributors
Thanks to: @0xfurai @Lars @joergenklopfer16

Docker Images

GitHub Container Registry (GHCR)

  • UI: ghcr.io/0xfurai/peekaping-web:0.0.48 / ghcr.io/0xfurai/peekaping-web:latest
  • API: ghcr.io/0xfurai/peekaping-api:0.0.48 / ghcr.io/0xfurai/peekaping-api:latest
  • Probe: ghcr.io/0xfurai/peekaping-probe:0.0.48 / ghcr.io/0xfurai/peekaping-probe:latest
  • Probe (slim): ghcr.io/0xfurai/peekaping-probe:0.0.48-slim / ghcr.io/0xfurai/peekaping-probe:slim
  • Migrate: ghcr.io/0xfurai/peekaping-migrate:0.0.48 / ghcr.io/0xfurai/peekaping-migrate:latest

Single mode (GHCR)

  • SQLite, or PostgreSQL in its own container: ghcr.io/0xfurai/peekaping:0.0.48 / ghcr.io/0xfurai/peekaping:latest
  • The old name, ghcr.io/0xfurai/peekaping-bundle-sqlite, gets the same tags through 0.0.49 and none from 0.0.50. Switch to ghcr.io/0xfurai/peekaping.

Docker Hub

  • UI: 0xfurai/peekaping-web:0.0.48 / 0xfurai/peekaping-web:latest
  • API: 0xfurai/peekaping-api:0.0.48 / 0xfurai/peekaping-api:latest
  • Probe: 0xfurai/peekaping-probe:0.0.48 / 0xfurai/peekaping-probe:latest
  • Probe (slim): 0xfurai/peekaping-probe:0.0.48-slim / 0xfurai/peekaping-probe:slim
  • Migrate: 0xfurai/peekaping-migrate:0.0.48 / 0xfurai/peekaping-migrate:latest

Single mode (Docker Hub)

  • SQLite, or PostgreSQL in its own container: 0xfurai/peekaping:0.0.48 / 0xfurai/peekaping:latest
  • The old name, 0xfurai/peekaping-bundle-sqlite, gets the same tags through 0.0.49 and none from 0.0.50. Switch to 0xfurai/peekaping.

Don't miss a new peekaping release

NewReleases is sending notifications on new releases.