Skip to notes

Public Homelab Notes / 04

Services

The website, media server, availability checks, and resource monitoring I run.

4 notes · Current setups, past observations, and future plans stay as recorded.

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