// as2.no — server wiki
Documentation and setup notes for the homelab server.
# Proxmox
Type-1 hypervisor (Proxmox VE) running the home lab. The as2.no host (Server4-Ubuntu2) is itself VM 104 on this hypervisor. Administered from as2.no over SSH as root (key auth). Last updated 2026-06-15.
VM memory reads high in Proxmox — that's cache, not a leak. Proxmox's per-VM gauge counts everything the guest has touched (incl. Linux page cache), so it shows ~85–90% for a VM using far less. Real usage is on the Linux Server page (from inside the guest, cache excluded) — e.g. VM 104 reads ~87% in Proxmox but ~41% real. qemu-guest-agent is installed (fixes graceful shutdown, fs-freeze backups, guest-IP reporting) but on PVE 9.x it does not change this gauge. Trust the guest/Linux-Server number for real usage.
| Setting | Value |
|---|---|
| Node | Server4-Proxmox |
| Location | Bod — cabled to C2960CX-3 (.2); reaches LAN over the Bod AP's wireless backhaul |
| CPU | Intel Core i7-9700 (8C/8T @ 3.0 GHz, turbo 4.7) |
| RAM | 64 GiB |
| Version | Proxmox VE 9.1.4 (kernel 6.17.4-2-pve) |
| Installed | 2024-05-09 (originally PVE 8.2) |
| System disk | Samsung 960 EVO 1TB NVMe |
| LAN IP | 192.168.10.20 |
| Web UI | https://proxmox.as2.no (valid cert; also https://192.168.10.20:8006) |
| Network | vmbr0 bridge → enp3s0, gateway 192.168.10.1 |
| Admin access | SSH key (root) from as2.no |
| VMID | Name | RAM | Disk | State | Notes |
|---|---|---|---|---|---|
| 101 | Server4-Win | 10 GB | 100 GB | running | Windows; 6 passthrough drives (see disks below) |
| 102 | Server4-Win11-test | 4 GB | 64 GB | running | Windows 11 test VM |
| 104 | Server4-Ubuntu2 | 4 GB | 40 GB | running | as2.no host (this wiki) |
| 105 | CML | 8 GB | 50 GB | stopped | Cisco Modeling Labs |
| 106 | eve-ng | 16 GB | 40 GB | stopped | EVE-NG network emulator |
| Storage | Type | Size | Used | Purpose |
|---|---|---|---|---|
| local | dir | 96 GB | 25% | ISOs, templates, host backups |
| local-lvm | lvmthin | 832 GB | 20% | VM disk images (on the 960 EVO NVMe) |
| Server4-Win-backup | cifs | 7.8 TB | 29% | SMB share for VM backups |
SMART status of the physical drives passed through to VM 101 (Server4-Win). Last checked 2026-06-21.
| Drive | Device | Win | SMART | Power-on | Status |
|---|---|---|---|---|---|
| WD Blue SN570 2TB NVMe | nvme0n1 | J: | PASSED | 18,698 h | OK — 0% wear, 100% spare |
| Samsung 860 EVO 1TB SSD | sda | F: | PASSED | 56,513 h | OK — low wear, 0 reallocated |
| Seagate ST8000AS0002 (Z8401TPQ) | sdc | H: | PASSED | 20,181 h | OK — 0 pending, 0 CRC |
| Seagate ST8000AS0002 (Z8401V6Z) | sdb | I: | PASSED* | 20,161 h | FAILING — self-test read failure at LBA 273408 |
| Seagate ST8000DM004 (WCT03T91) | sde | E: | PASSED | 74,101 h | AGING — clean but ~8.5 yr runtime |
| Seagate ST8000DM004 (WCT04458) | sdd | D: | PASSED | 74,101 h | AGING — clean but ~8.5 yr runtime |
sdb(Z8401V6Z) is failing — a SMART short self-test (2026-06-05) ended in a read failure at LBA 273408, a confirmed bad sector unrepaired since ~hour 5,427 (~1.6 yr). Also 16 pending / 16 offline-uncorrectable sectors and 21 UDMA CRC errors. Re-checked 2026-06-21 (now ~hour 20,161): identical — counts unchanged and the fault has not spread. *SMART overall still reports PASSED because pending sectors don't trip the threshold — treat it as failing anyway. It holds only the backup copy ofH:(healthy), so no primary data is at risk; replace it when convenient- The two ST8000DM004 drives have ~8.5 years of runtime — plan replacements
- None of these passthrough disks are included in Proxmox backups (
backup=0)
## Backups & offsite (Jottacloud)
VM images are dumped with vzdump (snapshot mode, zstd) to the Server4-Win-backup CIFS share — //192.168.10.3/Proxmox-backup on the Windows fileserver — then Jottacloud on that box uploads them offsite nightly at 02:00. Run manually for now (no scheduled job). Consistency: 101/104 get a guest-agent fs-freeze (101 = Windows/VSS-consistent, 104 = fs-consistent); 105/106 are consistent while stopped; 102 now has the guest agent too (added 2026-07-07), so its backups are fs-consistent.
Run the Proxmox backup BEFORE 02:00. It must finish the .vma.dat → .vma.zst rename before Jottacloud starts — otherwise Jottacloud opens the freshly-written .dat to upload it and Proxmox's rename fails with a sharing violation (unable to rename ... .vma.dat to ... .vma.zst). This silently broke every backup from ~mid-2025 until 2026-07-07; the fix is scheduling Jottacloud (not real-time) and keeping the two from overlapping.
# Re-check one drive's health + attributes ssh root@192.168.10.20 smartctl -H -A /dev/sdb # Manual VM backup -> Windows share (run BEFORE 02:00 / Jottacloud) ssh root@192.168.10.20 vzdump 104 --storage Server4-Win-backup --mode snapshot --compress zstd # Manage VMs (list / start / stop / config) ssh root@192.168.10.20 qm list ssh root@192.168.10.20 qm start 106
# Server4-Win (VM 101)
The Windows 11 file server — VM 101 on the Proxmox host. Six physical drives are passed straight through from the host (raw NVMe/SATA passthrough), so Windows owns and formats them directly. The QEMU guest agent is enabled, so the disks can be queried remotely from as2.no. Files are shared over FTPS via FileZilla Server (explicit TLS required, min TLS 1.2).
| Setting | Value |
|---|---|
| OS | Windows 11 |
| vCPU | 4 cores (1 socket) |
| RAM | 10 GiB |
| Firmware | UEFI (OVMF), q35 |
| System disk | 100 GB on local-lvm → C: |
| File sharing | FileZilla Server — explicit FTPS on port 21212 (PASV 50000–50100) |
| Guest agent | enabled |
Physical disk → Windows drive-letter map. Windows reports generic QEMU HARDDISK serials, so drives are matched by SATA port order (cross-checked against size and the QEMU serial sequence).
| Win disk | Drive | Size | Letter | Host dev | Notes |
|---|---|---|---|---|---|
| 0 | WD Blue SN570 2TB NVMe | 2 TB | J: | nvme0n1 | |
| 1 | Samsung 860 EVO 1TB | 1 TB | F: | sda | |
| 2 | Seagate ST8000AS0002 (Z8401TPQ) | 8 TB | H: | sdc | healthy — primary |
| 3 | Seagate ST8000AS0002 (Z8401V6Z) | 8 TB | I: | sdb | FAILING — backup of H: |
| 4 | Seagate ST8000DM004 (WCT03T91) | 8 TB | E: | sde | aging — primary |
| 5 | Seagate ST8000DM004 (WCT04458) | 8 TB | D: | sdd | aging — backup of E: |
| 6 | OS disk (local-lvm) | 100 GB | C: | — | Windows system |
| 7 | Seagate Expansion (USB) | 8 TB | G: | — | external, not passthrough |
- Backup pairs:
I:backs upH:, andD:backs upE: I:(failing, Z8401V6Z) only holds the backup copy ofH:, which is healthy — so no primary data is at immediate risk. OK for now; replaceI:when convenient, but until then theH: → I:backup is unreliableG:is a separate external USB drive (Seagate Expansion)
# Server4-Win11-test (VM 102)
Scratch Windows 11 VM for testing — VM 102 on the Proxmox host.
| Setting | Value |
|---|---|
| OS | Windows 11 |
| vCPU | 2 cores (1 socket) |
| RAM | 4 GiB |
| Firmware | UEFI (OVMF), q35 |
| Disks | 64 GB system + 32 GB scratch (local-lvm) |
| State | running |
# Server4-Ubuntu2 (VM 104)
The Ubuntu host serving as2.no — VM 104 on the Proxmox box (see Proxmox).
| Component | Details |
|---|---|
| Host | VM 104 on Server4-Proxmox |
| OS | Ubuntu 24.04.4 LTS |
| CPU | 4 vCPUs @ ~2GHz |
| RAM | 3.8 GiB |
| Disk | 40 GiB |
| Local IP | 192.168.10.15 |
| Public IP | 85.165.99.110 |
# CML (VM 105)
Cisco Modeling Labs (CML 2.10.0) — Cisco's official network simulation platform. Runs virtual topologies of real Cisco node types (IOS, IOS-XE, IOS-XR, NX-OS) for building and testing network designs — certification study (CCNA / CCNP / CCIE) and pre-deployment validation. VM 105, normally powered off; start it when needed.
Upgraded 2.8 → 2.10.0 on 2026-07-08. Gotcha for next time: CML 2.10+ pulls docker-ce from download.docker.com (via the new cml-docker-shim package), but the repo line in /etc/apt/sources.list.d/docker.list ships commented out. Result: the Cockpit upgrade reports SUCCESS (code 0) but silently stays on the old version, because docker-ce is "not installable" so the whole cml2 upgrade is held. Fix: uncomment the deb ...download.docker.com... noble stable line, sudo apt-get update (confirm apt-cache policy docker-ce shows a candidate), then re-run the Cockpit upgrade and reboot. Two attempts failed silently before we found this.
| Setting | Value |
|---|---|
| OS | Linux (CML 2.8.0) |
| vCPU | 4 cores (1 socket) |
| RAM | 8 GiB |
| Firmware | UEFI (OVMF) |
| Disk | 50 GB (local-lvm) |
| State | stopped |
# eve-ng (VM 106)
EVE-NG Community Edition (6.2.0) — a multi-vendor network emulation platform. Runs virtual devices from many vendors (Cisco, Juniper, Arista, Fortinet and more) in a single topology, complementing CML for multi-vendor labs and certification practice. VM 106, normally powered off; start it when needed.
| Setting | Value |
|---|---|
| OS | Linux (EVE-NG CE 6.2.0) |
| vCPU | 4 cores (1 socket) |
| RAM | 16 GiB |
| Disk | 40 GB (local-lvm) |
| State | stopped |
# Jellyfin (CT 111)
Self-hosted media server for movies & TV, in a privileged LXC on Proxmox (host). Hardware transcoding runs on the host's Intel iGPU (QuickSync); the library is mounted read-only over CIFS from Server4-Win. Served at https://jellyfin.as2.no (valid cert; nginx → container :8096) — reachable on the LAN, over WireGuard, and publicly on the internet (family access, no VPN) as of 2026-07-31.
| Setting | Value |
|---|---|
| OS | Debian 12 (LXC, privileged) |
| vCPU | 4 cores |
| RAM | 4 GiB |
| Disk | 16 GB (local-lvm) — OS + metadata; media on the share |
| Web UI | jellyfin.as2.no / 192.168.10.11:8096 |
| Transcoding | Intel QuickSync (UHD 630) via VAAPI + HDR tone-map |
| Media | Movies + Series, read-only CIFS from 192.168.10.3 |
- QuickSync passthrough:
/dev/dribound into the CT (device allows in/etc/pve/lxc/111.conf);jellyfinuser inrender+video;intel-opencl-icdfor HDR tone-mapping - Media: host CIFS mounts (
ro, root-only creds) bind-mounted into the CT at/media/Moviesand/media/Series - Reverse proxy: nginx on
192.168.10.15→192.168.10.11:8096(websockets); sharedas2.noLet's Encrypt cert (see HTTPS); JellyfinKnownProxies= .15 - Public exposure: ASUS router forwards TCP 443 → .15; hardened at nginx with a login rate-limit (
60r/mon/Users/AuthenticateByName) + afail2banjail (5 fails / 10 min → 1 h ban, LAN/WG whitelisted) + HSTS/security headers - Up/down + version on the Services page via the summary poller
# Networking
- Firewall: UFW — ports 80/443 restricted to
192.168.10.0/24(local only) - DNS: Domeneshop —
as2.noandarildsorli.noA records point to85.165.99.110 - HTTPS / TLS: Let's Encrypt (DNS-01) — see the HTTPS section
- LAN DNS resolver: dnsmasq on
192.168.10.15— see dnsmasq
# Switches — Cisco Catalyst 2960CX
Uniform four-switch access layer, one per room (model WS-C2960CX-8PC-L, 8-port PoE). SSH key auth on all four; IOS version, health (CPU / mem / flash / temp / uptime) and up/down status are polled live over SNMP v2c and shown on the network topology page. All four currently up on IOS 15.2(7)E14 (fleet-upgraded from 15.2(7)E8).
| Hostname | IP | Location | IOS | Role |
|---|---|---|---|---|
| C2960CX-1 | 192.168.10.4 | Stue | 15.2(7)E14 | feeds WLC root AP |
| C2960CX-2 | 192.168.10.5 | Kjellerstue | 15.2(7)E14 | — |
| C2960CX-3 | 192.168.10.2 | Bod | 15.2(7)E14 | Proxmox uplink |
| C2960CX-4 | 192.168.10.6 | Kontor | 15.2(7)E14 | via AP4800-4 wireless backhaul |
- Monitored via SNMP v2c by Telegraf; each switch forwards syslog to
192.168.10.15:514→ Loki (see Syslog pipeline) - SSH config and how it's applied are documented under device-side config
# Wireless — Cisco AP 4800 + WLC
Four Cisco Aironet AP4800s, one per room, joined to an embedded WLC (Mobility Express) running on AP4800-1. AP4800-1 (Stue) is the wireless root and backhauls to each room's AP; the controller is reachable at the VIP 192.168.10.7, AireOS 8.10.196.0 (version read live from the poller). Live WiFi client counts per radio are on the monitoring page.
| AP | Mgmt IP | Location | MAC | Notes |
|---|---|---|---|---|
| AP4800-1 | 192.168.10.120 | Stue | 78:72:5d:fb:8f:ea | WLC ME controller — VIP .7 |
| AP4800-2 | 192.168.10.152 | Kjellerstue | 78:72:5d:3f:9a:c2 | — |
| AP4800-3 | 192.168.10.132 | Bod | 78:72:5d:fb:8f:f6 | — |
| AP4800-4 | 192.168.10.105 | Kontor | 78:72:5d:3f:89:2c | wireless backhaul for C2960CX-4 |
- Controller VIP
192.168.10.7; the WLC also forwards syslog under host labelAP4800-1 - SNMP RO community is locked to
.15— see device-side config
# Router — ASUS (192.168.10.1)
The ASUS router is the LAN gateway and WAN edge. It serves DHCP, holds the public IP (dynamic, kept current by the Domeneshop updater), and forwards only WireGuard 51820/udp inbound. SSH-polled every 30s for per-interface throughput and WiFi-client counts.
| Setting | Value |
|---|---|
| Role | LAN gateway · default route |
| LAN IP | 192.168.10.1 |
| Public IP | 85.165.99.110 (dynamic, DDNS) |
| DHCP pool | 192.168.10.100 – .250 |
| Inbound | WireGuard 51820/udp only |
| Monitoring | SSH poller, 30s (rx/tx per interface, WiFi clients per radio) |
# dnsmasq (LAN DNS)
Local DNS resolver for the home network — forwards queries upstream and supports custom A records.
| Setting | Value |
|---|---|
| DNS port | 53/tcp+udp (LAN only) |
| Upstream | 208.67.222.222, 208.67.220.220 (OpenDNS) |
| Config | ~/dnsmasq/dnsmasq.conf |
| Bind address | 192.168.10.15 |
- Point router's DNS server to
192.168.10.15to use for all LAN clients - Add custom hostnames in
dnsmasq.confwithaddress=/hostname.local/192.168.x.x *.as2.noresolves to192.168.10.15on the LAN- UFW allows port 53 from
192.168.10.0/24only
# Start / restart cd ~/dnsmasq docker compose up -d # View logs docker logs -f dnsmasq # Test a query from the LAN dig @192.168.10.15 google.com
# WireGuard VPN
Self-hosted VPN via wg-easy — a Docker image that bundles WireGuard with a web UI for managing clients.
| Setting | Value |
|---|---|
| Host | as2.no |
| VPN port | 51820/udp |
| Web UI port | 51821/tcp |
| DNS | 1.1.1.1 (Cloudflare) |
| Config | ~/wg-easy/docker-compose.yml |
- WireGuard config stored in Docker volume
etc_wireguard - Web UI at
https://wg.as2.no(valid cert; nginx -> container :51821) — manage clients, download configs - Requires kernel capabilities
NET_ADMINandSYS_MODULE - IP forwarding enabled via
net.ipv4.ip_forward=1
# Start / restart cd ~/wg-easy docker compose up -d # View logs docker logs -f wg-easy
# HTTPS — Let's Encrypt (DNS-01)
TLS certificates issued by Let's Encrypt using the DNS-01 challenge — no public port 80 required.
| Item | Details |
|---|---|
| Plugin | certbot-dns-domeneshop |
| Domain | as2.no (+ *.as2.no wildcard) |
| Cert path | /etc/letsencrypt/live/as2.no/ |
| Renewal | Auto via systemd timer |
- DNS-01 works by creating a temporary
_acme-challengeTXT record at Domeneshop - Plugin credentials stored in
/etc/letsencrypt/renewal/as2.no.conf - Nginx uses
fullchain.pemandprivkey.pemfrom the cert path
# Check certificate expiry sudo certbot certificates # Force renewal (for testing) sudo certbot renew --dry-run # Check renewal timer systemctl status snap.certbot.renew.timer
# Domeneshop IP Updater
Monitors the server's public IP and automatically updates the as2.no A record at Domeneshop when it changes.
- Domains:
as2.no,arildsorli.no - Script:
~/domeneshop-ip-updater/updater.py - Credentials:
/etc/domeneshop-ip-updater/config(chmod 600) - State file:
/var/lib/domeneshop-ip-updater/last_ip - Runs every 5 minutes via
domeneshop-ip-updater.timer - GitHub: github.com/arildsorli/domeneshop-ip-updater (private)
# Check logs journalctl -u domeneshop-ip-updater.service -f # Check timer status systemctl status domeneshop-ip-updater.timer
# Monitoring stack
Two pipelines, both Docker Compose at ~/monitoring on .15 (its own git repo). Metrics: Telegraf → InfluxDB v2 → Grafana. Logs: devices → AxoSyslog (514) → Loki → Grafana. Embedded dashboards are on the monitoring page; the v2 overview cards come from the host pollers.
| Container | Image | Port | Role |
|---|---|---|---|
| grafana | grafana/grafana:11.3.0 | :3000 | dashboards (nginx → grafana.as2.no) |
| influxdb | influxdb:2.7 | 127.0.0.1:8086 | metrics TSDB — bucket network, org as2 |
| telegraf | telegraf:1.32 | — | SNMP poller (switches + WLC) + host metrics |
| loki | grafana/loki:3.2.1 | 127.0.0.1:3100 | log store — filesystem, 30-day |
| axosyslog | axoflow/axosyslog:4.8.1 | :514 udp+tcp | syslog receiver → Loki (see Syslog) |
What Telegraf collects (every 30s): SNMP v2c from the four 2960CX switches and the AireOS WLC at .7 — interface counters/errors, CPU, memory, flash, temperature, uptime, plus each switch's IOS sysDescr and the WLC's swVersion (these drive the live version badges on the topology page) and per-AP client counts. It also reads this server's own CPU / memory / disk / NIC via a read-only /hostfs mount.
- Grafana access: nginx vhost
grafana.as2.no→127.0.0.1:3000(also:3000direct on the LAN), on the as2.no Let's Encrypt cert. Anonymous Viewer + embedding are enabled so the dashboardiframes render; admin password is set in the UI (.envholds only the bootstrap value) - InfluxDB data model: measurements
asus_netdev(WAN/wired/wireless rx/tx),snmp_interface(switch + WLC interface counters/errors),switch_cpu/switch_mem/switch_env/switch_flash,wlc_ap_clients, and hostcpu/mem/disk/net. Throughput = non-negative derivative × 8 (bits/s) - Loki: single-binary, filesystem store, 30-day retention; labels
host·program·severity·facility - Secrets (InfluxDB token, Grafana pw, SNMP communities) live in
~/monitoring/.env(chmod 600, never committed)
# Syslog pipeline (AxoSyslog → Loki)
Switches, the WLC and the ASUS router forward syslog to 192.168.10.15:514. AxoSyslog normalises every format (including Cisco's non-standard frame), maps source IPs to friendly host names and ships to Loki — searchable in Grafana and on the monitoring page by host, severity, program and facility.
| Setting | Value |
|---|---|
| Receiver | AxoSyslog on .15:514 (udp/tcp) |
| Store | Loki — 30-day retention, 127.0.0.1:3100 |
| Labels | host · severity · program · facility |
| Sources | router (.1), C2960CX-1…4, WLC/AP (.7 → label AP4800-1) |
# Host pollers
Small scripts on .15 (in ~/monitoring/scripts, run by systemd timers) that feed data the SNMP path can't. Two write straight to InfluxDB; two emit the JSON the v2 dashboard reads — which is why the cards refresh roughly every minute.
| Timer | Every | Script | Output |
|---|---|---|---|
| asus-wan-poll | 30s | asus_wan_poll.sh | WAN / per-iface rx/tx → InfluxDB asus_netdev |
| asus-wifi-poll | 30s | asus_wifi_poll.sh | WiFi clients per radio → InfluxDB |
| client-inventory | 5 min | client_inventory.py | data/clients.json — merged roster (router · DHCP · ARP · Cisco MAC) |
| as2-summary | 60s | as2_summary.py | data/summary.json — overview cards |
| prune-code-extensions | daily | prune-code-extensions.sh | keeps newest version of each code-server extension (they never self-prune — Claude Code had grown to ~7 GB) |
| update-ieee-data | monthly | update-ieee-data -f | refreshes the IEEE OUI database so client_inventory resolves vendors from MAC prefixes |
as2_summary.pyassembles the overview: WAN throughput, switches + parsed IOS, WLC AireOS version, WiFi totals, Proxmox VM count + SMART disk health, server CPU/mem/disk, syslog rate- The ASUS pollers SSH in with a dedicated Dropbear key (
~/.ssh/asus_poller_ed25519), independent of the router's single web-admin session - Version-controlled unit files live in
~/monitoring/systemd/; the live copies are in/etc/systemd/system/
# Poller status / logs systemctl status as2-summary.timer journalctl -u as2-summary.service -f # Run a poller once by hand ~/monitoring/scripts/as2_summary.py
# Device-side SNMP/syslog config
What was applied on each device so it reports to the stack on .15. SNMP is read-only and locked to .15; syslog is sent to .15:514. Community strings live in ~/monitoring/.env (shown as placeholders below).
Cisco 2960CX switches (cx-1…4) — in config mode, saved to startup-config:
access-list 99 permit 192.168.10.15 snmp-server community <SNMP_COMMUNITY> RO 99 logging host 192.168.10.15 logging trap informational ! end & write memory
WLC (.7, AireOS Mobility Express) — SNMP RO community locked to .15 + syslog at notifications level. Applied once via ~/monitoring/scripts/wlc_phase2_config.exp <community>:
config snmp community create <WLC_SNMP_COMMUNITY> config snmp community accessmode ro <WLC_SNMP_COMMUNITY> config snmp community ipaddr 192.168.10.15 255.255.255.255 <WLC_SNMP_COMMUNITY> config snmp community mode enable <WLC_SNMP_COMMUNITY> config snmp version v2c enable config logging syslog host 192.168.10.15 config logging syslog level notifications save config
ASUS router (.1) — no SNMP, so it only sends syslog. The helper ~/monitoring/scripts/asus_set_syslog.sh logs into the web UI and sets nvram log_ipaddr=192.168.10.15, applied with restart_logger (no reboot). Its metrics come from the SSH pollers instead of Telegraf.
- The WLC needs a more complex community string than the switches — kept separate as
WLC_SNMP_COMMUNITY - All three forward syslog to AxoSyslog on
.15:514(see Syslog pipeline); only the switches and WLC are polled over SNMP by Telegraf
# Inventory DB & API
One asset table on .15 is the single source of truth behind the v2 clients, IP-plan, hardware and network (switches + APs, also WiFi) pages — each is a filtered view, so an IP or note reads the same everywhere and an edit updates every view. Curated fields are hand-edited in the browser; live fields belong to the poller. These CRT pages are read-only renderings of the same data.
Flow: client_inventory.py UPSERTs live state by MAC → inventory.db ← browser edits via the API; the inventory-api container serves the DB as JSON; render_crt.py regenerates the CRT tables between <!--GEN:*--> markers.
| Piece | Where | Role |
|---|---|---|
| DB | ~/monitoring/inventory/data/inventory.db | SQLite (WAL) — asset + ip_plan_doc; gitignored |
| API | inventory-api · 127.0.0.1:8090 | FastAPI container; nginx proxies /v2/api/ |
| Seed | inventory/migrate.py | one-time import, merged by MAC→IP→name |
| Live writer | scripts/client_inventory.py | UPSERTs live fields every 5 min |
| CRT writer | inventory/render_crt.py | regenerates CRT tables from the DB |
- Merge key is the MAC (stable; IPs move).
ip_static= assigned IP (blank = DHCP); a view showsip_staticor else the liveip_current. Notes are per-page —notes_ip_plan/notes_hardware/notes_network/notes_clients, each view its own note type(host/desktop/laptop/spare/given_away/client/switch/ap) picks the view;switch/apfeed the network & WiFi tables, a hardware form-factor outrankshostso HT-PC shows on hardware whileip_statickeeps it on IP-plan- Live fields (
online,ip_current,last_seen,switch_port,vendor) are poller-owned, never editable - Auth: GET open on the LAN; writes (POST/PATCH/DELETE on
/v2/api/assets) need the shared password — nginxlimit_except GET HEADbasic-auth, userarild, htpasswd/etc/nginx/.as2_inventory_htpasswd. API binds127.0.0.1only - Edit on v2 via the ✎ Edit button on IP-Plan / Hardware / Network (inline edit · add · delete); the Clients roster has an ✎ Edit notes toggle (notes only); password prompted once per session
- All IP labels on v2 are DB-driven: the structured tables, the Topology diagram (
network.js), the Proxmox VM table (liveqm list+ IP from the DB), and inline prose labels via<span data-host-ip="Device Name">filled byapp.js fillHostIPs(). Change a device's IP in the DB and every v2 label follows
# rebuild API / re-seed DB / regenerate CRT cd ~/monitoring && docker compose up -d --build inventory-api python3 ~/monitoring/inventory/migrate.py --write python3 ~/monitoring/inventory/render_crt.py # change the edit password printf 'arild:%s\n' "$(openssl passwd -apr1)" | sudo tee /etc/nginx/.as2_inventory_htpasswd sudo systemctl reload nginx
# Services
| Service | URL | Description |
|---|---|---|
| Homepage | https://as2.no | Nginx, local network only |
| VS Code | https://code.as2.no | code-server browser IDE |
| Portainer | https://portainer.as2.no | Docker container manager |
| Grafana | https://grafana.as2.no | Dashboards & logs |
| dnsmasq | 192.168.10.15:53 | LAN DNS resolver |
| FileZilla | ftps://192.168.10.3:21212 | FTPS file server (on Server4-Win, VM 101) |
# Nginx
Nginx (1.24.0) is the front door for everything on as2.no — it serves the static homepage and reverse-proxies the three other *.as2.no services. Every vhost redirects :80 → :443 and terminates TLS on the shared Let's Encrypt wildcard cert. UFW limits 80/443 to 192.168.10.0/24, so it's reachable on the LAN (or over WireGuard) only.
| server_name | Serves | Backend |
|---|---|---|
| as2.no · www.as2.no | Homepage (static) | root /home/arild/as2-homepage |
| code.as2.no | code-server IDE | proxy → 127.0.0.1:8080 |
| portainer.as2.no | Portainer | proxy → 127.0.0.1:9000 |
| grafana.as2.no | Grafana | proxy → 127.0.0.1:3000 |
- Vhosts live in
/etc/nginx/sites-available/, enabled via symlinks in/etc/nginx/sites-enabled/(one file per*.as2.nohost) - Shared CSP / security headers come from
/etc/nginx/snippets/—security-headers.conf(main site,script-src 'none'),-v2.conf(v2 dashboard + games allowscript-src 'self') and-games.conf - Theme switch: the
as2.novhost reads athemecookie at/—v2redirects to/v2/;/theme/v2and/theme/crtset the cookie. Works even for the no-JS CRT site - Live data proxy:
/v2/grafana/api/ds/queryis a same-origin reverse proxy to127.0.0.1:3000/api/ds/queryso the v2 dashboard fetches interface lists / uptime without CORS (read-only query endpoint only) - Caching: HTML and everything under
/v2/isno-cache(so?v=cache-busters take effect);css/woff2/svg/icocache for 7 days
# Test config, then reload (zero-downtime) sudo nginx -t sudo systemctl reload nginx # Enable a new vhost sudo ln -s /etc/nginx/sites-available/<name> /etc/nginx/sites-enabled/ # Tail access / error logs sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log
# Dev Stack
- Web server: Nginx (serves the site + reverse-proxies code/portainer/grafana)
- Containers: Docker + Docker Compose
- Container UI: Portainer (Docker container on port 9000)
- Observability: Grafana · InfluxDB · Loki · Telegraf · AxoSyslog — see Monitoring stack
- IDE: code-server v4 (port 8080, proxied by Nginx)
- Runtimes: Node.js v24 LTS, Python 3.12, pip
- Version control: git + GitHub CLI (
gh)
# Docker Workflows
Most services on .15 run as Docker containers, managed with Docker Compose across a few project directories.
| Stack | Containers | Purpose |
|---|---|---|
| ~/wg-easy | wg-easy | WireGuard VPN + web UI |
| ~/dnsmasq | dnsmasq | LAN DNS resolver |
| ~/monitoring | grafana · influxdb · loki · telegraf · axosyslog | observability stack (see Monitoring) |
| standalone | portainer | Docker management UI |
# List running containers docker ps # View resource usage docker stats # View logs for a container docker logs -f <container-name> # Restart a container docker compose -f ~/wg-easy/docker-compose.yml restart # Pull latest image and recreate container docker compose pull docker compose up -d # Remove unused images / volumes docker system prune
# as2-homepage
This website. Two maintained themes — the classic CRT terminal look (this page) and a modern v2 NOC dashboard — built with plain HTML, CSS and a little vanilla JS. No frameworks.
| Item | Details |
|---|---|
| URL | https://as2.no |
| Stack | HTML, CSS, Nginx |
| Source | ~/as2-homepage/ |
| Repo | github.com/arildsorli/as2-homepage (private) |
- Served directly from
~/as2-homepage/by Nginx (v2 lives inv2/) - HTTPS via Let's Encrypt DNS-01 challenge
- Theme switch is cookie-based; CRT keeps the scanlines, vignette, blinking cursor and ASCII header
cd ~/as2-homepage # edit files... git add . git commit -m "describe change" git push