Services / Note 01
Open Lab Frame
In this note
Public self-hosted portfolio and homelab website running from Raspberry Pi 5 behind Cloudflare Tunnel and Nginx.
Homelab Overview | Raspberry Pi 5 | VM101 - OpenLabFrame Dev | Uptime Kuma | Beszel | Network Topology
Status
State: In use / Production
Public Domain: https://openlabframe.com
Production Host: Raspberry Pi 5
Network: Private LAN
Web Server: Nginx
Nginx Container: ehworld
Public Ingress: Cloudflare Tunnel
Development VM: VM101 - OpenLabFrame Dev
Source Repository: <private Open Lab Frame source repository>
Open Lab Frame is the public-facing website for documenting the homelab, projects, infrastructure, and technical work.
Purpose
Open Lab Frame serves as the public presentation layer for the overall IT/homelab project.
Its goals include:
Documenting homelab projects
Showing current hardware
Showing deployed services
Presenting technical work publicly
Providing a long-term portfolio
Demonstrating self-hosting
Demonstrating deployment and infrastructure skills
Showing project progression over time
The website itself is intentionally separated from the private administrative homelab.
Public URL
Main website:
https://openlabframe.com
Public traffic reaches the site through Cloudflare rather than through direct router port forwarding.
Production Architecture
Current production path:
Visitor
↓
Internet
↓
Cloudflare
↓
Cloudflare Tunnel
↓
Raspberry Pi 5
<private Pi 5 host>
↓
Nginx container
ehworld
↓
Open Lab Frame production files
The site is therefore accessible publicly even though the home Starlink connection is behind CGNAT.
Why Cloudflare Tunnel Is Used
The home connection uses Starlink.
Rather than exposing Nginx through normal public IPv4 port forwarding:
Internet
X
Home Router Port Forward
X
Pi 5
the Pi 5 establishes an outbound Cloudflare Tunnel.
Pi 5
↓
cloudflared
↓
Cloudflare
↑
Public Visitor
This works without requiring a publicly routable home IPv4 address.
Production Host
Production runs on:
Raspberry Pi 5
Current system:
Hostname: <pi5-hostname>
User: <admin-user>
Network: Private LAN
RAM: 16 GB
Storage: 1 TB NVMe
The Pi 5 is the always-on infrastructure host for the site.
Nginx
Production Nginx runs in Docker.
Container:
ehworld
The container serves the built static Open Lab Frame files.
Current production directory:
<PRODUCTION_SITE_DIRECTORY>
Production Directory
Active website:
<PRODUCTION_SITE_DIRECTORY>
This directory contains the production dist build that Nginx actually serves.
It should not be treated as the primary editable source.
Staging Directory
Deployment staging directory:
<STAGING_DIRECTORY>
New builds are extracted and checked here before replacing production.
This provides a safety boundary between:
Uploaded build
and:
Live website
Production Backups
Timestamped live backups use paths such as:
<PRODUCTION_SITE_DIRECTORY>-backup-YYYYMMDD-HHMMSS
Older examples also exist under:
<PRODUCTION_BACKUP_DIRECTORY>
The live site is backed up before deployment.
Source vs Production
A critical Open Lab Frame design rule is:
Readable source
≠
Production build
The private GitHub repository contains readable development source.
The public Pi 5 serves the generated hardened production build.
Private Source Repository
Repository:
<private Open Lab Frame source repository>
The source repository is the authoritative editable website source.
Normal work should happen in:
src/
not directly inside the live Pi 5 production directory.
Current Source Structure
The current readable source baseline contains files such as:
src/
├── index.html
├── projects/
│ └── index.html
├── style.css
├── setup-menu.js
├── visitor-counter.js
├── owner-tools.js
└── history-modal.js
Additional supporting assets are stored separately.
Current Source Baseline
The current canonical readable-source baseline is the later audited:
src(4).zip
from September 2026.
This superseded the earlier:
src(3).zip
baseline.
The readable source remains human-friendly.
Production hardening happens only during the build process.
Development VM
Website development takes place on:
VM101 - OpenLabFrame Dev
Current details:
VM ID: 101
Network: Private LAN
User: <admin-user>
State: On-demand
VM101 does not need to remain powered on when the site is not being edited.
VM101 SSH
Windows SSH alias:
ssh <development-alias>
VM101 is used for:
Git development
Source editing
Build testing
Production build generation
Previewing changes
Deployment preparation
Git Repository on VM101
Local repository path:
<SOURCE_REPOSITORY>
Branch:
main
Repository:
<private Open Lab Frame source repository>
Development Toolchain
Current VM101 toolchain includes:
nvm 0.40.7
Node.js 24.21.0
npm 11.19.0
Dependency install:
npm ci
Production build:
npm run build
Output:
dist/
Build Philosophy
The build system keeps the development files readable while making production files harder to casually copy or understand.
Readable private source
↓
npm run build
↓
Production transformation
↓
dist/
The build process does not require the developer to maintain unreadable source code.
Build System
The project uses a custom:
build.mjs
build pipeline.
The build performs tasks including:
JavaScript bundling
JavaScript obfuscation
CSS processing
Selector hardening
Content hashing
Minification
Production asset generation
Selector Hardening
Production builds include deterministic selector hardening.
A dedicated module:
selector-hardening.mjs
uses:
postcss-selector-parser
SHA-256 based mapping
to transform project selectors.
Class Transformation
Readable classes in the source are transformed into production names similar to:
x**********
The exact output is deterministic for the same selector mapping.
ID Transformation
Readable HTML IDs are transformed into production names similar to:
y**********
JavaScript Selector Rewriting
The build system also rewrites JavaScript references including:
getElementById()
getElementsByClassName()
querySelector()
querySelectorAll()
closest()
matches()
This prevents the hardened HTML/CSS from becoming disconnected from JavaScript behavior.
ARIA Relationships
Accessibility relationships are also rewritten when selector hardening changes related IDs.
This preserves relationships such as:
aria-controls
aria-labelledby
where applicable.
Production hardening should not break accessibility relationships.
Font Awesome Classes
Font Awesome classes such as:
fa-*
are intentionally preserved.
They are not renamed by the project selector-hardening system.
Production JavaScript Fragmentation
Production JavaScript is split into multiple independently processed files.
Generated filenames use meaningless content-hashed names similar to:
j-XXXXXXXX.min.js
The build preserves the required execution order in:
dist/index.html
Production CSS
CSS is also split/minified into hashed production files similar to:
c-XXXXXXXX.min.css
The hashes change when the generated content changes.
No Source Maps
Production builds intentionally do not publish source maps.
*.map
files should not be exposed publicly.
This reduces the amount of readable development structure revealed by the deployed site.
Why Asset Hashing Is Used
Content-hashed filenames allow very aggressive browser/CDN caching.
Example:
c-7350C8F2.min.css
If the content changes:
new content
→ new filename
Therefore an old cached asset cannot silently replace the new version referenced by the HTML.
Production Cache Strategy
The production site uses different caching rules depending on the type of content.
HTML
HTML should remain refreshable.
Typical behavior:
Cache-Control: no-cache
Cloudflare may report:
CF-Cache-Status: DYNAMIC
for HTML.
Hashed Assets
Hashed CSS and JavaScript can use long caching.
Typical policy:
max-age=31536000
immutable
This is appropriate because changed content receives a changed filename.
Images
Images use a shorter cache duration.
Current design has used approximately:
4 hours
rather than immutable one-year caching.
Deployment Workflow
The normal Open Lab Frame production workflow should remain deliberate.
Readable source
↓
Git repository
↓
VM101
↓
npm ci
↓
npm run build
↓
dist/
↓
Validate locally
↓
Create ZIP
↓
Transfer to Pi 5
↓
Verify ZIP
↓
Extract to staging
↓
Backup live site
↓
rsync dry run
↓
Deploy
↓
nginx -t
↓
Reload
↓
Public validation
Step 1 - Edit Readable Source
Changes should normally be made in:
src/
The production files inside:
dist/
should not be treated as the primary editable source.
Step 2 - Build
From the repository:
npm ci
npm run build
Expected output:
dist/
Step 3 - Validate Build
Before deployment, validate:
Main page
Projects page
Navigation
Modals
Setup menu
History modal
Galleries
Lightbox behavior
Responsive layout
Keyboard focus
Links
Hardened selectors
Generated assets
Step 4 - Create Deployment ZIP
The built:
dist/
directory is packaged for transfer.
A previous production ZIP on the Pi 5 was:
<DEPLOYMENT_ARCHIVE>
ZIP integrity should be checked before deployment.
Step 5 - Stage the Build
Extract into:
<STAGING_DIRECTORY>
Do not immediately overwrite the live site.
Step 6 - Back Up Current Production
Create a timestamped copy of:
<PRODUCTION_SITE_DIRECTORY>
before deployment.
Example pattern:
ehlabframe-backup-YYYYMMDD-HHMMSS
Step 7 - rsync Dry Run
Before modifying production, perform a dry run.
Conceptually:
rsync -av --delete --dry-run \
<STAGING_DIRECTORY>/ \
<PRODUCTION_SITE_DIRECTORY>/
Review:
New files
Changed files
Deleted files
before removing --dry-run.
Step 8 - Deploy
After the dry run is verified:
rsync -av --delete \
<STAGING_DIRECTORY>/ \
<PRODUCTION_SITE_DIRECTORY>/
Step 9 - Test Nginx
Before reload:
docker exec ehworld nginx -t
Expected:
syntax is ok
test is successful
Do not reload Nginx if the configuration test fails.
Step 10 - Reload Nginx
After validation:
docker exec ehworld nginx -s reload
Then validate the public site.
Post-Deployment Validation
After deployment, verify:
Homepage loads
Projects page loads
Correct version/changelog appears
New assets are referenced
Old assets were removed where appropriate
No obvious source selectors leaked
HTTPS works
Cloudflare Tunnel works
Images load
Navigation works
Modals work
Mobile layout works
Desktop layout works
Hard refresh shows current content
Latest Production Direction
The production site has progressed beyond the earlier simple placeholder project page.
The Projects page now contains real homelab project sections.
Current Projects Page
Current device/project sections include:
Raspberry Pi 5
Raspberry Pi 3 Model B
HP ProDesk 600 G4
Raspberry Pi 5 Section
The Pi 5 section documents infrastructure including:
Docker
Nginx website hosting
Cloudflare Tunnel
Grafana
Uptime Kuma
Monitoring
Traffic information
It includes expandable dashboard / monitoring galleries.
Status:
In use
Raspberry Pi 3 Section
The Pi 3 project documents:
Tailscale
Pi-hole
DNS
Private network access
It includes Pi-hole dashboard imagery.
Status:
In use
HP ProDesk 600 G4 Section
The HP ProDesk section documents the media-server project.
Current site status has shown:
Configuring
with Jellyfin as the main highlighted project/service.
Service Icons
The Projects page includes locally hosted labeled icons for services such as:
Jellyfin
Docker
Nginx
Grafana
Cloudflare
Tailscale
Pi-hole
These are part of the local production website assets.
Project Status Styling
Current project status examples include:
Green:
In use
Gold:
Configuring
This gives visitors a quick indication of project maturity.
Accessibility
Current website improvements include:
Descriptive alt text
Keyboard focus indicators
Responsive layouts
Keyboard-accessible interactions
Expandable galleries
Lightbox behavior
Production hardening should preserve these accessibility features.
Changelog
The website includes an internal history / changelog interface.
The latest production build has identified:
Version 0.2 - Latest
with the title:
Device Projects and Setup Updates
The changelog helps document visible project evolution over time.
History Modal
The main site includes a history/changelog modal.
Supporting JavaScript:
history-modal.js
The UI includes a history icon and tooltip.
Setup Menu
Setup/menu interaction is handled by:
setup-menu.js
The menu provides access to setup/project information without requiring a full-page redesign for every section.
Owner Tools
Owner-specific functionality uses:
owner-tools.js
A protected owner-check path has also existed under:
a protected owner-only route
Owner-only functionality should remain separated from normal public visitor behavior.
Visitor Counter
The website includes a visitor counter.
Frontend:
visitor-counter.js
The public API path has used:
a visitor-count API
Visitor Counting Logic
The visitor system uses a long-lived browser cookie:
a long-lived visitor cookie
with a long expiration period.
The intent is to avoid incrementing the counter repeatedly for the same returning browser.
Cloudflare Worker / D1
Visitor count state is handled through Cloudflare infrastructure.
A small Cloudflare D1-backed persistence layer stores the visitor count. Exact schema details are omitted from the public copy.
The frontend only needs the API result rather than direct database access.
YouTube Click Tracking
A separate API endpoint has also been implemented for YouTube interactions:
a click-tracking API
This was designed to track:
Total clicks
Unique clicks
without requiring a full external analytics platform.
Production Nginx Resource Usage
Nginx on the Pi 5 has demonstrated very low resource requirements.
Observed Nginx process memory was approximately:
~26.7 MiB RSS combined
A rapid direct HTTP test of approximately:
500 requests
only produced roughly:
2–3% CPU
while returning successful:
HTTP 200
responses.
The Pi 5 therefore has substantial headroom for the static website.
Monitoring
Uptime Kuma is used to monitor important website/infrastructure availability.
The Pi 5 itself is also visible in:
Beszel
for resource monitoring.
These provide different views:
Uptime Kuma
→ Is it reachable?
Beszel
→ How much resource is it using?
Cloudflare Tunnel
The Cloudflare Tunnel is one of the most important production dependencies.
If:
Nginx = healthy
but:
Cloudflare Tunnel = broken
the public website may still become unavailable.
This is why the ingress path should be considered as multiple layers.
Failure Layers
Public Open Lab Frame depends on:
Cloudflare
↓
Cloudflare Tunnel
↓
Pi 5
↓
Docker
↓
Nginx
↓
Production files
Each layer can fail independently.
Direct Local Testing
If the public site fails, troubleshooting should determine whether Nginx itself is still serving locally.
Conceptually:
Public site broken
↓
Test Pi 5 locally
↓
If local works:
Check Cloudflare / Tunnel
If local fails:
Check Nginx / files / Pi 5
This prevents blaming the application when the problem is actually ingress-related.
VM101 Is Not a Production Dependency
VM101 - OpenLabFrame Dev can be turned off after development.
Production does not depend on VM101.
VM101 OFF
↓
Production website still works
because production runs from the Pi 5.
VM101 is only needed when:
Editing
Building
Testing
Preparing deployment
<private development hostname>
VM101 development preview is available internally through:
<private development URL>
Path:
Client
↓
Pi-hole
↓
VM102 / NPM
↓
<private development host>:<SERVICE_PORT>
↓
VM101 development preview
If VM101 is powered off:
<private development hostname>
being unavailable is expected.
Production vs Development
Development
VM101
↓
Readable source
↓
Local preview
Production
VM101
↓
npm run build
↓
dist/
↓
Pi 5
↓
Nginx
↓
Cloudflare
↓
Public site
This separation protects production stability.
Git Workflow
Normal workflow:
Edit
↓
git status
↓
Test
↓
Commit
↓
Push
↓
Build
↓
Deploy
The GitHub repository remains the canonical source history.
Git Mirror
A Pi 5 Git mirror job has also existed for additional local repository continuity.
Timer:
open-lab-frame-mirror.timer
Schedule:
Daily around 03:00
The timer was configured with:
Persistent
so a missed scheduled run can execute after the host returns.
Search Engine Indexing
The Projects page was updated after previously showing older placeholder content such as:
Under Development
The current public page no longer contains that old placeholder.
Google Search Console workflow included:
URL Inspection
↓
Test Live URL
↓
Request Indexing
Search results may take time to reflect the latest site content even after the public page itself is already updated.
Search Indexing Principle
The public website is the source of truth for current content.
A stale search result does not necessarily mean the deployment is stale.
Always verify:
Actual public HTML
before troubleshooting Google indexing.
Security
Important rules:
Keep source repository private where intended.
Do not place secrets in frontend JavaScript.
Do not place private API keys in production static files.
Do not store Cloudflare tokens in Obsidian.
Do not publish SSH private keys.
Keep Cloudflare Access protection where owner-only tools require it.
Continue using hashed/minified production builds.
Keep readable source separate from production.
Validate every production build before deployment.
Things That Should Never Be in Public dist/
Do not include:
Private SSH keys
Cloudflare API tokens
Passwords
Private environment files
Secret webhooks
Private recovery codes
Unnecessary internal documentation
Source maps
Everything inside:
dist/
should be assumed publicly downloadable.
Deployment Safety Rules
Always preserve this order:
Build
→ Test
→ ZIP
→ Transfer
→ Integrity Check
→ Stage
→ Backup
→ rsync Dry Run
→ Deploy
→ nginx -t
→ Reload
→ Public Validation
Do not skip directly from:
New build
to:
Overwrite production
without validation and a rollback path.
Recovery
If a new deployment breaks the site, the previous timestamped production backup can be restored.
Conceptually:
Broken deployment
↓
Stop making additional changes
↓
Identify latest known-good backup
↓
Restore backup to active directory
↓
nginx -t
↓
Reload
↓
Validate public site
A dedicated procedure should eventually document the exact rollback commands.
Suggested procedure note:
Open Lab Frame Deployment
Current Architecture Summary
PUBLIC INTERNET
│
↓
Cloudflare
│
↓
Cloudflare Tunnel
│
↓
Raspberry Pi 5
<private Pi 5 host>
│
↓
Nginx / ehworld
│
↓
Production dist/
Development remains separate:
VM101
↓
GitHub source
↓
npm run build
↓
dist/
↓
Deployment
↓
Pi 5
Future Direction
Open Lab Frame should continue growing alongside the homelab.
Future additions can document:
NAS Compute Build
Managed networking
Additional Raspberry Pi nodes
New self-hosted applications
Storage architecture
Infrastructure improvements
Recovery procedures
Homelab milestones
The site should remain a public representation of actual completed or actively developing work rather than becoming disconnected from the real homelab.
Related Notes
Homelab Overview
Raspberry Pi 5
VM101 - OpenLabFrame Dev
Network Topology
Nginx Proxy Manager
Uptime Kuma
Beszel
Open Lab Frame Project
Open Lab Frame Deployment
Back to Services
Services / Note 02
Jellyfin
In this note
Primary self-hosted media server running on VM100 - Jellyfin .
Homelab Overview | VM100 - Jellyfin | VPS Caddy | Tailscale | Pi-hole | Beszel | Uptime Kuma
Status
State: In use
Host: VM100 - Jellyfin
Network: Private LAN
Service Port: 8096
Tailscale Backend: <private Jellyfin Tailscale endpoint>:<SERVICE_PORT>
Role: Primary personal / remote media server
Operation: Always on
Purpose
Jellyfin provides self-hosted media playback for:
Local devices
Personal remote devices
Friends / family
iPhone
Web clients
Future TV / streaming clients
Media can be delivered using:
Direct Play
Direct Stream
Transcoding
depending on client compatibility.
Server Location
Jellyfin runs inside:
VM100 - Jellyfin
which runs on:
Proxmox Host
which runs on:
HP ProDesk 600 G4
Architecture:
HP ProDesk 600 G4
↓
Proxmox
↓
VM100
↓
Docker
↓
Jellyfin
Direct LAN Address
Direct Jellyfin endpoint:
<private service endpoint>
This is the shortest path from devices on the home LAN.
Public Jellyfin Address
Public friendly URL:
https://jellyfin.openlabframe.com
Remote public users connect through:
VPS Caddy
rather than connecting directly to the home Starlink connection.
Main Network Paths
Jellyfin currently has three important access paths.
Local LAN
Local Device
↓
Home Wi-Fi / Ethernet
↓
<private Jellyfin host>
↓
Jellyfin
Private Tailscale
Personal Remote Device
↓
Tailscale
↓
<private Jellyfin Tailscale endpoint>
↓
Jellyfin
Public VPS
Remote Friend
↓
Internet
↓
jellyfin.openlabframe.com
↓
VPS / Caddy
↓
Tailscale
↓
<private Jellyfin Tailscale endpoint>:<SERVICE_PORT>
↓
Jellyfin
Local DNS
Pi-hole can direct selected personal devices to Jellyfin locally.
At home:
iPhone
↓
Pi-hole DNS
↓
<private Jellyfin host>
↓
Jellyfin
This avoids unnecessary traffic through:
Internet
→ VPS
→ Tailscale
→ Back Home
when the client is already on the home LAN.
Verified Local iPhone Connection
A Jellyfin session from the iPhone was observed with:
<private local client>
This confirmed that the phone was reaching Jellyfin over the local LAN.
The phone was connected through Wi-Fi rather than Ethernet.
That is still a normal local LAN connection.
Verified Remote Tailscale Connection
The iPhone was also observed connecting through a Tailscale address:
<private remote Tailscale client>
This occurred when the phone was using the private Tailscale path rather than the local LAN path.
Network Route vs Playback Mode
These are different concepts.
Direct network connection
≠
Direct Play
A device can reach Jellyfin directly over:
LAN
or:
Tailscale
and Jellyfin can still decide to transcode the media.
The route determines:
How the client reaches Jellyfin
Playback mode determines:
How Jellyfin delivers the media
Jellyfin Playback Modes
Direct Play
Direct Play means the original media can be sent to the client without conversion.
Conceptually:
Media file
↓
Jellyfin
↓
Client
with essentially no media conversion.
This is normally the most efficient playback mode.
Direct Stream
Direct Stream means Jellyfin may repackage the media into another container while keeping major codecs unchanged.
Conceptually:
Original streams
↓
Container change
↓
Client
This generally requires much less processing than full transcoding.
Transcoding
Transcoding occurs when Jellyfin needs to convert part of the media into something compatible with the client.
Possible triggers include:
Unsupported video codec
Unsupported audio codec
Unsupported container
Unsupported subtitle format
Bitrate limitation
Resolution limitation
Client capability limitation
iPhone Client
Current personal mobile client:
iPhone 16 Pro Max
Jellyfin iOS was observed using:
Jellyfin iOS 1.8.0
Client option observed:
Use Native Video Player
was enabled.
iPhone Playback Test
A tested media file was transcoded.
Observed Jellyfin playback information:
Transcoding
Framerate:
~158 fps
Output:
28.1 Mbps
MP4
H264
FLAC
Jellyfin reported the reasons:
The container is not supported
The subtitle codec is not supported
This confirmed that the network route itself was not the problem.
The iPhone had a direct local connection, but media compatibility still caused Jellyfin to transcode.
Subtitle Impact
Subtitles can independently cause transcoding.
For example:
Video codec supported
Audio supported
Container mostly usable
BUT
Subtitle codec unsupported
can still produce:
Transcoding
depending on the client.
This is especially relevant with subtitle formats that the iOS client cannot render directly.
Container Compatibility
A media file can contain supported codecs but still require conversion because the container is unsupported by the client.
Example:
Supported video
+
Supported audio
+
Unsupported container
↓
Remux / transcode
This distinction is useful when diagnosing why Jellyfin is not Direct Playing.
How to Check Playback Mode
While media is playing:
Jellyfin Dashboard
↓
Active Device / Session
↓
Playback details
Jellyfin will report whether the session is:
Direct Play
Direct Stream
Transcoding
If transcoding, it also reports the reason.
This is more reliable than trying to infer playback mode from CPU usage alone.
Hardware Acceleration
The HP ProDesk 600 G4 contains:
Intel Core i7-8700
Intel UHD 630
The Intel integrated GPU is used by Jellyfin for hardware-accelerated media processing.
Technology:
Intel Quick Sync
Purpose of Quick Sync
Hardware acceleration helps reduce CPU load during:
Video decoding
Video encoding
Transcoding
Multiple simultaneous streams
Instead of relying entirely on:
i7-8700 CPU
Jellyfin can use:
Intel UHD 630
for supported workloads.
Why Transcoding Is Acceptable
Direct Play is preferred when possible, but transcoding is not automatically a problem.
The current server has hardware acceleration specifically so incompatible clients or files can still play.
A successful hardware-transcoded stream is normal behavior.
The main goal is:
Playback works reliably
+
Server resource usage remains reasonable
rather than forcing every single file into Direct Play.
Public Friend Access
Friends / family use:
https://jellyfin.openlabframe.com
They do not need:
Tailscale
Pi-hole
Nginx Proxy Manager
Direct LAN access
Public architecture:
Friend
↓
Internet
↓
VPS
↓
Caddy
↓
Tailscale
↓
VM100
↓
Jellyfin
Why NPM Is Not Used
Nginx Proxy Manager is not part of the public Jellyfin path.
Correct public route:
VPS
↓
Caddy
↓
Tailscale
↓
Jellyfin
Not:
VPS
↓
NPM
↓
Jellyfin
This avoids an unnecessary reverse-proxy hop.
Starlink / CGNAT
The home Internet connection uses:
Starlink
Starlink operates behind CGNAT.
Because of that, the public Jellyfin architecture uses a VPS instead of relying on normal inbound IPv4 port forwarding.
Public Internet
↓
VPS
↓
Tailscale
↓
Home Jellyfin
Tailscale Backend
VM100 Tailscale address:
<private Jellyfin Tailscale endpoint>
Jellyfin service:
<private Jellyfin Tailscale endpoint>:<SERVICE_PORT>
This is the private backend used by the VPS Caddy reverse proxy.
VPS Dependency
Public Jellyfin depends on several layers:
Public DNS
↓
VPS
↓
Caddy
↓
Tailscale
↓
VM100
↓
Jellyfin
Failure at any one of these layers can break public playback.
Failure Examples
VPS Down
Public Jellyfin:
DOWN
Local Jellyfin:
Can still work
Private Tailscale access:
Can still work
Tailscale on VPS Down
Caddy cannot reach VM100
↓
Public Jellyfin fails
while local Jellyfin may remain healthy.
VM100 Down
All Jellyfin paths fail:
Local
Private remote
Public VPS
because the actual Jellyfin server is unavailable.
Home Starlink Down
The VPS may still be online, but:
VPS
X
Home backend
so public Jellyfin will fail until the home connection returns.
Monitoring
Uptime Kuma monitors Jellyfin availability.
Current monitor:
Jellyfin Service - VM100
Purpose:
Is Jellyfin actually responding?
This is separate from simply checking whether VM100 is alive.
Host vs Service Monitoring
Example:
VM100:
UP
Jellyfin Service:
DOWN
means:
The VM is alive
but Jellyfin itself has a problem
This makes troubleshooting much easier.
Beszel
Beszel monitors VM100 resource usage.
This provides quick visibility into:
VM CPU
VM RAM
Disk usage
Network usage
Docker containers
Container memory
Container CPU
Jellyfin can therefore be examined from two perspectives:
Uptime Kuma
→ Is Jellyfin working?
Beszel
→ How much resource is it using?
Media Automation
Jellyfin runs alongside a private media-automation environment documented publicly only as:
ARR Stack
High-level flow:
Request / Automation
↓
ARR Stack
↓
Completed Media
↓
Jellyfin
Individual applications and internal download/integration details are intentionally omitted from the public version.
Media Storage
The current VM provides the active Jellyfin environment.
Long term, bulk media is planned to move to:
NAS Compute Build
The planned NAS / compute machine will provide much larger HDD-based storage for:
Movies
TV
Anime
Other media
Recordings
Archives
while Jellyfin/application workloads can use faster storage where useful.
Future NAS Relationship
Planned future model:
Jellyfin
↓
Fast application / metadata storage
+
NAS HDD media library
The future NAS currently plans:
4 × 8 TB IronWolf Pro
configured as two mirrored pairs.
Approximate mirrored capacity:
~16 TB before filesystem overhead
Client Strategy
The general playback preference is:
Direct Play when practical
but:
Hardware transcoding when needed
The client should not be forced into an unsupported playback mode merely to avoid transcoding.
Remote Playback Considerations
Remote playback performance depends on:
Home upload bandwidth
VPS connectivity
Remote user's download bandwidth
Media bitrate
Whether transcoding occurs
Tailscale path quality
Jellyfin client compatibility
A large 4K remux can require substantial upload bandwidth when Direct Played remotely.
Security
Important Jellyfin rules:
Do not publicly expose VM100 directly unless deliberately required.
Keep public ingress through the VPS/Caddy design.
Keep private access available through Tailscale.
Keep admin credentials private.
Do not store Jellyfin passwords in Obsidian.
Keep Docker and Jellyfin updated.
Limit administrative access to authorized users.
Recovery Testing
A controlled reboot of Proxmox Host was performed.
VM100 recovered automatically.
After reboot:
VM100 returned
Docker returned
Jellyfin returned
Media automation services returned
Gluetun returned
Uptime Kuma detected recovery
Beszel resumed reporting
This demonstrated successful automatic recovery after a normal Proxmox host reboot.
Current Access Summary
At Home
Client
↓
LAN
↓
<private Jellyfin host>:<SERVICE_PORT>
↓
Jellyfin
Private Remote
Client
↓
Tailscale
↓
<private Jellyfin Tailscale endpoint>:<SERVICE_PORT>
↓
Jellyfin
Public Remote
Client
↓
jellyfin.openlabframe.com
↓
VPS Caddy
↓
Tailscale
↓
<private Jellyfin Tailscale endpoint>:<SERVICE_PORT>
↓
Jellyfin
Related Notes
Homelab Overview
VM100 - Jellyfin
HP ProDesk 600 G4
Proxmox Host
VPS Caddy
Tailscale
Pi-hole
Uptime Kuma
Beszel
NAS Compute Build
Back to Services
Services / Note 03
Uptime Kuma
In this note
Primary availability-monitoring and alerting platform for important homelab hosts and services.
Homelab Overview | Raspberry Pi 5 | Beszel | Proxmox Host | VM100 - Jellyfin | VM102 - Management
Status
State: In use
Host: Raspberry Pi 5
Network: Private LAN
Service Port: 3001
Role: Availability monitoring + outage alerts
Operation: Always on
Direct backend:
<private service endpoint>
Friendly URL:
<private uptime URL>
Purpose
Uptime Kuma answers the most important operational question:
Is the service actually up?
It is used for:
Host reachability
HTTP/HTTPS service checks
Push monitoring
Service outage detection
Recovery detection
Discord alerts
ntfy notifications
iPhone notifications
Monitoring Philosophy
Uptime Kuma and Beszel intentionally have different roles.
Uptime Kuma
Is it alive?
Is it responding?
Did it go down?
Did it recover?
Notify me.
Beszel
How much CPU?
How much RAM?
How much disk?
How hot?
How much network?
What are the containers doing?
Together:
Uptime Kuma
→ Availability
Beszel
→ Resource usage
Host
Uptime Kuma runs on:
Raspberry Pi 5
The Pi 5 also hosts services including:
Open Lab Frame
Beszel
Grafana
Prometheus
Loki
Alloy
Cloudflare Tunnel
Core Alert Path
The current alert architecture is:
Service problem
↓
Uptime Kuma
↓
Discord
+
ntfy
↓
iPhone
Both outage and recovery notifications have been tested successfully.
Notification Channels
Current alert channels include:
Discord
ntfy
iPhone push notification
This means a critical service problem does not require manually checking the dashboard to discover it.
Current Monitoring Areas
Uptime Kuma monitors important parts of the homelab such as:
Not every application needs the same monitoring method.
Proxmox Monitor
Monitor:
HP ProDesk - Proxmox
Target:
<private Proxmox host>
Purpose:
Is the physical Proxmox host reachable?
Proxmox Detection Settings
The original configuration used a relatively forgiving outage window.
Original approximate settings:
Heartbeat Interval: 60 seconds
Retries: 2
Heartbeat Retry Interval: 60 seconds
Global Timeout: 10 seconds
During a controlled Proxmox reboot, the host recovered before Kuma exhausted the retry window.
Therefore the Proxmox host never officially changed to:
DOWN
even though the VMs temporarily disappeared.
Current Proxmox Monitor Settings
The monitor was made more aggressive:
Heartbeat Interval: 30 seconds
Retries: 1
Heartbeat Retry Interval: 15 seconds
Global Timeout: 10 seconds
The purpose is to detect a real physical-host outage much faster.
Why the Proxmox Monitor Is Aggressive
A temporary application restart may be tolerable.
A physical Proxmox host disappearing is more important because:
Proxmox Down
↓
VM100 Down
VM101 unavailable
VM102 Down
↓
Multiple applications unavailable
Therefore the physical-host monitor uses a shorter detection window.
VM102 Monitoring
VM102 - Management has separate host and service monitors.
Current examples:
VM102 - Management
NPM - VM102
Portainer - VM102
VM102 Host Monitor
Monitor type:
Ping
Target:
<private management host>
Typical configuration:
Heartbeat Interval: 60 seconds
Retries: 2
Retry Interval: 20 seconds
Purpose:
Is VM102 itself alive?
NPM Monitor
Monitor:
NPM - VM102
Direct target:
<private service endpoint>
Monitor type:
HTTP(s)
Expected healthy response:
HTTP 200
Purpose:
Is Nginx Proxy Manager itself responding?
Portainer Monitor
Monitor:
Portainer - VM102
Direct target:
<private service endpoint>
Expected healthy response:
HTTP 200
TLS verification can be ignored for this direct backend monitor because Portainer uses its own local certificate on the raw 9443 interface.
Why VM102 Uses Separate Monitors
Example:
VM102 - Management UP
NPM - VM102 DOWN
Portainer - VM102 UP
Interpretation:
VM102 itself is healthy
NPM specifically failed
Whereas:
VM102 - Management DOWN
NPM - VM102 DOWN
Portainer - VM102 DOWN
suggests:
VM102 itself or its network path is unavailable
This makes troubleshooting much easier.
VM100 Monitoring
VM100 - Jellyfin is monitored at both the host/service level.
Examples include:
VM100
Jellyfin
Gluetun
ARR Stack
This allows Kuma to distinguish:
VM is alive
from:
Application is healthy
Jellyfin Monitor
Jellyfin monitoring answers:
Is Jellyfin actually responding?
rather than only:
Is VM100 running?
Example:
VM100 UP
Jellyfin DOWN
means the problem is likely inside the VM/application layer rather than Proxmox itself.
Gluetun Push Monitor
Gluetun uses a special Push monitor rather than only an HTTP check.
Monitor name:
Gluetun - VM100
The health signal is generated by VM100.
Gluetun Health Architecture
Docker
↓
Gluetun health state
↓
VM100 checker script
↓
Uptime Kuma Push endpoint
↓
Kuma monitor
When Gluetun is healthy:
Heartbeat arrives
↓
Monitor stays UP
When Gluetun becomes unhealthy:
Heartbeat stops
↓
Kuma misses heartbeat
↓
Monitor becomes DOWN
↓
Notification sent
Gluetun Health Script
VM100 contains:
/usr/local/sbin/check-gluetun-health
The script checks Docker's health state for Gluetun.
Known-working manual execution:
sudo /usr/local/sbin/check-gluetun-health
echo "Exit code: $?"
Healthy result:
Exit code: 0
Push URL Storage
The Gluetun Push URL is stored on VM100 at:
<PROTECTED_PUSH_URL_FILE>
The file contains the secret Push token.
The token itself should not be stored in Obsidian.
Correct Push Endpoint
The working Push URL uses the Pi 5 directly over the LAN.
Structure:
<private service endpoint>/api/push/<TOKEN>?...
The actual token should remain private.
Why the Public Kuma Hostname Was Not Used
At one point the Push URL used:
<private uptime URL>/...
That path went through Cloudflare Access and returned:
HTTP 302
The hostname was changed while keeping the current Push token.
Correct backend:
<private service endpoint>
After the change:
HTTP 200
was returned.
Token Reset History
The Gluetun Push token was reset several times during setup.
Important rule:
Every token reset
→ previous Push URL becomes obsolete
Only the newest token remains valid.
Once the current URL returned:
HTTP 200
the token was considered correct and should not be reset again unless intentionally revoking it.
Safe Push Test
The Push URL can be tested without printing the secret:
sudo bash -c '
URL="$(cat <PROTECTED_PUSH_URL_FILE>)"
curl -sS -o /dev/null -w "HTTP %{http_code}\n" "$URL"
'
Expected:
HTTP 200
Do not use:
cat <PROTECTED_PUSH_URL_FILE>
in screenshots or documentation because it exposes the Push token.
Gluetun Systemd Automation
The health checker is automated with systemd.
Service:
gluetun-kuma-check.service
Timer:
gluetun-kuma-check.timer
The timer runs approximately every:
60 seconds
Service Definition
Conceptually:
[Unit]
Description=Report Gluetun health to Uptime Kuma
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/check-gluetun-health
Timer Definition
Conceptually:
[Unit]
Description=Check Gluetun health every minute
[Timer]
OnBootSec=30s
OnUnitActiveSec=60s
AccuracySec=5s
Unit=gluetun-kuma-check.service
[Install]
WantedBy=timers.target
Expected Timer State
Check with:
systemctl status gluetun-kuma-check.timer --no-pager
Expected:
Active: active (waiting)
Next scheduled run:
systemctl list-timers gluetun-kuma-check.timer --no-pager
End-to-End Gluetun Alert Test
The complete alert chain was deliberately tested.
The test was designed to avoid actually stopping Gluetun.
Instead, only the heartbeat timer was stopped.
Command:
sudo systemctl stop gluetun-kuma-check.timer
Gluetun itself continued running.
Expected Test Behavior
Heartbeat stops
↓
Uptime Kuma waits for expected heartbeat
↓
No heartbeat received
↓
Gluetun - VM100 becomes DOWN
↓
ntfy notification
↓
iPhone notification
This worked successfully.
Recovery Test
The timer was restarted:
sudo systemctl start gluetun-kuma-check.timer
Expected:
Active: active (waiting)
Within approximately one heartbeat interval:
New heartbeat
↓
Kuma changes monitor to UP
↓
Recovery notification
↓
iPhone
The recovery notification was also successfully received.
Verified Gluetun Alert Chain
Gluetun healthy
↓
Heartbeat
↓
Kuma UP
Heartbeat stops
↓
Kuma DOWN
↓
iPhone notification
Heartbeat returns
↓
Kuma UP
↓
Recovery notification
The alert chain has therefore been verified end-to-end.
Proxmox Reboot Test
The physical HP ProDesk 600 G4 running Proxmox Host was deliberately rebooted.
Command:
sudo reboot
The purpose was to test automatic recovery after an infrastructure interruption.
What Went Down
During the reboot, Kuma observed failures such as:
NPM - VM102
→ EHOSTUNREACH
Portainer - VM102
→ ECONNREFUSED
Gluetun - VM100
→ No heartbeat
These were expected while the underlying host and VMs restarted.
What Recovered Automatically
After the Proxmox host returned:
VM100 returned
VM102 returned
Docker returned
Jellyfin returned
Gluetun returned
ARR Stack services returned
NPM returned
Portainer returned
Beszel Agents returned
Kuma monitors changed back to Up
No manual service startup was required.
Why Proxmox Initially Did Not Go Down
The Proxmox host rebooted quickly enough that its original monitoring retry window did not expire.
Meanwhile, applications took longer to become ready.
Typical boot sequence:
Physical host
↓
Networking
↓
Proxmox reachable
↓
VMs boot
↓
Guest Linux
↓
Docker
↓
Applications
Therefore service monitors remained down longer than the physical host itself.
Notification Behavior
Kuma can generate two important notification types:
DOWN
and:
UP / Recovery
Both have been successfully tested through the iPhone alert chain.
ntfy
ntfy provides the mobile notification path.
Conceptually:
Uptime Kuma
↓
ntfy
↓
iPhone
This allows homelab alerts to appear as normal phone notifications.
Discord
Discord is also configured as a notification destination.
This provides:
Persistent channel history
+
Another alert destination
Useful events include:
Service DOWN
Service recovery
Host outages
Push-monitor failures
Monitor Retries
Not every failed request should immediately generate a notification.
Retries help prevent alerts from:
Single lost packets
Brief container restarts
Short network hiccups
Temporary service initialization
The appropriate retry window depends on the importance of the monitor.
Physical Host vs Application Monitoring
Physical infrastructure generally deserves faster detection.
Example:
Proxmox
30-second checks
1 retry
15-second retry interval
Application services may use more forgiving settings.
This avoids unnecessary phone alerts while still detecting serious host outages quickly.
VM101 Monitoring Rule
VM101 - OpenLabFrame Dev is intentionally powered off when not editing the website.
Therefore:
VM101 DOWN
or:
<private development hostname> unavailable
is normally expected.
VM101 should not be treated as a 24/7 critical monitor.
A monitor for it should either:
Stay paused when VM101 is off
Be enabled only during development
Avoid normal critical notifications
Beszel vs Kuma Example
A container may show in Beszel:
Status: Up
Health: None
while Kuma shows:
UP
This is normal.
Health: None means:
Docker HEALTHCHECK not defined
not:
Application broken
Kuma independently tests actual service availability.
Failure Diagnosis Examples
VM102 Up, NPM Down
VM102 - Management UP
NPM - VM102 DOWN
Portainer - VM102 UP
Likely:
NPM-specific problem
VM102, NPM, Portainer All Down
VM102 DOWN
NPM DOWN
Portainer DOWN
Likely:
VM102 or underlying Proxmox/network problem
VM100 Up, Jellyfin Down
VM100 UP
Jellyfin DOWN
Likely:
Jellyfin/container/application problem
rather than a full VM outage.
Dependency Awareness
Some services depend on several layers.
Example:
↓
Pi-hole
↓
VM102
↓
NPM
↓
Pi 5
↓
Monitoring individual layers makes diagnosis easier.
Uptime Percentage During Setup
A newly created monitor may initially show a low uptime percentage because of:
Setup time
Pending state
Token resets
Manual testing
Deliberate outage tests
Example:
Gluetun - VM100
~50% uptime
during initial configuration did not mean Gluetun was actually failing half the time.
The percentage naturally improves as successful checks accumulate.
Security
Uptime Kuma contains sensitive infrastructure information.
Important rules:
Keep administration protected.
Do not expose Push tokens.
Do not store notification secrets in Obsidian.
Do not store Discord webhook URLs here.
Do not store ntfy credentials here.
Do not publish private monitor targets unnecessarily.
Use trusted/private access for administration.
Secrets That Should Not Be Stored Here
Do not record:
Push tokens
Discord webhook URLs
ntfy credentials/tokens
API keys
Passwords
Private SSH keys
Document:
Where the credential is stored
rather than the credential itself.
Recovery Philosophy
Monitoring is only useful if recovery behavior is understood.
The homelab has already tested:
Service outage detection
Recovery detection
Phone notification
Host reboot recovery
VM recovery
Container recovery
This is more valuable than simply having a dashboard that always appears green.
Monitoring Architecture Summary
HOMELAB
│
┌──────────────┼──────────────┐
↓ ↓ ↓
Hosts Services Push Checks
│ │ │
└──────────────┼──────────────┘
↓
Uptime Kuma
↓
┌───────┴───────┐
↓ ↓
Discord ntfy
↓
iPhone
Related Notes
Homelab Overview
Raspberry Pi 5
HP ProDesk 600 G4
Proxmox Host
VM100 - Jellyfin
VM102 - Management
Jellyfin
Beszel
Nginx Proxy Manager
Back to Services
Services / Note 04
Beszel
In this note
Lightweight resource and system-status dashboard for the main homelab systems.
Homelab Overview | Raspberry Pi 5 | HP ProDesk 600 G4 | VM100 - Jellyfin | VM102 - Management | Uptime Kuma
Status
State: In use
Hub Host: Raspberry Pi 5
Network: Private LAN
Hub Port: 8090
Friendly URL: <private Beszel URL>
Operation: Always on
Beszel is used as the simple resource-monitoring layer for the homelab.
The goal is not to build a large monitoring wall.
The main questions Beszel should answer are:
Is the system online?
How much CPU is it using?
How much RAM is it using?
How much disk is being used?
How hot is it?
How much network traffic is it using?
What are its Docker containers doing?
Why Beszel
Beszel was chosen because the preferred monitoring style is simple.
Rather than relying on a large Grafana dashboard for everyday checks:
Beszel
→ Quick resource overview
Uptime Kuma
→ Up / Down + alerts
Grafana / Prometheus can remain available for deeper troubleshooting when needed.
Hub
The Beszel Hub runs on:
Raspberry Pi 5
Direct address:
<private service endpoint>
Friendly URL:
<private Beszel URL>
Docker Location
Beszel Docker project:
<BESZEL_DIRECTORY>
Compose file:
<BESZEL_DIRECTORY>/compose.yaml
Persistent Hub data:
<BESZEL_DIRECTORY>/beszel_data
Hub Container
Container:
beszel
Image:
henrygd/beszel:latest
The Hub has been verified as:
Up
healthy
Direct HTTP testing returned:
HTTP 200
Friendly URL
Nginx Proxy Manager provides:
<private Beszel URL>
Backend:
<private service endpoint>
Pi-hole DNS
Pi-hole resolves:
<private Beszel hostname>
↓
<private management host>
<private management host> is:
VM102 - Management
running:
Nginx Proxy Manager
Full path:
Client
↓
Pi-hole
↓
<private Beszel hostname>
↓
<private management host>
↓
NPM
↓
<private Pi 5 host>:<SERVICE_PORT>
↓
Beszel Hub
Current Monitored Systems
The Beszel dashboard currently includes:
HP ProDesk - Proxmox
Pi 5
VM100 - Jellyfin
VM102 - Management
The Raspberry Pi 3 is intentionally not included .
The goal is to avoid adding systems simply because they exist.
Dashboard Philosophy
Current desired dashboard:
HP ProDesk - Proxmox 🟢
Pi 5 🟢
VM100 - Jellyfin 🟢
VM102 - Management 🟢
with quick visibility into:
CPU
RAM
Disk
Network
Temperature
Uptime
Containers
This provides enough visibility without creating unnecessary monitoring clutter.
Raspberry Pi 5 Monitoring
The Pi 5 hosts both:
Beszel Hub
+
Beszel Agent
Because the Hub and Agent are on the same machine, they use a local shared socket arrangement.
The agent can report:
CPU usage
Memory usage
Disk usage
Network usage
Temperature
Load average
Host uptime
Docker containers
Pi 5 Agent
Container:
beszel-agent
Image:
henrygd/beszel-agent:latest
The Hub and Agent share a socket directory.
Conceptually:
Beszel Hub
↕
Unix Socket
↕
Beszel Agent
This avoids needing to expose the agent port on the Pi 5 for the same-host connection.
Docker Visibility
The Pi 5 agent has read-only access to:
/var/run/docker.sock
This allows Beszel to display Docker container usage.
Pi 5 Observed Metrics
Typical observed values have included approximately:
CPU: ~1–4%
Memory: ~10%
Disk: ~2%
Temperature: ~57–59 °C
These values indicate substantial remaining headroom.
HP ProDesk / Proxmox Monitoring
The physical HP ProDesk 600 G4 runs the native Beszel Linux agent directly on the Proxmox host.
System name:
HP ProDesk - Proxmox
Host address:
<private Proxmox host>
Agent port:
<Beszel agent port>
Proxmox Agent Type
The Proxmox host uses:
Native Linux binary agent
rather than Docker.
This avoids installing Docker on the hypervisor solely for monitoring.
Proxmox Agent Service
Systemd service:
beszel-agent.service
Expected state:
active (running)
The native agent starts automatically with the Proxmox host.
Proxmox Agent Listener
The agent listens on:
<Beszel agent port>
This can be checked with:
ss -ltnp | grep <Beszel agent port>
Proxmox Observed Metrics
Typical observed values have included approximately:
CPU: < 1% while idle
Memory: ~35–36%
Disk: ~5.5%
Temperature: ~35–37 °C
Beszel has also shown:
72 services
0 failed
for the Proxmox host.
This provides a useful quick systemd health overview.
VM100 - Jellyfin Monitoring
VM100 - Jellyfin runs a Docker-based Beszel agent.
System:
VM100 - Jellyfin
LAN IP:
<private Jellyfin host>
Agent port:
<Beszel agent port>
VM100 Purpose in Beszel
Beszel provides:
VM CPU usage
VM memory usage
Disk usage
Network activity
Uptime
Docker container CPU
Docker container memory
Container status
Docker health status where configured
VM100 Docker Visibility
Beszel can display containers including:
Jellyfin
Gluetun
ARR Stack
Beszel Agent
Portainer Agent
Typical VM100 Usage
Observed approximate values have included:
CPU: < 1% while idle
Memory: ~37%
Disk: ~29%
These values vary depending on:
Jellyfin playback
Transcoding
Downloads
Media imports
ARR Stack activity
ARR Stack extraction
Torrent activity
VM102 - Management Monitoring
VM102 - Management also runs a Docker-based Beszel agent.
System:
VM102 - Management
LAN IP:
<private management host>
Agent port:
<Beszel agent port>
VM102 Docker Services
Beszel can see management containers such as:
Nginx Proxy Manager
Portainer
Beszel Agent
Typical VM102 Usage
Observed values have included approximately:
CPU: < 1%
Memory: ~15–16%
Disk: ~11%
This confirms that VM102 remains a lightweight management VM.
Docker Health Column
Beszel displays Docker health information where Docker containers define a:
HEALTHCHECK
Some VM100 containers show:
Healthy
while others show:
None
Health None Does Not Mean Broken
Example:
ARR Stack
Status: Up
Health: None
means:
Container is running
BUT
No Docker HEALTHCHECK is defined
It does not mean:
ARR Stack is unhealthy
Containers With Health Checks
Observed examples include:
Gluetun → Healthy
ARR Stack → Healthy
ARR Stack → Healthy
ARR Stack → Healthy
Other containers may show:
Health: None
while operating normally.
Beszel vs Uptime Kuma
These systems intentionally do different jobs.
Beszel
Answers:
How much resource is the system using?
Examples:
CPU
RAM
Disk
Temperature
Network
Containers
Load average
Uptime
Uptime Kuma
Answers:
Is the service actually reachable?
Examples:
Jellyfin responding?
NPM responding?
Portainer responding?
Gluetun heartbeat arriving?
Proxmox reachable?
Example
Beszel might show:
ARR Stack
Container: Up
Health: None
while Uptime Kuma shows:
ARR Stack
UP
There is no conflict.
Beszel is reporting Docker metadata.
Uptime Kuma is checking the actual service.
Monitoring Architecture
HOMELAB MONITORING
┌─────────────────────────┐
│ Uptime Kuma │
│ │
│ Up / Down │
│ Service availability │
│ Discord alerts │
│ ntfy / iPhone alerts │
└─────────────────────────┘
+
┌─────────────────────────┐
│ Beszel │
│ │
│ CPU │
│ RAM │
│ Disk │
│ Network │
│ Temperature │
│ Containers │
└─────────────────────────┘
Why Not Expand Grafana Instead
Raspberry Pi 5 already hosts deeper monitoring tools such as:
Grafana
Prometheus
Loki
Alloy
Node Exporter
However, the preferred everyday experience is simpler.
Instead of opening a dashboard full of graphs:
Beszel
→ Fast overview
Grafana can remain available for:
Historical analysis
Deeper troubleshooting
Detailed metrics
Log investigation
Resource Overhead
Beszel itself is lightweight enough to run alongside the existing Pi 5 infrastructure.
The Pi 5 currently has:
16 GB RAM
and normal overall resource usage remains low.
Therefore Beszel does not represent a meaningful resource concern for the current hardware.
Access
Direct LAN access:
<private service endpoint>
Preferred friendly access:
<private Beszel URL>
The friendly URL is preferred for normal use.
The direct URL remains useful for troubleshooting if NPM is unavailable.
Remote Access
When away from home, selected personal devices can reach the friendly URL through:
Tailscale
and:
Pi-hole
Example:
Remote Device
↓
Tailscale
↓
Pi 3 DNS / subnet route
↓
<private Beszel hostname>
↓
VM102 / NPM
↓
Pi 5 / Beszel
Beszel does not need to be publicly exposed.
Security
Beszel is an administrative monitoring service.
Important rules:
Keep Beszel private.
Do not expose it publicly without a clear need.
Require authentication.
Keep agents limited to trusted systems.
Do not store Beszel tokens in Obsidian.
Do not store agent public/private authentication material unnecessarily.
Keep Docker socket mounts read-only.
Use Tailscale for remote private access.
Agent Credentials
When a system is added to Beszel, the Hub generates agent authentication information.
Examples include:
Public Key
Token
These should not be recorded directly in this Obsidian vault.
Document the agent configuration without storing its secret token.
Proxmox Agent Updates
The native Proxmox Beszel agent was configured to allow automatic daily agent updates during installation.
This keeps the lightweight monitoring agent current without requiring frequent manual updates.
Reboot Recovery
The Proxmox host has already undergone a controlled reboot recovery test.
After the host returned:
HP ProDesk - Proxmox
Pi 5
VM100 - Jellyfin
VM102 - Management
returned to normal monitoring.
Beszel agents on always-on systems are configured to start automatically.
Failure Interpretation
Beszel Hub Down
Beszel dashboard unavailable
does not necessarily mean the monitored systems are down.
The monitoring interface itself may simply be unavailable.
Agent Down
If one system stops reporting while others continue:
Check that system's Beszel agent
before assuming the entire machine is offline.
NPM Down
Friendly URL:
<private Beszel URL>
may fail while the direct Hub still works:
<private service endpoint>
Pi-hole Down
The friendly hostname may stop resolving for selected clients while the direct Hub address still works.
Current System Summary
Beszel Hub
└── Raspberry Pi 5
└── <private Pi 5 host>:<SERVICE_PORT>
Monitored Systems
├── HP ProDesk - Proxmox
│ └── Native binary agent
│
├── Pi 5
│ └── Local Docker agent / socket
│
├── VM100 - Jellyfin
│ └── Docker agent
│
└── VM102 - Management
└── Docker agent
Intentionally Not Monitored
Raspberry Pi 3 is intentionally not currently included.
Reason:
Keep the dashboard focused
+
Pi 3 is a lightweight dedicated DNS/networking node
If resource monitoring becomes useful later, it can always be added.
Related Notes
Homelab Overview
Raspberry Pi 5
HP ProDesk 600 G4
Proxmox Host
VM100 - Jellyfin
VM102 - Management
Uptime Kuma
Nginx Proxy Manager
Pi-hole
Tailscale
Back to Services