NodeWatcher is a lightweight, self-hosted VPS monitoring dashboard for Node.js 22+ with SQLite persistence. The repository folder is still named node-monitor, but the product is documented here as NodeWatcher.
It accepts HTTP reports from agents, stores metrics in SQLite, and renders a server-rendered dashboard with no frontend build step. The UI is a plain HTML template string styled with Tailwind via CDN and charts via Chart.js.
- Lightweight monitoring dashboard for VPS or other Linux/Unix hosts
- Agent reporting endpoint for collecting CPU, memory, disk, network, and Docker metrics
- SQLite-backed persistence for the Express target and D1-backed persistence for the Cloudflare Worker target
- In-memory live cache over SQLite for fast dashboard reads and low-latency updates
- Search, sorting, and filtering in the server table, plus a detail modal with charts
- Auto-refresh polling and copy-to-clipboard helpers for the agent endpoint and token
- Default password seed plus a forced password change on first login
- Auto-generated agent token stored in the database and refreshable from the dashboard
Screenshots can be added here in the future.
flowchart LR
A[Agent: Go or Node.js] -->|POST /api/report| B[Express API or Hono Worker]
B --> C[(SQLite / D1)]
C --> D[In-memory server cache]
D --> E[Browser dashboard UI]
The live server list is held in an in-memory Map in the Express implementation. That cache is rebuilt from SQLite at startup and then updated as new reports arrive. SQLite remains the source of truth for persistence, history, and retention jobs.
- Node.js 22+ is required because the server uses the built-in node:sqlite module.
- The startup scripts run with node --experimental-sqlite.
- Docker is optional but supported.
- A Cloudflare account is only needed if you want to deploy the Worker/D1 target.
git clone <repository-url>
cd node-monitor
npm install
npm startThen open http://localhost:3000.
Default login credentials:
- Username: not used; the UI uses a password-only login flow
- Password: 123456
On first login, the app forces a password change. After that, use the newly chosen password.
The dashboard shows an auto-generated agent token. Copy it and point an agent at the dashboard endpoint.
The Express target reads configuration from src/config.js.
| Setting | Default | Source | Notes |
|---|---|---|---|
| PORT | 3000 | Environment variable PORT | Env-configurable. |
| MONITOR_PORT | 3000 | docker-compose host port mapping | Compose-only variable used to map the host port to container port 3000; the Node.js app does not read it. |
| DB_PATH | data/monitor.db | Environment variable DB_PATH | Env-configurable. |
| MAX_HISTORY | 100 | Code constant | In-memory history buffer size. |
| OFFLINE_TIMEOUT_MS | 30000 | Code constant | Marks a server offline after 30 seconds without an update. |
| SWEEP_INTERVAL_MS | 5000 | Code constant | Sweep interval for offline detection. |
| RAW_RETENTION_MS | 48 hours | Code constant | Raw samples are retained for 48 hours. |
| ROLLUP_RETENTION_MS | 90 days | Code constant | Hourly rollups are retained for 90 days. |
| ROLLUP_INTERVAL_MS | 600000 | Code constant | Maintenance and rollup interval, 10 minutes. |
Run the app directly on a VPS or local machine:
npm install
npm startThe server listens on the configured PORT and writes its SQLite database to DB_PATH. The default database file is data/monitor.db.
The repository includes a Dockerfile and docker-compose.yml for containerized deployment.
docker compose up -dThe compose file defines a monitor service and an agent service. The dashboard container exposes port 3000 by default, and the volume monitor-data persists the database at /app/data. The compose file maps the host port from MONITOR_PORT (default 3000) to container port 3000. The application inside the container always runs with PORT=3000; only TOKEN is passed through to the app as an environment variable. The agent service uses the same TOKEN value so the dashboard and agents can authenticate together.
The Worker implementation uses Hono and D1, which is SQLite-compatible. Follow the steps in DEPLOY-CLOUDFLARE.md for the Workers deployment path.
NodeWatcher ships with two agent options:
- The Go agent in agent/README.md is the recommended companion project and is maintained separately from the dashboard.
- The example Node.js agent is agent.js. It uses systeminformation and must be installed manually because it is not declared in package.json dependencies. Install it with:
npm install systeminformationThe example agent reads DASHBOARD_URL, TOKEN, and HOSTNAME from the environment.
| Method | Path | Auth requirement | Description |
|---|---|---|---|
| POST | /api/report | Token in JSON body | Accepts agent metrics and creates or updates a server record. |
| GET | /api/servers | None in the current Express code | Returns all known servers as JSON. |
| GET | /api/server/:hostname | None in the current Express code | Returns one server by hostname. |
| DELETE | /api/server/:hostname | None in the current Express code | Deletes a server and its history from memory and SQLite. |
| GET | /api/server/:hostname/history | None in the current Express code | Returns history points for a server. Valid ranges are today, 24h, 7d, 30d, and 60d. Invalid ranges return 400; unknown hosts return 404. |
| GET | /api/stats | None in the current Express code | Returns aggregate counts and averages across all servers. |
| GET | /api/health | None | Basic health endpoint. |
| GET | /api/token | Session auth | Returns the current agent token for the signed-in dashboard user. |
| POST | /api/token/refresh | Session auth | Rotates the agent token and invalidates all existing agents. |
| GET | / | Session auth | Dashboard page. |
| GET / POST | /login | None | Login page and login action. |
| GET / POST | /change-password | Session auth plus forced password-change state | Page and handler for changing the password. |
| POST | /logout | None | Destroys the current session cookie. |
Example report payload:
{
"token": "hex-token",
"hostname": "web-01",
"os": "Linux 6.8.0",
"platform": "linux",
"uptime": 123456,
"cpu": {
"name": "x86_64",
"usage": 38.2
},
"memory": {
"used": 2048,
"total": 8192,
"percent": 25
},
"disk": {
"used": 120,
"total": 500,
"percent": 24
},
"network": {
"rx": 1024000,
"tx": 2048000
},
"docker": {
"containers": 4,
"running": 3
}
}Validation rules for report payloads:
- hostname is required and must be a non-empty string.
- uptime must be a number when present.
- cpu.usage must be a number when present.
- memory.percent must be a number when present.
- disk.percent must be a number when present.
NodeWatcher keeps raw samples for 48 hours and rolls them into hourly buckets for 90 days. The maintenance job runs at startup and every 10 minutes in the Express target.
The history endpoint uses different data sources depending on the range:
- today and 24h read raw history from the history table.
- 7d, 30d, and 60d read the hourly rollup from history_hourly.
The API down-samples history to a maximum of 500 points when a range is very large.
- The Express middleware enables a Content Security Policy with default-src 'self', script-src including self and the CDN assets, and script-src-attr 'none'. Inline event handlers are intentionally blocked.
- Global rate limiting is 300 requests per minute, dashboard requests are limited to 120 per minute, and login attempts are limited to 10 per minute.
- Agent token validation uses timing-safe comparison.
- The default password is 123456 and is seeded on first run, but the app forces a password change on the first login.
- Dashboard sessions are stored in memory in the Express target, use the nm_session cookie, are HttpOnly, SameSite=Lax, and expire after 24 hours. In production they are marked Secure.
Important limitations:
- In the current Express implementation, /api/servers, /api/server/:hostname, DELETE /api/server/:hostname, /api/stats, and the history endpoint are not protected by session authentication.
- Sessions are in-memory, so all users are logged out when the Express process restarts.
- For production exposure, run NodeWatcher behind HTTPS and a reverse proxy or another authentication layer.
.
├── index.js # Starts the Express server and maintenance loop
├── package.json # npm scripts and runtime dependencies
├── src/ # Application code
│ ├── app.js # Creates the Express app
│ ├── auth.js # Password hashing, sessions, and cookies
│ ├── config.js # Runtime configuration defaults
│ ├── database.js # SQLite schema, maintenance, and access helpers
│ ├── middleware.js # Security middleware and rate limits
│ ├── store.js # In-memory server cache and report processing
│ ├── routes/ # API, auth, and dashboard routes
│ └── views/ # Server-rendered dashboard and auth templates
├── worker/ # Cloudflare Worker implementation and routes
├── migrations/ # D1 schema migration for the Worker target
├── agent.js # Example Node.js agent
├── agent/ # Go agent companion project
├── Dockerfile # Container build for the Express target
├── docker-compose.yml # Container orchestration example
└── DEPLOY-CLOUDFLARE.md # Cloudflare Worker deployment guide
| Script | What it does |
|---|---|
| npm start | Starts the Express app with node --experimental-sqlite. |
| npm run dev | Alias for the development/start command. |
| npm run agent | Starts the example Node.js agent with node --experimental-sqlite. |
| npm run tunnel | Starts a Cloudflared tunnel to the local dashboard. |
| npm run cf:dev | Starts the Cloudflare Worker locally with Wrangler. |
| npm run cf:deploy | Deploys the Worker with Wrangler. |
| npm run cf:migrate | Applies D1 migrations to the remote database. |
| npm run cf:migrate:local | Applies D1 migrations to the local database. |
| Problem | What to try |
|---|---|
| ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite | Use Node.js 22+; older versions do not support the built-in node:sqlite module. |
| Agents do not appear in the dashboard | Check that the agent uses the correct dashboard URL and the current token. The token is generated automatically on first run and can be refreshed from the dashboard. |
| Servers stay offline | The Express target marks a server offline after 30 seconds without a report. Check whether the agent is running and reachable. |
| 7D/30D/60D charts are empty | The hourly rollup only contains completed hours. A new range will appear only after some hourly buckets exist. |
| CSP errors in the browser console | Inline event handlers are intentionally blocked by the CSP. Use the built-in UI behavior or adjust the deployment environment carefully if you change the CSP. |
NodeWatcher is licensed under the MIT License. See LICENSE.md for the text.
The Go agent in agent/ is a separate companion project and is licensed separately under agent/LICENSE.md.