A lightweight, self-hosted uptime monitoring application featuring a Go backend (SQLite) and a modern React frontend (TypeScript & Vite).
- Automated Polling: Background health checks scheduled per target using cron expressions with response time tracking and status code verification.
- SQLite Storage: Built-in SQLite database with automated migration management.
- Modern Web Dashboard:
- Responsive layout optimized for both desktop and mobile views.
- Compact check history bars with recent status check indicators.
- Collapsible sidebar navigation for mobile devices with indicator arrows.
- URL truncation and expansion on hover.
- Incident Notifications: Records target status changes, certificate expiry thresholds, and common URL-check failures in SQLite. Logged-in users can open the incidents page from the bell button, see the affected target, URL, incident type, cause, and timestamp, and mark individual incidents or all incidents as read. The page is responsive on desktop and mobile.
- Comprehensive Testing:
- Go unit tests for database models, repositories, polling service, and HTTP handlers.
- Vitest & React Testing Library component tests for the frontend.
- Go (v1.27.1 or newer)
- Node.js (v24.21.0 or newer recommended) & npm
-
Navigate to the project root directory.
-
Run the application:
go run cmd/uptime/main.go
The backend server will start on
:8080. -
Run Go tests:
go test -v ./...
- Navigate to the
webdirectory:cd web - Install dependencies:
npm install
- Run the development server (proxies
/apito:8080):Dashboard is available onnpm run dev
http://localhost:5173/. - Run frontend tests:
npm test - Build for production:
npm run build
Build the image (multi-stage; uses nginx:alpine as the runtime base):
docker build -f Dockerfile-alpine -t shennarwp/uptime:alpine-latest .Run the container. The database is persisted in a mounted directory (see UPTIME_DB_PATH below), so it survives container restarts and removal:
docker run --detach \
--name uptime \
--restart always \
-v ~/uptime/data:/app/data \
-p 80:80 \
shennarwp/uptime:alpine-latestNotes:
- The image serves port
80(nginx serving the React frontend and proxying/apito the Go backend on:8080). The-p 80:80mapping is required when running the container directly; deployments behind an external reverse proxy can use an internal network instead. UPTIME_DB_PATHdefaults to/app/data/uptime.db; the SQLite database (including its WAL files) lives in the mounted volume, so mount a directory (not a single file) or data will be lost on container recreation.- Override the database path at runtime with
-e UPTIME_DB_PATH=/some/other/path.db.
Protected endpoints (GET /api/v1/incidents/latest, POST /api/v1/targets, PUT /api/v1/target/{id}, DELETE /api/v1/target/{id}, PATCH /api/v1/incident/{id}/read, and POST /api/v1/incidents/read) are guarded by a bearer token read from the UPTIME_API_TOKEN environment variable. Requests must send an Authorization: Bearer <token> header; anything else returns 401 Unauthorized. The equivalent /api/... paths remain available as compatibility aliases.
docker run ... -e UPTIME_API_TOKEN=your-secret-token ...If UPTIME_API_TOKEN is unset the guard fails closed — all writes are denied (a warning is logged at startup). You must set the variable for the edit/login flow to work.
The web UI has a Login button in the header. Entering a token calls POST /api/v1/auth/verify to confirm it's correct; on success the token is stored in the browser's localStorage and the button becomes Logout. The add, edit, and delete controls are only shown while logged in, and the token is cleared automatically if a mutation is ever rejected with 401.
GET /api/v1/incidents returns incidents newest first. Each incident includes its type, affected target name and URL, timestamp, and is_read state. Incident types are going_down, going_up, cert_30_days, cert_10_days, cert_expired, dns_error, timeout_error, connection_refused, tls_error, and network_error.
GET /api/v1/incidents/latest?target_id=<id>&type=<incident_type> returns one full incident object for the requested target and type. An unread match is preferred; if all matching incidents have been read, the newest matching incident is returned. The endpoint returns 404 Not Found when no matching incident exists and requires the bearer token.
Health checks classify incidents as follows:
| Type | Meaning |
|---|---|
going_down |
The target returned an HTTP 5xx response after previously being up. |
going_up |
The target returned to an HTTP status below 500 after previously being down. |
cert_30_days |
The TLS certificate entered the 30-day expiry window. |
cert_10_days |
The TLS certificate entered the 10-day expiry window. |
cert_expired |
The TLS certificate has already expired. |
dns_error |
The hostname could not be resolved, including errors such as lookup example.com ... no such host. |
timeout_error |
The connection or request exceeded its timeout. |
connection_refused |
The target host actively refused the connection. |
tls_error |
The TLS handshake or certificate validation failed for a reason other than an expiry notification. |
network_error |
Another transport-level network failure occurred. |
HTTP 4xx and 5xx responses remain status incidents because the endpoint responded successfully at the transport layer. Repeated transport failures of the same type in one failure streak are deduplicated; a new incident is recorded when the failure type changes or a missing incident is discovered for an existing failure streak.
Use PATCH /api/v1/incident/{id}/read to acknowledge one incident or POST /api/v1/incidents/read to acknowledge all incidents. Both operations require the bearer token.
When UPTIME_NTFY_URL is set, the poller sends ntfy notifications about health status and certificate expiry.
Status changes. After each health check a notification is sent whenever a target transitions UP -> DOWN, remains DOWN, or recovers (DOWN -> UP). While a target stays UP, no notification is sent. Each message includes the target name, its URL, the new status (🟢 UP / 🔴 DOWN), and — when down — the HTTP status code and/or the error message.
Certificate expiry. A dedicated daily job (independent of each target's poll interval) checks the TLS certificate of every HTTPS target once a day. A reminder is sent once when the certificate has 30 days or fewer remaining, and then once per calendar day once it has 10 days or fewer. Renewing the certificate resets the reminder cycle. Messages include the target name, URL, days remaining, and the expiry date.
The variable is a plain HTTP(S) URL to an ntfy topic, e.g. https://ntfy.sh/mytopic. If it is unset or empty, notifications are disabled.
docker run ... -e UPTIME_NTFY_URL=https://ntfy.sh/mytopic ...In the deployment workflow the URL is provided via the UPTIME_NTFY_URL repository secret; see .github/workflows/deploy.yml and docker-compose.yml.
The backend exposes an OpenAPI (Swagger) specification generated from Go annotations using swaggo/swag.
- Spec files:
api/swagger.jsonandapi/swagger.yaml - Interactive UI: served by the backend at
http://localhost:8080/swagger/(Swagger UI) when running the Go server.
The spec is generated from swagger comment annotations in the code (cmd/uptime/main.go, internal/handler/, internal/database/models.go). After changing the API, regenerate with:
go tool swag init -g cmd/uptime/main.go --output apiCommit the regenerated api/ files together with your API changes.
cmd/uptime/- Application entry point (main.go)api/- Generated OpenAPI spec (swagger.json,swagger.yaml,docs.go)internal/database/- SQLite connection, database models, migrations, and target repositoryinternal/service/- Target and polling servicesinternal/handler/- HTTP API handlersweb/- React frontend (Vite, TypeScript, CSS)src/components/- React components (Sidebar,TargetCard,CheckHistoryBar,Header,TruncatedUrl) and unit tests
