Lightweight, self-hosted uptime monitoring with email & Telegram alerts,
SSL certificate tracking, real-time dashboard, and a single Docker image.
Observer Lite is a self-hosted uptime monitoring tool built for simplicity and efficiency. It monitors up to 500+ endpoints simultaneously, tracks SSL certificate expiry, sends instant alerts via email and Telegram, and delivers a real-time dark-mode dashboard — all from a single Docker image with no external database required.
Think of it as a lighter, more flexible alternative to Uptime Kuma — with a built-in status code DSL, response body matching, API keys, audit logging, and configurable data retention.
Full documentation is available in docs/ and can be published with GitHub Pages at:
https://cagatayuresin.github.io/observer-lite/
| Category | Details |
|---|---|
| Monitor Types | HTTP GET / POST / HEAD, Ping (ICMP), SSL Certificate, Heartbeat |
| Status Code Matching | Flexible DSL: 2xx, !5xx, 200|301, 2xx|!503 |
| Body Matching | contains, equals, not_equals against response body |
| SSL Monitoring | Certificate validity + configurable expiry warning (default 30 days) |
| Alerting | Email (SMTP) and Telegram with cooldown, retry logic, and recovery notifications |
| Incidents | Automatic open/close lifecycle, acknowledgement, root cause tracking |
| Maintenance Windows | Suppress alerts during planned downtime, optional cron repeat |
| Real-time Dashboard | Server-Sent Events (SSE) push — no polling required |
| User Management | Roles: superadmin, admin, viewer; forced password change on first login |
| API Keys | obs_-prefixed keys, SHA-256 hashed storage, per-key last-used tracking |
| Audit Log | Immutable record of all user actions |
| Import / Export | JSON-based monitor configuration portability |
| Data Retention | Configurable (default 90 days), auto-cleanup runs nightly |
| UI | Dark mode by default, fully bundled — zero CDN dependencies |
docker run -d \
--name observer-lite \
--restart unless-stopped \
-p 3000:3000 \
-v observer_data:/data \
-e SECRET_KEY="$(openssl rand -hex 32)" \
-e DATABASE_PATH="/data/observer.db" \
-e DATABASE_URL="sqlite+aiosqlite:////data/observer.db" \
ghcr.io/cagatayuresin/observer-lite:latestOpen http://localhost:3000 — log in with admin / admin and set a new password when prompted.
services:
observer-lite:
image: ghcr.io/cagatayuresin/observer-lite:latest
container_name: observer-lite
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- observer_data:/data
environment:
SECRET_KEY: "change-this-to-a-long-random-secret-key"
DATABASE_PATH: "/data/observer.db"
DATABASE_URL: "sqlite+aiosqlite:////data/observer.db"
PORT: "3000"
volumes:
observer_data:docker compose up -dAll configuration is provided through environment variables.
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_KEY |
Yes | change-me-… |
HMAC secret for JWT signing and credential encryption. Use a random 32+ byte value. |
DATABASE_PATH |
No | /data/observer.db |
Path to the SQLite database file. |
DATABASE_URL |
No | sqlite+aiosqlite:////data/observer.db |
Full SQLAlchemy async connection URL. |
PORT |
No | 3000 |
Port the application listens on. |
DEBUG |
No | false |
Enable debug logging (true / false). |
Security: Never use the default
SECRET_KEYin production. Generate one withopenssl rand -hex 32.
- Navigate to
http://your-host:3000 - Log in with username
adminand passwordadmin - You will be immediately redirected to the password change screen
- Set a strong password — the account is then ready to use
Sends an outbound HTTP request and evaluates the response against your rules.
-
Status Code DSL — combine patterns with
|(OR) and prefix!to negate:Expression Matches 2xxAny 200–299 response 200Exactly 200 2xx|301Any 2xx or exactly 301 !5xxAnything except 5xx 2xx|!503Any 2xx, or anything that isn't 503 -
Body Matching — optionally assert the response body
contains,equals, ornot_equalsa value.
Uses the system ping binary to check ICMP reachability. Works with hostnames and IP addresses.
Performs a TLS handshake and reports certificate validity and days remaining until expiry. An alert is sent when expiry is within the configured threshold (default: 30 days).
A passive monitor — your service calls Observer Lite to signal it is alive. If no ping is received within check_interval + grace_seconds, the monitor goes down.
GET /api/heartbeat/{token}
POST /api/heartbeat/{token}
Notification channels are configured per-monitor. Each channel can independently toggle alerts for down, recovery, and SSL expiry events.
Configure via the Notification Channels page in the UI. Required fields: SMTP host, port, sender address, recipients list. TLS and STARTTLS are both supported. Passwords are stored with Fernet encryption (AES-128-CBC + HMAC-SHA256).
Create a bot via @BotFather, then provide the bot_token and chat_id in the channel settings. A test notification can be sent from the UI before saving.
observer-lite/
├── backend/ # Python 3.12 + FastAPI (async)
│ └── app/
│ ├── checkers/ # HTTP, Ping, SSL, Heartbeat probes
│ ├── routers/ # REST API endpoints
│ ├── scheduler/# APScheduler engine + per-monitor jobs
│ ├── services/ # Business logic (incidents, notifications, retention)
│ ├── sse/ # Server-Sent Events broadcaster
│ └── utils/ # Crypto, pagination
└── frontend/ # Vue 3 + Vite + Tailwind CSS + Pinia
Stack highlights:
- Backend: FastAPI, SQLAlchemy 2.0 async, aiosqlite, APScheduler 3.x (SQLAlchemyJobStore), aiosmtplib, python-jose, bcrypt, slowapi
- Frontend: Vue 3, Vite, Tailwind CSS, Pinia, uPlot (lightweight charts, ~40 KB)
- Transport: Server-Sent Events for real-time updates (no WebSocket needed)
- Packaging: Multi-stage Docker build — Node.js build + Python 3.12-slim runtime, single image
Important: The application must run with
--workers 1. The APScheduler and SSE broadcaster are in-memory singletons; multiple workers would duplicate checks and miss events.
Interactive API documentation is available at /api/docs (Swagger UI) and /api/redoc (ReDoc) when the server is running.
Key endpoint groups:
POST /api/auth/login | refresh | logout | change-password
GET /api/auth/me
GET|POST|PUT|DELETE /api/monitors/{id}
POST /api/monitors/{id}/pause | resume | check-now
GET /api/monitors/{id}/stats | history | incidents
GET|POST|PUT|DELETE /api/channels + POST /{id}/test
GET|POST|PUT|DELETE /api/incidents + POST /{id}/acknowledge
GET|POST|PUT|DELETE /api/maintenance
GET|POST|DELETE /api/api-keys
GET|PUT /api/settings
GET /api/sse/dashboard (SSE stream)
GET /api/audit-log
GET /api/export/monitors | POST /api/import/monitors
Authentication is via JWT Bearer token or an obs_-prefixed API key.
See CONTRIBUTING.md for the full development guide including local setup, coding standards, and PR process.
# Backend
cd backend
uv venv && uv pip install -e ".[dev]"
alembic upgrade head
uvicorn app.main:app --reload --workers 1
# Frontend (separate terminal)
cd frontend
npm install
npm run dev
# Tests
cd backend
pytest tests/ --cov=app -qThe GitHub Actions workflow publishes the single production image to GHCR. Successful CI runs on main update the moving latest and main tags. Versioned image tags are published only when pushing a matching v* Git tag, with the version read from the root VERSION file.
Moving tags:
ghcr.io/cagatayuresin/observer-lite:latest
ghcr.io/cagatayuresin/observer-lite:main
Release tags include:
ghcr.io/cagatayuresin/observer-lite:0.2.0
ghcr.io/cagatayuresin/observer-lite:v0.2.0
ghcr.io/cagatayuresin/observer-lite:0.2
When pushing a Git tag, use the v-prefixed form that matches VERSION, for example v0.2.0.
Observer Lite is designed with security in mind:
- Passwords hashed with bcrypt (cost 12)
- JWT access tokens expire in 15 minutes; refresh tokens in 30 days
- SMTP/Telegram credentials stored with Fernet symmetric encryption, key derived via PBKDF2
- API key raw values shown once; only SHA-256 hash stored
- Login endpoint rate-limited to 5 attempts / minute per IP via slowapi
Please report vulnerabilities privately — see SECURITY.md for the responsible disclosure process.
MIT © 2026 Çağatay Üresin