checkByID snapshotted a Site under RLock, ran a network check for seconds, then handleStatusChange wrote the entire stale struct back into liveState. Any concurrent mutation during the check — a user pause, a config edit, or a push heartbeat — was silently reverted. Worst case: a heartbeat set UP and an in-flight checkPush overwrote it with a stale DOWN, firing a false alert. Introduce applyState(id, mutate): a single read-modify-write helper that runs the mutator against the CURRENT live entry under the write lock, so config and Paused are preserved automatically and status transitions are computed from the true current status. Route handleStatusChange, RecordHeartbeat, ToggleSitePause and checkGroup through it. Logs and alerts now fire after the lock is released, off the critical section. Push false-DOWN is closed by a guard: a non-UP result whose snapshot LastCheck predates the live LastCheck is dropped, since a heartbeat (or newer check) superseded it. HTTP/probe stamp LastCheck=now before the call, so they are unaffected (and serial per site anyway). Also fixes a latent bug where RecordHeartbeat read StatusChangedAt after overwriting it, always reporting "was down 0s"; downSince is now captured before mutation. Adds regression tests for pause/config-edit/heartbeat-during-check and removed-site-dropped. Full suite green under -race.
uptop
Self-hosted uptime monitoring with a TUI over SSH.
No browser. No client install. Just ssh -p 23234 your-server.
What is this
An uptime monitor you manage entirely from the terminal. It runs as a server, exposes an SSH endpoint, and drops you into a full TUI — monitors, alerts, logs, nodes, all there.
Built on RDGames/go-upkeep. Rewritten for clustering, config-as-code, and a proper dashboard.
Features
- 6 check types — HTTP, Push (heartbeat), Ping, Port, DNS, Groups
- 10 alert providers — Discord, Slack, Email, Ntfy, Webhook, Telegram, PagerDuty, Pushover, Gotify, Opsgenie
- Config as code — define monitors in YAML, apply declaratively, version control your setup
- HA clustering — leader/follower with automatic failover
- Prometheus metrics —
/metricsendpoint, wire it straight to Grafana - Public status page — HTML + JSON, toggle with an env var
- SQLite or Postgres — SQLite for single-node, Postgres for production
- Uptime Kuma import — migrate from Kuma with one command
Screenshots
![]() |
![]() |
![]() |
![]() |
![]() |
|
Quick start
go run cmd/uptop/main.go
ssh -p 23234 localhost
Want some data to look at first:
go run cmd/uptop/main.go -demo
Install
Docker (recommended)
services:
uptop:
image: lerkolabs/uptop:latest
restart: unless-stopped
ports:
- "23234:23234"
- "8080:8080"
environment:
- UPTOP_DB_TYPE=sqlite
- UPTOP_DB_DSN=/data/uptop.db
- UPTOP_STATUS_ENABLED=true
# - UPTOP_ADMIN_KEY=ssh-ed25519 AAAA... you@host
volumes:
- ./data:/data
First run: set UPTOP_ADMIN_KEY to your SSH public key, or attach to the container and add it in the Users tab.
Binary (Linux amd64)
Download from Releases.
From source
go install gitea.lerkolabs.com/lerkolabs/uptop/cmd/uptop@latest
Upgrading: Pull the new image (or binary) and restart. Database migrations run automatically on startup.
Config as code
Export your current monitors:
uptop export -o monitors.yaml
Apply a config file:
uptop apply -f monitors.yaml
uptop apply -f monitors.yaml --dry-run # see what would change
uptop apply -f monitors.yaml --prune # delete anything not in the YAML
Full reference in docs/config-as-code.md.
Environment variables
| Variable | Default | Description |
|---|---|---|
UPTOP_PORT |
23234 |
SSH server port |
UPTOP_HTTP_PORT |
8080 |
HTTP server port (status page, push, metrics) |
UPTOP_DB_TYPE |
sqlite |
sqlite or postgres |
UPTOP_DB_DSN |
uptop.db |
Database path or connection string |
UPTOP_STATUS_ENABLED |
false |
Enable public status page |
UPTOP_STATUS_TITLE |
System Status |
Status page title |
UPTOP_ENCRYPTION_KEY |
AES-256-GCM key for alert credentials (details) | |
UPTOP_CLUSTER_MODE |
leader |
leader, follower, or probe |
UPTOP_PEER_URL |
Leader URL for follower and probe nodes | |
UPTOP_CLUSTER_SECRET |
Shared key for cluster + API auth | |
UPTOP_INSECURE_SKIP_VERIFY |
false |
Skip TLS verification for checks |
UPTOP_ALLOW_PRIVATE_TARGETS |
false |
Allow monitoring RFC1918/loopback addresses |
UPTOP_ADMIN_KEY |
SSH public key seeded as first admin on startup |
See .env.example for all options including TLS, probes, and advanced settings.
Encryption
Set UPTOP_ENCRYPTION_KEY to encrypt alert credentials (SMTP passwords, webhook URLs, API tokens) at rest with AES-256-GCM. Generate a key:
openssl rand -hex 32
Without this, credentials are stored as plaintext in the database. uptop warns on startup if unset. To encrypt credentials on an existing install, run uptop migrate-secrets with the key set.
Clustering
uptop supports three modes: leader (default single node), follower (HA failover — takes over if the leader goes down), and probe (stateless distributed checks from multiple regions).
See docs/clustering.md for setup guides, or the working examples in deploy/.
Migrating from Uptime Kuma
Export your Kuma backup JSON, then:
curl -X POST http://localhost:8080/api/import/kuma \
-H "X-Upkeep-Secret: your-secret" \
-H "Content-Type: application/json" \
-d @kuma-backup.json
License
MIT — see LICENSE.




