Monitor websites uptime using Cloudflare Workers
TypeScript 86%
CSS 12%
Shell 2%
HTML <1%
Makefile <1%

README.md

Multi-region uptime monitor #

A self-hosted uptime monitor that runs checks from Cloudflare Workers in up to nine regions. It includes latency charts, request history, public monitor views, and grouped status pages.

The API, scheduler, web UI, and PostgreSQL data stay on your server. Only the HTTP probes run on Cloudflare.

How it works #

  1. The scheduler reads due monitors from PostgreSQL.
  2. Checks for the same region are batched and signed with a shared secret.
  3. A Worker near the configured cloud region requests each target and returns the result.
  4. The API stores the observations and serves the admin UI and public pages.

Cloudflare placement is a hint, not a fixed city or data center. A Worker runs on Cloudflare's network near the selected AWS region.

Requirements #

  • Docker with Compose
  • An existing PostgreSQL database
  • A Cloudflare account with Workers enabled
  • Node.js 24+ and pnpm 11+ on the machine used to deploy the Workers

Setup #

Clone the repository, install the locked dependencies, and create your environment file:

pnpm install --frozen-lockfile
cp .env.example .env

1. Configure the application #

Set DATABASE_URL to a PostgreSQL database that the containers can reach. On Docker Desktop, a database running on the host is available through host.docker.internal, not localhost.

Generate the two secrets used by the app:

openssl rand -hex 32 # SESSION_SECRET
openssl rand -hex 32 # PROBE_SIGNING_SECRET

Generate an Argon2id hash for the admin password without putting the password in shell history:

read -s -p 'Admin password: ' UPTIME_ADMIN_PASSWORD; echo
export UPTIME_ADMIN_PASSWORD
pnpm --filter @uptime/api exec node --input-type=module -e \
  'import argon2 from "argon2"; console.log(await argon2.hash(process.env.UPTIME_ADMIN_PASSWORD, { type: argon2.argon2id }))'
unset UPTIME_ADMIN_PASSWORD

Put the result in ADMIN_PASSWORD_HASH. Keep it quoted because Argon2 hashes contain $ characters.

At minimum, edit these values in .env:

DATABASE_URL=postgresql://uptime:password@database.example.com:5432/uptime
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD_HASH='$argon2id$...'
SESSION_SECRET=...
PROBE_SIGNING_SECRET=...
REGIONS_LIST="us-east,eu-west,asia"
WORKERS_URL_DOMAIN=replace-after-the-first-worker-deployment

The API creates the configured admin only when the database has no admin yet. Use scripts/reset-admin.sh if you need to change it later.

2. Choose probe regions #

REGIONS_LIST is a comma-separated list. It controls the regions shown in the editor, accepted by the API, run by the scheduler, and deployed by the Worker script.

ID Approximate anchor Placement hint
us-east Virginia, USA aws:us-east-1
us-west Oregon, USA aws:us-west-2
canada-central Montréal, Canada aws:ca-central-1
eu-west Ireland aws:eu-west-1
eu-north Stockholm, Sweden aws:eu-north-1
eu-south Milan, Italy aws:eu-south-1
asia Singapore aws:ap-southeast-1
asia-east Tokyo, Japan aws:ap-northeast-1
asia-south Mumbai, India aws:ap-south-1

us-east,eu-west,asia is a reasonable three-continent starting point. Omit REGIONS_LIST to enable all nine.

A monitor's target checks per day are:

number of regions × 1440 ÷ interval in minutes

The scheduler batches up to five same-region checks in one Worker call. This reduces Worker invocations without changing the number of target checks.

3. Deploy the Workers #

Log in to Cloudflare and confirm the target account:

pnpm --filter @uptime/probe-worker exec wrangler login
pnpm --filter @uptime/probe-worker exec wrangler whoami

Check the selected Worker builds without deploying:

scripts/deploy-workers.sh --dry-run

Create a temporary secrets file containing the same probe secret used by the scheduler, then deploy:

WORKER_SECRETS_FILE="$(mktemp)"
chmod 600 "$WORKER_SECRETS_FILE"
grep '^PROBE_SIGNING_SECRET=' .env > "$WORKER_SECRETS_FILE"
scripts/deploy-workers.sh --secrets-file "$WORKER_SECRETS_FILE"
unlink "$WORKER_SECRETS_FILE"
unset WORKER_SECRETS_FILE

On the first deployment, Wrangler may ask you to create a workers.dev subdomain. It then prints URLs in this form:

https://uptime-probe-eu-west.your-account.workers.dev

Copy only the part after the Worker name into .env:

WORKERS_URL_DOMAIN=your-account.workers.dev

Do not include https://, uptime-probe-eu-west., or a path. The scheduler builds every enabled URL as:

https://uptime-probe-{region}.{WORKERS_URL_DOMAIN}

Test one of the deployed URLs:

curl -i https://uptime-probe-eu-west.your-account.workers.dev/

GET / should return 405 Method Not Allowed. That confirms the Worker is reachable; actual probe requests must also have a valid scheduler signature.

4. Start the app #

The Compose file uses prebuilt linux/amd64 images from dmarcosm/uptime-*. It runs migrations first, then starts the API, scheduler, and web UI:

docker compose pull
docker compose up -d
docker compose ps

Open http://localhost:5176. The API health check is available at http://localhost:3000/health unless API_PORT was changed.

PostgreSQL is not included in the Compose file. Migration errors stop the API and scheduler so a missing schema is not used accidentally.

Notifications #

Use Notifications in the admin panel to configure Telegram, Discord, Resend, Gotify, generic webhooks, SMTP, or Home Assistant and send a test message. In each monitor's editor, choose destinations, consecutive failure/recovery thresholds, and optional repeat reminders. Defaults are three failed rounds to declare an outage, two healthy rounds to recover, and reminders off.

See notification setup and delivery behavior for provider setup, regional check semantics, deployment, and adding provider classes.

Updating #

Pull the current images and recreate the services:

docker compose pull
docker compose up -d

Redeploy the Workers when probe code or Wrangler configuration changes:

scripts/deploy-workers.sh --dry-run
scripts/deploy-workers.sh

Existing Worker secrets remain in Cloudflare when omitted from a later deployment. Supply --secrets-file again when rotating PROBE_SIGNING_SECRET.

Development #

pnpm install --frozen-lockfile
pnpm typecheck
pnpm test
pnpm --filter @uptime/api dev
pnpm --filter @uptime/web dev

The API and scheduler do not load .env themselves during local pnpm runs. Export the variables first with set -a; source .env; set +a. Do not run multiple schedulers against the same database unless that is intentional.

Deployment notes #

  • Put TLS in front of the web UI and API for Internet-facing installations, and set SESSION_COOKIE_SECURE=true.
  • The Worker URLs are public, but requests require an HMAC signature and expire after a short clock-skew window.
  • Only monitor intentionally public targets. The project blocks private and special-use literal addresses, but it is not a general-purpose SSRF boundary.
  • Raw observations are retained for 90 days. Completed UTC days are stored as daily uptime rollups; the current day is calculated from live observations.
  • DNS diagnostics are optional per monitor and do not affect uptime state.

See the deployment runbook for local Worker testing, custom domains, credential rotation, rollback, and troubleshooting. Historical uptime imports are documented in daily-uptime-import.md.

Cloudflare references: Workers workers.dev URLs and Placement Hints.