A lightweight, self-hostable SNMP monitor for home labs and small networks: poll your devices over SNMPv2c/v3, keep the history in SQLite, and browse it in a dependency-light web UI.
SNMPCanvas polls interface traffic, CPU, memory, storage, temperatures, and fans on an interval you choose, keeps a configurable window of history, and draws the graphs. It is part of the Canvas family: CrossCanvas draws your network, PingCanvas turns those diagrams into a live reachability wall, and SNMPCanvas adds the performance history - including an export file PingCanvas reads directly, overlaying live values onto board labels and connections (schema below). A fourth sibling, SyslogCanvas, collects syslog and SNMP traps from the same gear.
Unlike CrossCanvas and PingCanvas, SNMPCanvas has a backend: polling needs a process that outlives a browser tab, and history needs somewhere to live. The family's small-footprint ethos carries over all the same - one container, one SQLite file, two runtime dependencies, and a frontend that is still plain HTML/CSS/JS with no build step.
your devices ──SNMP (UDP/161)──► SNMPCanvas ──► SQLite ──► web UI & graphs
(v2c/v3) │
└──► snmp-status.json ──► dashboards
One Node process does everything: an in-process scheduler polls each device on its interval, counter deltas become rates, samples land in SQLite, and the same process serves the UI. Interfaces you mark for export are also written to a small JSON status file every cycle, for other tools to read.
- Devices list - up/down status, CPU, busiest interface, uptime, last poll; sortable by any column.
- Device page - CPU / memory / filesystem / temperature / fan cards (when the device exposes them), then every interface with live in/out rates, errors, and discards. A filter box and per-interface Track/Export toggles keep big switches manageable.
- History graphs - click any interface or resource card: traffic (average + peak), errors/discards, a link-status strip, and dashed 95th-percentile lines, from 1 hour to 90 days. Charts are hand-drawn SVG that follows the app theme, with an optional link-speed scale for honest utilization reading.
- Add-device flow - enter an address and credentials; SNMPCanvas
verifies with a GET, walks the standard tables, and shows you everything
it found so you choose what gets tracked. No MIB files involved -
everything is well-known numeric OIDs (see
server/oids.js). - SNMPv2c and v3 - v3 auth MD5/SHA-1/SHA-256/SHA-512, privacy DES/AES-128 and both AES-256 key-localization variants (Blumenthal and Reeder/Cisco - devices vary, and the wizard offers both).
- Interface export - check the Export box on any interface and its
latest stats are written atomically to
snmp-status.jsonafter every poll, with short stable per-interface codes for easy mapping. - Tunable data volume - a global polling interval (default 30 seconds, per-device override) and retention window (default 90 days, pruned nightly). At the defaults, expect roughly 2,900 samples per tracked entity per day.
- Single shared password for the UI (scrypt-hashed), sessions, login rate limiting, and one-click database backups from the Settings page.
- Inventory export - download the monitored fleet as a CSV that CrossCanvas imports directly (File -> Import inventory), so devices SNMPCanvas already discovered can seed a diagram - and a device with an IP is monitoring-ready in PingCanvas at once.
- 29 themes carried over from CrossCanvas's palette family, grouped the same way (Paper / Warm / Cool / Night / Screen).
SNMPCanvas is intentionally a store of SNMP data with a clear window onto it: polling, history, graphs, and the export file. Auto-discovery, topology, alerting, and automation aren't on the roadmap - they are jobs for tools built around them, and the pieces here are shaped to sit alongside those tools rather than replace them (reachability alerting pairs naturally with an uptime monitor; the wall display is PingCanvas's whole job). Keeping the moving parts few is a design choice - and if you want it to become something bigger, the license makes forking genuinely easy.
# docker-compose.yml
services:
snmpcanvas:
build: . # or a published image once available
ports: ["9161:9161"]
volumes: ["./data:/data:z"] # :z = SELinux label; harmless elsewhere
environment:
- TZ=Etc/UTC # your timezone: prune schedule + log stamps
restart: unless-stoppedmkdir -p data && sudo chown 1000:1000 data # container runs as uid 1000
docker compose up -d
Open http://host:9161, set the admin password on the first-run page, and
add a device. That's the whole install. (The default port is a nod to SNMP's
UDP/161, picked to coexist quietly with common home-lab neighbors like
Uptime Kuma on 3001 and CrossCanvas/PingCanvas on 8080/8443.)
Two first-run notes: the setup page belongs to whoever reaches the port
first, so on anything but a trusted segment either set ADMIN_PASSWORD in
the compose file or claim the page immediately after up -d. And the
add-device form defaults to SNMPv2c, which is a cleartext protocol - see
Security posture below.
Run the included script once on the docker host, then restart:
./tools/gen-cert.sh 192.168.1.50 nas.lan # your host's IPs / names
docker compose restart
It writes a self-signed cert to data/certs/server.crt + server.key; the
server detects the pair at startup and switches to HTTPS on the same port
(session cookies become Secure automatically). Prefer a real certificate?
Place your own PEM pair at those two paths (or point TLS_CERT/TLS_KEY
elsewhere) - nothing else changes. Delete the files to fall back to HTTP.
If you mount a different host directory at /data (say /srv/noc-data),
the certs belong in that directory's certs/ subfolder - tell the script
with CERT_DIR=/srv/noc-data/certs ./tools/gen-cert.sh .... And if HTTPS
doesn't come up after a restart, the server stayed on HTTP because it
couldn't use the cert - docker compose logs snmpcanvas | grep -i tls names
the cause, which is almost always one of two things: the pair isn't at
<data>/certs/server.crt + server.key, or it isn't readable by uid 1000
(sudo chown -R 1000:1000 <data>/certs fixes that one).
Put host-specific settings (volume paths, environment variables, ports) in a
docker-compose.override.yml next to the compose file - Docker Compose
merges it automatically, and it's gitignored so updates never conflict with
your edits:
# docker-compose.override.yml (example)
services:
snmpcanvas:
volumes:
- /projects/noc-data:/data:z # replaces ./data (same container path)
environment:
- TZ=America/Chicagogit pull
sudo docker compose up -d --build
up -d --build rebuilds the image and recreates the container only when
something changed; the data directory is a bind mount, so devices, history,
and settings ride through every update (schema migrations run automatically
on first boot). Old image layers accumulate over time - an occasional
sudo docker image prune -f tidies them up.
Node 20+: npm install && npm start (listens on :9161, data in ./data).
| Variable | Default | Purpose |
|---|---|---|
PORT |
9161 |
HTTP/HTTPS listen port |
SNMPCANVAS_DATA |
/data |
Directory for the SQLite db, certs, and default export file |
TLS_CERT / TLS_KEY |
`$DATA/certs/server.crt | key` |
ADMIN_PASSWORD |
- | Pre-set the UI password (otherwise first-run setup page) |
SNMPCANVAS_SECRET |
- | If set, SNMP credentials are AES-256-GCM encrypted at rest |
POLL_CONCURRENCY |
4 |
Max devices polled simultaneously |
COOKIE_SECURE |
auto | Secure cookies: on with HTTPS, off with HTTP; set to override |
TZ |
UTC | Timezone for the nightly prune and log timestamps |
Polling interval, retention days, and the export path are set in the UI (Settings) and stored in the database.
SNMPCanvas is a networked app with a small, deliberate threat model:
- SNMPv1/v2c has no encryption and no real authentication: the community string travels in cleartext in every packet, and so does everything it returns. Use v3 with authPriv where the device supports it, restrict SNMP by source address on the device, and keep polling traffic on a trusted VLAN. Never expose the poller or the monitored agents to the internet.
- SNMP credentials are stored in the SQLite database because the poller
sends them on every cycle. By default the protection is filesystem
permissions on the
/datavolume; setSNMPCANVAS_SECRETand they are AES-256-GCM encrypted with a key derived from your secret instead (lose the secret, re-enter the credentials). The same applies to database backups downloaded from Settings: withoutSNMPCANVAS_SECRET, the.dbfile carries every community string and v3 key in the clear - treat it accordingly. - The web UI has one shared password and is designed for a trusted network
segment; a reverse proxy adds TLS termination and extra auth cleanly if
you want to go further. The first-run setup page belongs to whoever
reaches it first - claim it promptly or pre-set
ADMIN_PASSWORD. - SNMP polls leave the container as outbound UDP/161 through Docker's NAT,
so devices see the docker host's IP. If your devices restrict SNMP by
source address, allow the host IP - or run the container with
network_mode: hostor a macvlan.
Everything - devices, credentials, history, interface codes - lives in one
SQLite file, snmpcanvas.db in the data volume. To move an instance to another
machine (or restore a backup):
- Shut down first, then copy. On
docker compose down, SNMPCanvas checkpoints the write-ahead log back into the main file, so a clean stop leavessnmpcanvas.dbself-contained - copy just that one file. If you can't be sure it stopped cleanly (adocker kill, a power loss), copy all three ofsnmpcanvas.db,snmpcanvas.db-wal,snmpcanvas.db-shmtogether and let SQLite merge them on open. The Download database backup button in Settings sidesteps this entirely - it writes a consistent snapshot regardless of running state. - Carry
SNMPCANVAS_SECRETwith the file. If the source instance encrypts credentials (the secret is set), the community strings and v3 keys are only decryptable with that same secret - set the identicalSNMPCANVAS_SECRETon the target, or polling fails on every device (the raw ciphertext gets handed to SNMP). A database with no secret carries its credentials in cleartext, so treat the file like a password store when moving it. - The admin password travels too (its hash is in the file), so the target logs in with the source's password and skips the first-run setup page.
Turning on encryption for an existing database - no rebuild needed. Setting
SNMPCANVAS_SECRET does not retroactively encrypt credentials already stored in
cleartext (encryption happens when a credential is written, not at startup). But
you don't have to start over: set the secret, then re-save each device's
credentials (Edit device -> Save, e.g. when you change a community off
public). That write encrypts them. Re-saving credentials leaves the interface
codes untouched, so any PingCanvas board bound to those codes keeps working -
no board changes, no history lost.
Every poll cycle, everything marked Export is written atomically to one
file (default /data/snmp-status.json). Interfaces go to interfaces[];
every other exported sensor - CPU, memory, disk, temperature, fan, power,
utilization, plus per-device uptime (Edit dialog) - goes to metrics[] as a
code plus a short pre-formatted display string, so a consumer like
PingCanvas can stay a dumb "code -> text" swapper:
{
"schemaVersion": 2,
"interfaces": [ ... ],
"metrics": [
{ "code": "H4TN", "kind": "cpu", "host": "compute-01",
"display": "CPU 45%", "value": 45, "unit": "%", "status": "ok",
"sampledAt": "2026-07-17T01:10:42Z" }
]
}kind:"cpu" (ok under 85%, warn to 94%, crit at 95%+) and
kind:"battery" (ok, warn/crit as charge drops) carry a coloring
status; every other kind is display-only, so a wall shows the number without
screaming about it. The status field is forward-safe either way: today's kiosk
colors a device frame from CPU status and ignores the rest until a coloring
gate opens. Unavailable values keep their entry with display: "--". Export checkboxes live in the interface table (interfaces),
the Sensors dialog (everything else), and the device Edit dialog
(uptime); each exported item shows its code chip in the UI, already wrapped
in braces and copied to the clipboard on click.
On a PingCanvas board, wrap the code in braces - {K7Q2} - in a device
label line or a connection annotation. Braces are required on labels and
optional on annotations, so the braced form always works; text around the
token is the board author's own (Rx {K7Q2} renders as Rx ▼56M ▲32M).
The v1 interfaces[] shape is unchanged:
{
"schemaVersion": 1,
"generator": "snmpcanvas/0.1.0",
"generatedAt": "2026-07-16T14:05:03Z",
"interfaces": [
{
"id": "core-sw1:eth0",
"code": "K7Q2",
"device": { "name": "core-sw1", "host": "10.0.0.2", "status": "up" },
"ifIndex": 1,
"name": "eth0",
"alias": "uplink to fw",
"speedBps": 1000000000,
"adminStatus": "up",
"operStatus": "up",
"sampledAt": "2026-07-16T14:05:01Z",
"inBps": 12345678.9,
"outBps": 234567.1,
"inErrorsPerSec": 0,
"outErrorsPerSec": 0,
"inDiscardsPerSec": 0,
"outDiscardsPerSec": 0
}
]
}Rates are null when unknown (first poll after add/reboot, device down);
metadata is retained while a device is down so a dashboard can grey the tile
out instead of losing it.
code is a short, stable key for consumers that don't want to type the full
id - and interfaces, sensors, and uptime all mint the same kind of code:
4+ characters (uppercase, confusables like 0/O and 1/I excluded) derived
from md5(deviceName:entityName), lengthened only on hash collision, minted
once and stored. A code survives un-export/re-export, rediscovery, and even
deleting and re-adding a device - the same device and entity names produce
the same code. Every code shows as a clickable chip in the UI.
Because the export path defaults to /data/snmp-status.json and /data is
a bind mount, the file lands on the docker host - another container on
the same host (a PingCanvas deployment, say) can bind-mount that directory
read-only and ingest it directly. To write it somewhere else, add a second
volume (e.g. - /srv/dashboards:/export) and set Settings → export path to
/export/snmp-status.json.
At add time SNMPCanvas GETs the system group, then walks:
ifTable/ifXTable- interfaces, with 64-bit counters when available (per-interface fallback to 32-bit with a sanity clamp, and a heads-up warning on fast links that only offer 32-bit counters),hrProcessorLoadandhrStorageTable(HOST-RESOURCES-MIB) - CPU, RAM, and fixed disks on Linux, Windows, and many appliances,- temperature and fan sensors - LM-SENSORS-MIB (lmsensors on Linux/Proxmox; TrueNAS exposes per-drive temps this way), the standard ENTITY-SENSOR-MIB, vendor health OIDs (Cisco ENVMON, MikroTik), and the ASRock Rack BMC sensor table - IPMI controllers make great SNMP devices in their own right, reading fan tachometers and temperatures straight off the hardware even when the host OS can't. Junk readings (unconnected headers, 0 °C placeholders, "Not Available" rows) and redundant per-core sensors are listed but left untracked by default,
- the vendor map in
server/oids.js, matched bysysObjectIDprefix - vendor CPU/memory OIDs for network devices that don't speak HOST-RESOURCES. Cisco (CISCO-PROCESS-MIB,CISCO-MEMORY-POOL-MIB) is included, and adding a vendor is one data entry. PRs with tested entries are very welcome.
Devices that expose none of the CPU/memory tables simply don't show those cards - interfaces still work fully.
Anything a shell command can print becomes a sensor. On the monitored host,
add extend directives to /etc/snmp/snmpd.conf whose names pick the
kind by prefix - temp- (°C), fan- (RPM), power- (watts), util-
(percent), batt- (battery charge %), runtime- (seconds remaining) - and
whose output is a single number:
extend temp-GPU /usr/bin/nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits
extend power-GPU /usr/bin/nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits
extend util-GPU /usr/bin/nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits
Restart snmpd, rediscover the device, and the outputs appear as sensor
cards with history. This is the doorway for data SNMP can't see natively:
NVIDIA GPUs (no hwmon - AMD GPUs surface through lm-sensors automatically),
USB UPSes, anything scriptable. The command runs under the agent's
account on the monitored host - SNMPCanvas only ever reads the published
number. (NVIDIA note: fan.speed reports percent, not RPM - name it
util-GPU-Fan, not fan-.)
A USB UPS via NUT is the canonical battery
example - upsc already prints bare numbers:
extend batt-UPS1 /usr/bin/upsc ups battery.charge
extend runtime-UPS1 /usr/bin/upsc ups battery.runtime
extend util-UPS1-Load /usr/bin/upsc ups ups.load
Battery cards alarm LOW (red at 20% and below), runtime displays humanize
(Runtime 1h 0m), and exported battery metrics carry an ok/warn/crit
status - forward-safe under the kiosk contract, which ignores status on
non-CPU kinds until a coloring gate opens.
If a device renumbers its ifIndexes (some do, after reboots or module
changes), affected interfaces are flagged stale in the UI; use
Rediscover on the device page to reconcile - new entities are added,
vanished ones stop being polled but keep their history. On Cisco,
snmp-server ifindex persist avoids the situation entirely.
npm install
npm run mock-agent # fake SNMP device on udp/16100 (v2c "public", v3 "labuser")
npm start # UI on http://localhost:9161
Add 127.0.0.1:16100 as a device and you have moving graphs without any
hardware.
| Path | Purpose |
|---|---|
server/server.js |
HTTP entry point: static files + API dispatch (plain node:http) |
server/api.js |
All /api/* handlers |
server/oids.js |
Every OID used, plus the vendor map - the file to extend |
server/snmp.js |
net-snmp wrapper (v2c/v3 sessions, walks, Counter64 handling) |
server/discover.js |
Add-device probe and table walks |
server/poller.js |
Tick scheduler, rate math, up/down, nightly prune |
server/exporter.js |
snmp-status.json writer |
server/db.js / auth.js |
SQLite schema and migrations; scrypt password + sessions |
public/ |
The whole frontend: vanilla HTML/CSS/JS, no build step |
tools/mock-agent.js |
Fake device for development |
Runtime dependencies:
net-snmp and
better-sqlite3 - the
complete list, by design.
Bug reports are welcome via Issues, and tested vendor-map entries are
especially useful: if your device's CPU, memory, or temperature OIDs
aren't discovered, an issue with a sanitized snmpwalk (or a one-entry PR
against server/oids.js) is what makes support for it happen. Small,
self-contained fixes are welcome as pull requests too.
For larger features - alerting, discovery, rollups, and the like - I'd rather you fork than open a big PR. SNMPCanvas is deliberately small, the whole backend is nine readable files, and The Unlicense means you owe nobody anything. Build the monitor you want.
SNMPCanvas stands on two excellent MIT-licensed libraries:
- net-snmp by Mark
Abrahams, Stephen Vickers, and contributors - the pure-JavaScript SNMP
engine behind every poll, walk, and v3 handshake (its agent support powers
tools/mock-agent.jstoo). - better-sqlite3 by Joshua Wise and contributors - the synchronous SQLite bindings that keep the storage layer a single dependency, wrapping the public-domain SQLite library itself.
The visual language is borrowed from CrossCanvas, SNMPCanvas's sister project.
The Unlicense - public domain, same as CrossCanvas, PingCanvas,
and SyslogCanvas. Use it, fork it, ship it at work, no attribution required.
(Dependencies keep their own MIT licenses in node_modules/ when you
install or ship an image.)
