Files
docs/infra/proxmox-state/index.html
T

5520 lines
170 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Single source of truth for the homelab">
<link rel="canonical" href="http://192.168.1.8:13080/infra/proxmox-state/">
<link rel="prev" href="../../ct-inventory/">
<link rel="next" href="../portmap/">
<link rel="icon" href="../../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.6">
<title>Proxmox state - nuclide.systems docs</title>
<link rel="stylesheet" href="../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="black" data-md-color-accent="blue">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#proxmox-host-optimization-inventory-nuc" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../.." title="nuclide.systems docs" class="md-header__button md-logo" aria-label="nuclide.systems docs" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
nuclide.systems docs
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Proxmox state
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="(prefers-color-scheme: dark)" data-md-color-scheme="slate" data-md-color-primary="black" data-md-color-accent="blue" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
<input class="md-option" data-md-color-media="(prefers-color-scheme: light)" data-md-color-scheme="default" data-md-color-primary="black" data-md-color-accent="blue" aria-hidden="true" type="radio" name="__palette" id="__palette_1">
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item">
<a href="../../CHANGELOG/" class="md-tabs__link">
Changelog
</a>
</li>
<li class="md-tabs__item">
<a href="../../RESUME/" class="md-tabs__link">
Resume
</a>
</li>
<li class="md-tabs__item">
<a href="../../ct-inventory/" class="md-tabs__link">
CT inventory
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="./" class="md-tabs__link">
Infra
</a>
</li>
<li class="md-tabs__item">
<a href="../../services/homelab-architecture/" class="md-tabs__link">
Services
</a>
</li>
<li class="md-tabs__item">
<a href="../../security/data-leak-audit-comparison/" class="md-tabs__link">
Security & audits
</a>
</li>
<li class="md-tabs__item">
<a href="../../ideas/stack-ideas/" class="md-tabs__link">
Ideas
</a>
</li>
<li class="md-tabs__item">
<a href="../../history/traefik-migration/" class="md-tabs__link">
History
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../.." title="nuclide.systems docs" class="md-nav__button md-logo" aria-label="nuclide.systems docs" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
nuclide.systems docs
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../CHANGELOG/" class="md-nav__link">
<span class="md-ellipsis">
Changelog
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../RESUME/" class="md-nav__link">
<span class="md-ellipsis">
Resume
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../ct-inventory/" class="md-nav__link">
<span class="md-ellipsis">
CT inventory
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5" checked>
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="">
<span class="md-ellipsis">
Infra
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Infra
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Proxmox state
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Proxmox state
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#tldr-top-5-actionable-wins" class="md-nav__link">
<span class="md-ellipsis">
TL;DR — top 5 actionable wins
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#1-system-snapshot" class="md-nav__link">
<span class="md-ellipsis">
1. System snapshot
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-memory-vmct-sizing-measured-numbers" class="md-nav__link">
<span class="md-ellipsis">
2. Memory &amp; VM/CT sizing (measured numbers)
</span>
</a>
<nav class="md-nav" aria-label="2. Memory &amp; VM/CT sizing (measured numbers)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#concrete-ct-101-picture" class="md-nav__link">
<span class="md-ellipsis">
Concrete CT 101 picture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#concrete-ct-104-picture" class="md-nav__link">
<span class="md-ellipsis">
Concrete CT 104 picture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#other-guests" class="md-nav__link">
<span class="md-ellipsis">
Other guests
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#3-zfs-tuning" class="md-nav__link">
<span class="md-ellipsis">
3. ZFS tuning
</span>
</a>
<nav class="md-nav" aria-label="3. ZFS tuning">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#pool" class="md-nav__link">
<span class="md-ellipsis">
Pool
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#arc" class="md-nav__link">
<span class="md-ellipsis">
ARC
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#per-dataset" class="md-nav__link">
<span class="md-ellipsis">
Per-dataset
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pool-features" class="md-nav__link">
<span class="md-ellipsis">
Pool features
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#commands" class="md-nav__link">
<span class="md-ellipsis">
Commands
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#4-storage-vm-disk-options" class="md-nav__link">
<span class="md-ellipsis">
4. Storage &amp; VM disk options
</span>
</a>
<nav class="md-nav" aria-label="4. Storage &amp; VM disk options">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#vm-100-haos" class="md-nav__link">
<span class="md-ellipsis">
VM 100 (haos)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#lxc-local-zfs-storage" class="md-nav__link">
<span class="md-ellipsis">
LXC local-zfs storage
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#unas-share-current-state-measured" class="md-nav__link">
<span class="md-ellipsis">
UNAS share — current state (measured)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#5-cpu-boot-kernel" class="md-nav__link">
<span class="md-ellipsis">
5. CPU / boot / kernel
</span>
</a>
<nav class="md-nav" aria-label="5. CPU / boot / kernel">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#hybrid-core-scheduling" class="md-nav__link">
<span class="md-ellipsis">
Hybrid-core scheduling
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#6-reliability-spof" class="md-nav__link">
<span class="md-ellipsis">
6. Reliability / SPOF
</span>
</a>
<nav class="md-nav" aria-label="6. Reliability / SPOF">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#single-disk-is-the-biggest-risk" class="md-nav__link">
<span class="md-ellipsis">
Single disk is the biggest risk
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#boot-redundancy" class="md-nav__link">
<span class="md-ellipsis">
Boot redundancy
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#7-backups-high-priority-silent-risk" class="md-nav__link">
<span class="md-ellipsis">
7. Backups (HIGH PRIORITY — silent risk)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#8-apt-repositories-cleanup" class="md-nav__link">
<span class="md-ellipsis">
8. APT / repositories cleanup
</span>
</a>
<nav class="md-nav" aria-label="8. APT / repositories cleanup">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#unattended-upgrades-configured-2026-05-20" class="md-nav__link">
<span class="md-ellipsis">
Unattended-upgrades (configured 2026-05-20)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#9-networking" class="md-nav__link">
<span class="md-ellipsis">
9. Networking
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#10-container-specific-issues" class="md-nav__link">
<span class="md-ellipsis">
10. Container-specific issues
</span>
</a>
<nav class="md-nav" aria-label="10. Container-specific issues">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#ct-104-docker-ai-image-gen-48-gib-cap-16-cores-gpu-passthrough" class="md-nav__link">
<span class="md-ellipsis">
CT 104 (docker / AI / image-gen) — 48 GiB cap, 16 cores, GPU passthrough
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-101-shepard-docker-currently-160-gib-cap-peak-156-gib" class="md-nav__link">
<span class="md-ellipsis">
CT 101 (shepard / docker) — currently 160 GiB cap, peak 15.6 GiB
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-102-adguard-dns-undersized" class="md-nav__link">
<span class="md-ellipsis">
CT 102 (adguard / DNS) — undersized
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-103-backrest-fine" class="md-nav__link">
<span class="md-ellipsis">
CT 103 (backrest) — fine
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-105-nextcloud-privileged-cifs" class="md-nav__link">
<span class="md-ellipsis">
CT 105 (nextcloud) — privileged + CIFS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-108-zoraxy-slight-oversize" class="md-nav__link">
<span class="md-ellipsis">
CT 108 (zoraxy) — slight oversize
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-104-docker-stacks-inventory-optstacks" class="md-nav__link">
<span class="md-ellipsis">
CT 104 — Docker stacks inventory (/opt/stacks)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#general-lxc-hygiene" class="md-nav__link">
<span class="md-ellipsis">
General LXC hygiene
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#anti-fat-finger-protection-protection-1-boot-order" class="md-nav__link">
<span class="md-ellipsis">
Anti-fat-finger protection (protection: 1) + boot order
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#11a-dns-adguard-rewrites-site-wide-consistency" class="md-nav__link">
<span class="md-ellipsis">
11a. DNS — AdGuard rewrites &amp; site-wide consistency
</span>
</a>
<nav class="md-nav" aria-label="11a. DNS — AdGuard rewrites &amp; site-wide consistency">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#why-this-matters" class="md-nav__link">
<span class="md-ellipsis">
Why this matters
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#recommended-rewrite-set" class="md-nav__link">
<span class="md-ellipsis">
Recommended rewrite set
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#verify-clients-are-actually-using-adguard" class="md-nav__link">
<span class="md-ellipsis">
Verify clients are actually using AdGuard
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#optional-hardening-once-the-rewrites-are-stable" class="md-nav__link">
<span class="md-ellipsis">
Optional hardening once the rewrites are stable
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#action-checklist" class="md-nav__link">
<span class="md-ellipsis">
Action checklist
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#11b-tls-certificates-for-the-proxmox-web-ui" class="md-nav__link">
<span class="md-ellipsis">
11b. TLS certificates for the Proxmox web UI
</span>
</a>
<nav class="md-nav" aria-label="11b. TLS certificates for the Proxmox web UI">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#option-a-reverse-proxy-pve-through-zoraxy-recommended" class="md-nav__link">
<span class="md-ellipsis">
Option A — Reverse-proxy PVE through Zoraxy (recommended)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#option-b-pves-built-in-acme-with-dns-01" class="md-nav__link">
<span class="md-ellipsis">
Option B — PVE's built-in ACME with DNS-01
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#option-c-push-zoraxys-cert-into-pve" class="md-nav__link">
<span class="md-ellipsis">
Option C — Push Zoraxy's cert into PVE
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#recommended-path" class="md-nav__link">
<span class="md-ellipsis">
Recommended path
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#other-cts-with-web-uis-worth-fronting-via-zoraxy" class="md-nav__link">
<span class="md-ellipsis">
Other CTs with web UIs worth fronting via Zoraxy
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#11-maintenance-observability" class="md-nav__link">
<span class="md-ellipsis">
11. Maintenance / observability
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#13-update-management-current-model" class="md-nav__link">
<span class="md-ellipsis">
13. Update management — current model
</span>
</a>
<nav class="md-nav" aria-label="13. Update management — current model">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#layer-1-host-packages-debian-proxmox" class="md-nav__link">
<span class="md-ellipsis">
Layer 1 — Host packages (Debian + Proxmox)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#layer-2-ct-os-packages-debian" class="md-nav__link">
<span class="md-ellipsis">
Layer 2 — CT OS packages (Debian)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#layer-3-helper-script-app-binaries-adguard-zoraxy" class="md-nav__link">
<span class="md-ellipsis">
Layer 3 — Helper-script app binaries (AdGuard, Zoraxy)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#layer-4-docker-engine-inside-cts" class="md-nav__link">
<span class="md-ellipsis">
Layer 4 — Docker engine inside CTs
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#layer-5-docker-images-the-75-containers" class="md-nav__link">
<span class="md-ellipsis">
Layer 5 — Docker images (the ~75 containers)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#14-vm-100-haos-auto-restart-watchdog" class="md-nav__link">
<span class="md-ellipsis">
14. VM 100 (HAOS) auto-restart watchdog
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#15-identity-pocket-id-on-its-own-ct" class="md-nav__link">
<span class="md-ellipsis">
15. Identity — Pocket-ID on its own CT
</span>
</a>
<nav class="md-nav" aria-label="15. Identity — Pocket-ID on its own CT">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#state-as-of-2026-05-20" class="md-nav__link">
<span class="md-ellipsis">
State as of 2026-05-20
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duplication-procedure-used" class="md-nav__link">
<span class="md-ellipsis">
Duplication procedure used
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#zoraxy-cutover" class="md-nav__link">
<span class="md-ellipsis">
Zoraxy cutover
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#secrets-rotation-list-deferred-to-cutover-day" class="md-nav__link">
<span class="md-ellipsis">
Secrets-rotation list (deferred to cutover day)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#16-ct-109-ops-planned-observability-ops-lxc" class="md-nav__link">
<span class="md-ellipsis">
16. CT 109 "ops" — planned observability + ops LXC
</span>
</a>
<nav class="md-nav" aria-label="16. CT 109 &#34;ops&#34; — planned observability + ops LXC">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#stack-to-deploy-on-ct-109" class="md-nav__link">
<span class="md-ellipsis">
Stack to deploy on CT 109
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#sidecars-deployed-on-each-host" class="md-nav__link">
<span class="md-ellipsis">
Sidecars deployed on each host
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#services-that-stay-where-they-are-not-on-ct-109" class="md-nav__link">
<span class="md-ellipsis">
Services that stay where they are (NOT on CT 109)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#migration-of-gotify-tier-b-schedule-when-ct-109-is-otherwise-stable" class="md-nav__link">
<span class="md-ellipsis">
Migration of Gotify (Tier B — schedule when CT 109 is otherwise stable)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#migration-of-mcp-gateway-tier-b-split-control-workload" class="md-nav__link">
<span class="md-ellipsis">
Migration of MCP Gateway (Tier B — split control / workload)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#arcane-specifics" class="md-nav__link">
<span class="md-ellipsis">
Arcane specifics
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#build-order" class="md-nav__link">
<span class="md-ellipsis">
Build order
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#17-lldp-unifi-topology-visibility" class="md-nav__link">
<span class="md-ellipsis">
17. LLDP / UniFi topology visibility
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#18a-unas-access-uid-consistency-model-post-nfsv4-investigation" class="md-nav__link">
<span class="md-ellipsis">
18a. UNAS access — UID consistency model (post-NFSv4 investigation)
</span>
</a>
<nav class="md-nav" aria-label="18a. UNAS access — UID consistency model (post-NFSv4 investigation)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#universal-uid-landscape-on-unas" class="md-nav__link">
<span class="md-ellipsis">
Universal UID landscape on UNAS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#per-ct-access-pattern-canonical" class="md-nav__link">
<span class="md-ellipsis">
Per-CT access pattern (canonical)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#convention-for-new-containers" class="md-nav__link">
<span class="md-ellipsis">
Convention for new containers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#why-bindfs-for-ct-105-specifically" class="md-nav__link">
<span class="md-ellipsis">
Why bindfs for CT 105 specifically
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#trigger-event-to-revisit" class="md-nav__link">
<span class="md-ellipsis">
Trigger event to revisit
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#18-homarr-inventory-services-to-include-on-the-dashboard" class="md-nav__link">
<span class="md-ellipsis">
18. Homarr inventory — services to include on the dashboard
</span>
</a>
<nav class="md-nav" aria-label="18. Homarr inventory — services to include on the dashboard">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#group-infrastructure" class="md-nav__link">
<span class="md-ellipsis">
Group: infrastructure
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#group-network" class="md-nav__link">
<span class="md-ellipsis">
Group: network
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#group-ops-to-populate-when-ct-109-lands" class="md-nav__link">
<span class="md-ellipsis">
Group: ops (to populate when CT 109 lands)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#group-apps-subset-long-list-fill-from-existing-homepage-labels-in-optstacks" class="md-nav__link">
<span class="md-ellipsis">
Group: apps (subset — long list, fill from existing homepage.* labels in /opt/stacks/*/)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#12-suggested-action-order" class="md-nav__link">
<span class="md-ellipsis">
12. Suggested action order
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#19-changes-applied-2026-05-20-session-2" class="md-nav__link">
<span class="md-ellipsis">
19. Changes applied 2026-05-20 (session 2)
</span>
</a>
<nav class="md-nav" aria-label="19. Changes applied 2026-05-20 (session 2)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#optimizations-executed" class="md-nav__link">
<span class="md-ellipsis">
Optimizations executed
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pocket-id-migration-completed" class="md-nav__link">
<span class="md-ellipsis">
Pocket-ID migration completed
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#proxmox-oidc-via-pocket-id" class="md-nav__link">
<span class="md-ellipsis">
Proxmox OIDC via Pocket-ID
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#audit-footnote-side-effects-of-this-run" class="md-nav__link">
<span class="md-ellipsis">
Audit footnote — side effects of this run
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../portmap/" class="md-nav__link">
<span class="md-ellipsis">
Port map
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../storage/" class="md-nav__link">
<span class="md-ellipsis">
Storage
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../volumes/" class="md-nav__link">
<span class="md-ellipsis">
Volumes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../docker-networks/" class="md-nav__link">
<span class="md-ellipsis">
Docker networks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../connection-hosts/" class="md-nav__link">
<span class="md-ellipsis">
Connection hosts
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../proxmox-memory-audit/" class="md-nav__link">
<span class="md-ellipsis">
Memory audit
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
Services
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../services/homelab-architecture/" class="md-nav__link">
<span class="md-ellipsis">
Homelab architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/dev-environment/" class="md-nav__link">
<span class="md-ellipsis">
Dev environment (Coder + Gitea)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/mcp-gateway/" class="md-nav__link">
<span class="md-ellipsis">
MCP gateway
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/mcp-servers/" class="md-nav__link">
<span class="md-ellipsis">
MCP servers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/pocket-id/" class="md-nav__link">
<span class="md-ellipsis">
Pocket-ID (OIDC)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/backrest/" class="md-nav__link">
<span class="md-ellipsis">
Backrest (backups)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/databases/" class="md-nav__link">
<span class="md-ellipsis">
Databases
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/secrets-manager/" class="md-nav__link">
<span class="md-ellipsis">
Secrets manager
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/comfyui/" class="md-nav__link">
<span class="md-ellipsis">
ComfyUI
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/zoraxy/" class="md-nav__link">
<span class="md-ellipsis">
Zoraxy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/adguard-dns/" class="md-nav__link">
<span class="md-ellipsis">
AdGuard DNS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/cloud-gpu/" class="md-nav__link">
<span class="md-ellipsis">
Cloud GPU
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/arcane/" class="md-nav__link">
<span class="md-ellipsis">
Arcane
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/doc-ingestion/" class="md-nav__link">
<span class="md-ellipsis">
Doc ingestion pipeline
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/llm-benchmark/" class="md-nav__link">
<span class="md-ellipsis">
LLM benchmark
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_7" >
<label class="md-nav__link" for="__nav_7" id="__nav_7_label" tabindex="0">
<span class="md-ellipsis">
Security & audits
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_7_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_7">
<span class="md-nav__icon md-icon"></span>
Security & audits
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../security/data-leak-audit-comparison/" class="md-nav__link">
<span class="md-ellipsis">
Comparison
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/data-leak-audit-2026-05-20-tr004-cloud-sandbox/" class="md-nav__link">
<span class="md-ellipsis">
2026-05-20 · cloud-sandbox breach
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/data-leak-audit-2026-05-21-tr004-artifacts/" class="md-nav__link">
<span class="md-ellipsis">
2026-05-21 · artifacts (clean)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/audit-claude-code-meta/" class="md-nav__link">
<span class="md-ellipsis">
Self-audit (Claude Code)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/transcripts/audit-chat-2026-05-21/" class="md-nav__link">
<span class="md-ellipsis">
Transcript (audit session)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_8" >
<label class="md-nav__link" for="__nav_8" id="__nav_8_label" tabindex="0">
<span class="md-ellipsis">
Ideas
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_8_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_8">
<span class="md-nav__icon md-icon"></span>
Ideas
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../ideas/stack-ideas/" class="md-nav__link">
<span class="md-ellipsis">
Stack ideas
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_9" >
<label class="md-nav__link" for="__nav_9" id="__nav_9_label" tabindex="0">
<span class="md-ellipsis">
History
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_9_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_9">
<span class="md-nav__icon md-icon"></span>
History
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../history/traefik-migration/" class="md-nav__link">
<span class="md-ellipsis">
Traefik (abandoned 2026-05-16)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../history/traefik-migration-docker-labels/" class="md-nav__link">
<span class="md-ellipsis">
Traefik labels (abandoned)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../history/mcp-gateway-requirements/" class="md-nav__link">
<span class="md-ellipsis">
MCP gateway requirements (superseded)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../history/scrubbing-list-2026-05-17/" class="md-nav__link">
<span class="md-ellipsis">
Scrubbing list (2026-05-17)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../history/case-study/" class="md-nav__link">
<span class="md-ellipsis">
Case study
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#tldr-top-5-actionable-wins" class="md-nav__link">
<span class="md-ellipsis">
TL;DR — top 5 actionable wins
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#1-system-snapshot" class="md-nav__link">
<span class="md-ellipsis">
1. System snapshot
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-memory-vmct-sizing-measured-numbers" class="md-nav__link">
<span class="md-ellipsis">
2. Memory &amp; VM/CT sizing (measured numbers)
</span>
</a>
<nav class="md-nav" aria-label="2. Memory &amp; VM/CT sizing (measured numbers)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#concrete-ct-101-picture" class="md-nav__link">
<span class="md-ellipsis">
Concrete CT 101 picture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#concrete-ct-104-picture" class="md-nav__link">
<span class="md-ellipsis">
Concrete CT 104 picture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#other-guests" class="md-nav__link">
<span class="md-ellipsis">
Other guests
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#3-zfs-tuning" class="md-nav__link">
<span class="md-ellipsis">
3. ZFS tuning
</span>
</a>
<nav class="md-nav" aria-label="3. ZFS tuning">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#pool" class="md-nav__link">
<span class="md-ellipsis">
Pool
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#arc" class="md-nav__link">
<span class="md-ellipsis">
ARC
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#per-dataset" class="md-nav__link">
<span class="md-ellipsis">
Per-dataset
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pool-features" class="md-nav__link">
<span class="md-ellipsis">
Pool features
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#commands" class="md-nav__link">
<span class="md-ellipsis">
Commands
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#4-storage-vm-disk-options" class="md-nav__link">
<span class="md-ellipsis">
4. Storage &amp; VM disk options
</span>
</a>
<nav class="md-nav" aria-label="4. Storage &amp; VM disk options">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#vm-100-haos" class="md-nav__link">
<span class="md-ellipsis">
VM 100 (haos)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#lxc-local-zfs-storage" class="md-nav__link">
<span class="md-ellipsis">
LXC local-zfs storage
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#unas-share-current-state-measured" class="md-nav__link">
<span class="md-ellipsis">
UNAS share — current state (measured)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#5-cpu-boot-kernel" class="md-nav__link">
<span class="md-ellipsis">
5. CPU / boot / kernel
</span>
</a>
<nav class="md-nav" aria-label="5. CPU / boot / kernel">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#hybrid-core-scheduling" class="md-nav__link">
<span class="md-ellipsis">
Hybrid-core scheduling
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#6-reliability-spof" class="md-nav__link">
<span class="md-ellipsis">
6. Reliability / SPOF
</span>
</a>
<nav class="md-nav" aria-label="6. Reliability / SPOF">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#single-disk-is-the-biggest-risk" class="md-nav__link">
<span class="md-ellipsis">
Single disk is the biggest risk
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#boot-redundancy" class="md-nav__link">
<span class="md-ellipsis">
Boot redundancy
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#7-backups-high-priority-silent-risk" class="md-nav__link">
<span class="md-ellipsis">
7. Backups (HIGH PRIORITY — silent risk)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#8-apt-repositories-cleanup" class="md-nav__link">
<span class="md-ellipsis">
8. APT / repositories cleanup
</span>
</a>
<nav class="md-nav" aria-label="8. APT / repositories cleanup">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#unattended-upgrades-configured-2026-05-20" class="md-nav__link">
<span class="md-ellipsis">
Unattended-upgrades (configured 2026-05-20)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#9-networking" class="md-nav__link">
<span class="md-ellipsis">
9. Networking
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#10-container-specific-issues" class="md-nav__link">
<span class="md-ellipsis">
10. Container-specific issues
</span>
</a>
<nav class="md-nav" aria-label="10. Container-specific issues">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#ct-104-docker-ai-image-gen-48-gib-cap-16-cores-gpu-passthrough" class="md-nav__link">
<span class="md-ellipsis">
CT 104 (docker / AI / image-gen) — 48 GiB cap, 16 cores, GPU passthrough
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-101-shepard-docker-currently-160-gib-cap-peak-156-gib" class="md-nav__link">
<span class="md-ellipsis">
CT 101 (shepard / docker) — currently 160 GiB cap, peak 15.6 GiB
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-102-adguard-dns-undersized" class="md-nav__link">
<span class="md-ellipsis">
CT 102 (adguard / DNS) — undersized
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-103-backrest-fine" class="md-nav__link">
<span class="md-ellipsis">
CT 103 (backrest) — fine
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-105-nextcloud-privileged-cifs" class="md-nav__link">
<span class="md-ellipsis">
CT 105 (nextcloud) — privileged + CIFS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-108-zoraxy-slight-oversize" class="md-nav__link">
<span class="md-ellipsis">
CT 108 (zoraxy) — slight oversize
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-104-docker-stacks-inventory-optstacks" class="md-nav__link">
<span class="md-ellipsis">
CT 104 — Docker stacks inventory (/opt/stacks)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#general-lxc-hygiene" class="md-nav__link">
<span class="md-ellipsis">
General LXC hygiene
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#anti-fat-finger-protection-protection-1-boot-order" class="md-nav__link">
<span class="md-ellipsis">
Anti-fat-finger protection (protection: 1) + boot order
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#11a-dns-adguard-rewrites-site-wide-consistency" class="md-nav__link">
<span class="md-ellipsis">
11a. DNS — AdGuard rewrites &amp; site-wide consistency
</span>
</a>
<nav class="md-nav" aria-label="11a. DNS — AdGuard rewrites &amp; site-wide consistency">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#why-this-matters" class="md-nav__link">
<span class="md-ellipsis">
Why this matters
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#recommended-rewrite-set" class="md-nav__link">
<span class="md-ellipsis">
Recommended rewrite set
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#verify-clients-are-actually-using-adguard" class="md-nav__link">
<span class="md-ellipsis">
Verify clients are actually using AdGuard
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#optional-hardening-once-the-rewrites-are-stable" class="md-nav__link">
<span class="md-ellipsis">
Optional hardening once the rewrites are stable
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#action-checklist" class="md-nav__link">
<span class="md-ellipsis">
Action checklist
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#11b-tls-certificates-for-the-proxmox-web-ui" class="md-nav__link">
<span class="md-ellipsis">
11b. TLS certificates for the Proxmox web UI
</span>
</a>
<nav class="md-nav" aria-label="11b. TLS certificates for the Proxmox web UI">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#option-a-reverse-proxy-pve-through-zoraxy-recommended" class="md-nav__link">
<span class="md-ellipsis">
Option A — Reverse-proxy PVE through Zoraxy (recommended)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#option-b-pves-built-in-acme-with-dns-01" class="md-nav__link">
<span class="md-ellipsis">
Option B — PVE's built-in ACME with DNS-01
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#option-c-push-zoraxys-cert-into-pve" class="md-nav__link">
<span class="md-ellipsis">
Option C — Push Zoraxy's cert into PVE
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#recommended-path" class="md-nav__link">
<span class="md-ellipsis">
Recommended path
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#other-cts-with-web-uis-worth-fronting-via-zoraxy" class="md-nav__link">
<span class="md-ellipsis">
Other CTs with web UIs worth fronting via Zoraxy
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#11-maintenance-observability" class="md-nav__link">
<span class="md-ellipsis">
11. Maintenance / observability
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#13-update-management-current-model" class="md-nav__link">
<span class="md-ellipsis">
13. Update management — current model
</span>
</a>
<nav class="md-nav" aria-label="13. Update management — current model">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#layer-1-host-packages-debian-proxmox" class="md-nav__link">
<span class="md-ellipsis">
Layer 1 — Host packages (Debian + Proxmox)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#layer-2-ct-os-packages-debian" class="md-nav__link">
<span class="md-ellipsis">
Layer 2 — CT OS packages (Debian)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#layer-3-helper-script-app-binaries-adguard-zoraxy" class="md-nav__link">
<span class="md-ellipsis">
Layer 3 — Helper-script app binaries (AdGuard, Zoraxy)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#layer-4-docker-engine-inside-cts" class="md-nav__link">
<span class="md-ellipsis">
Layer 4 — Docker engine inside CTs
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#layer-5-docker-images-the-75-containers" class="md-nav__link">
<span class="md-ellipsis">
Layer 5 — Docker images (the ~75 containers)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#14-vm-100-haos-auto-restart-watchdog" class="md-nav__link">
<span class="md-ellipsis">
14. VM 100 (HAOS) auto-restart watchdog
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#15-identity-pocket-id-on-its-own-ct" class="md-nav__link">
<span class="md-ellipsis">
15. Identity — Pocket-ID on its own CT
</span>
</a>
<nav class="md-nav" aria-label="15. Identity — Pocket-ID on its own CT">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#state-as-of-2026-05-20" class="md-nav__link">
<span class="md-ellipsis">
State as of 2026-05-20
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duplication-procedure-used" class="md-nav__link">
<span class="md-ellipsis">
Duplication procedure used
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#zoraxy-cutover" class="md-nav__link">
<span class="md-ellipsis">
Zoraxy cutover
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#secrets-rotation-list-deferred-to-cutover-day" class="md-nav__link">
<span class="md-ellipsis">
Secrets-rotation list (deferred to cutover day)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#16-ct-109-ops-planned-observability-ops-lxc" class="md-nav__link">
<span class="md-ellipsis">
16. CT 109 "ops" — planned observability + ops LXC
</span>
</a>
<nav class="md-nav" aria-label="16. CT 109 &#34;ops&#34; — planned observability + ops LXC">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#stack-to-deploy-on-ct-109" class="md-nav__link">
<span class="md-ellipsis">
Stack to deploy on CT 109
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#sidecars-deployed-on-each-host" class="md-nav__link">
<span class="md-ellipsis">
Sidecars deployed on each host
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#services-that-stay-where-they-are-not-on-ct-109" class="md-nav__link">
<span class="md-ellipsis">
Services that stay where they are (NOT on CT 109)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#migration-of-gotify-tier-b-schedule-when-ct-109-is-otherwise-stable" class="md-nav__link">
<span class="md-ellipsis">
Migration of Gotify (Tier B — schedule when CT 109 is otherwise stable)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#migration-of-mcp-gateway-tier-b-split-control-workload" class="md-nav__link">
<span class="md-ellipsis">
Migration of MCP Gateway (Tier B — split control / workload)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#arcane-specifics" class="md-nav__link">
<span class="md-ellipsis">
Arcane specifics
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#build-order" class="md-nav__link">
<span class="md-ellipsis">
Build order
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#17-lldp-unifi-topology-visibility" class="md-nav__link">
<span class="md-ellipsis">
17. LLDP / UniFi topology visibility
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#18a-unas-access-uid-consistency-model-post-nfsv4-investigation" class="md-nav__link">
<span class="md-ellipsis">
18a. UNAS access — UID consistency model (post-NFSv4 investigation)
</span>
</a>
<nav class="md-nav" aria-label="18a. UNAS access — UID consistency model (post-NFSv4 investigation)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#universal-uid-landscape-on-unas" class="md-nav__link">
<span class="md-ellipsis">
Universal UID landscape on UNAS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#per-ct-access-pattern-canonical" class="md-nav__link">
<span class="md-ellipsis">
Per-CT access pattern (canonical)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#convention-for-new-containers" class="md-nav__link">
<span class="md-ellipsis">
Convention for new containers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#why-bindfs-for-ct-105-specifically" class="md-nav__link">
<span class="md-ellipsis">
Why bindfs for CT 105 specifically
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#trigger-event-to-revisit" class="md-nav__link">
<span class="md-ellipsis">
Trigger event to revisit
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#18-homarr-inventory-services-to-include-on-the-dashboard" class="md-nav__link">
<span class="md-ellipsis">
18. Homarr inventory — services to include on the dashboard
</span>
</a>
<nav class="md-nav" aria-label="18. Homarr inventory — services to include on the dashboard">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#group-infrastructure" class="md-nav__link">
<span class="md-ellipsis">
Group: infrastructure
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#group-network" class="md-nav__link">
<span class="md-ellipsis">
Group: network
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#group-ops-to-populate-when-ct-109-lands" class="md-nav__link">
<span class="md-ellipsis">
Group: ops (to populate when CT 109 lands)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#group-apps-subset-long-list-fill-from-existing-homepage-labels-in-optstacks" class="md-nav__link">
<span class="md-ellipsis">
Group: apps (subset — long list, fill from existing homepage.* labels in /opt/stacks/*/)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#12-suggested-action-order" class="md-nav__link">
<span class="md-ellipsis">
12. Suggested action order
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#19-changes-applied-2026-05-20-session-2" class="md-nav__link">
<span class="md-ellipsis">
19. Changes applied 2026-05-20 (session 2)
</span>
</a>
<nav class="md-nav" aria-label="19. Changes applied 2026-05-20 (session 2)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#optimizations-executed" class="md-nav__link">
<span class="md-ellipsis">
Optimizations executed
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pocket-id-migration-completed" class="md-nav__link">
<span class="md-ellipsis">
Pocket-ID migration completed
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#proxmox-oidc-via-pocket-id" class="md-nav__link">
<span class="md-ellipsis">
Proxmox OIDC via Pocket-ID
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#audit-footnote-side-effects-of-this-run" class="md-nav__link">
<span class="md-ellipsis">
Audit footnote — side effects of this run
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="proxmox-host-optimization-inventory-nuc">Proxmox Host Optimization Inventory — <code>nuc</code><a class="headerlink" href="#proxmox-host-optimization-inventory-nuc" title="Permanent link">&para;</a></h1>
<p>Generated: 2026-05-20
Host: <code>nuc</code> · PVE <code>9.1.11</code> · Kernel <code>6.17.13-4-pve</code> · Debian 13 (trixie)
CPU: Intel Core Ultra 7 155H (16C / 22T, hybrid P+E+LP-E) · 1 socket · 1 NUMA
RAM: 62 GiB physical · 31 GiB zram swap (50 % of RAM, zstd, prio 100)
Storage: single Crucial P3 2 TB NVMe (QLC, DRAM-less) → <code>rpool</code> (ZFS, ashift=12, no redundancy)
Workload: 1 VM (HAOS) + 9 LXCs (Docker, AdGuard, Backrest, Nextcloud, Zoraxy, Pocket-ID, Dev, Secrets, DB) + 1 planned (Ops/CT109)</p>
<hr />
<h2 id="tldr-top-5-actionable-wins">TL;DR — top 5 actionable wins<a class="headerlink" href="#tldr-top-5-actionable-wins" title="Permanent link">&para;</a></h2>
<ol>
<li><strong>Memory overcommit is dangerous.</strong> Allocated guest RAM (≈ 290 GiB) is ~4.7× physical (62 GiB). Right-size CT 101 (was 160 → done, now 32) and CT 104 (still 128) — see §2. ✅ <em>applied 2026-05-20</em></li>
<li><strong>ZFS ARC is artificially capped at 6.2 GiB.</strong> Default would be ~31 GiB. After §1 settles, raise to 16 GiB. See §3.</li>
<li><strong>No redundancy on a QLC SSD with 19 % wear and 59 TB written.</strong> Single-disk <code>rpool</code> on a DRAM-less consumer QLC drive is a SPOF. Add a second NVMe and convert to mirror — biggest reliability win available. See §6.</li>
<li><strong>Backups never prune.</strong> Was configured <code>keep-all=1</code> — fixed to <code>keep-last=3,keep-daily=7,keep-weekly=4,keep-monthly=6</code>. See §7. ✅ <em>applied 2026-05-20</em></li>
<li><strong><code>atime</code> and <code>autotrim</code></strong> on ZFS. ✅ <em>applied 2026-05-20</em></li>
<li><strong>No DNS rewrites</strong> in AdGuard — every internal target is IP-only; add a split-horizon for <code>nuclide.systems</code> and a <code>.lan</code> shorthand set. See §11a.</li>
<li><strong>Self-signed Proxmox web UI cert</strong> — front via Zoraxy for free LE. See §11b.</li>
</ol>
<hr />
<h2 id="1-system-snapshot">1. System snapshot<a class="headerlink" href="#1-system-snapshot" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Resource</th>
<th>State</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Load avg</td>
<td>normal</td>
<td>PSI: CPU some=2.4 % / IO some=1 % over 60 s</td>
</tr>
<tr>
<td>Memory</td>
<td>52 / 62 GiB used, 4.5 GiB free</td>
<td>tight; zram swap 15 GiB in use</td>
</tr>
<tr>
<td>Swap</td>
<td>zram0 (zstd, 31 GiB) prio 100</td>
<td>working as designed; just a symptom of §1</td>
</tr>
<tr>
<td>ARC</td>
<td>6.0 / 6.2 GiB (capped)</td>
<td>hit ratio ~99 % but cap is far below default</td>
</tr>
<tr>
<td>NVMe wear</td>
<td><strong>Percentage Used 19 %</strong>, 59.2 TB written</td>
<td>~5 % wear/year at current rate; healthy for now</td>
</tr>
<tr>
<td>Temperature</td>
<td>5658 °C</td>
<td>well under the 95 °C critical threshold</td>
</tr>
<tr>
<td>Uptime</td>
<td>(see <code>uptime</code>)</td>
<td>scrub clean, no checksum errors</td>
</tr>
<tr>
<td>Cluster</td>
<td>standalone</td>
<td>quorum OK, no HA configured</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="2-memory-vmct-sizing-measured-numbers">2. Memory &amp; VM/CT sizing (measured numbers)<a class="headerlink" href="#2-memory-vmct-sizing-measured-numbers" title="Permanent link">&para;</a></h2>
<p>Read from <code>/sys/fs/cgroup/lxc/&lt;id&gt;/memory.{current,peak,max}</code> and <code>free -h</code> inside each guest:</p>
<table>
<thead>
<tr>
<th>Guest</th>
<th>Cap</th>
<th>Current</th>
<th>Peak</th>
<th>Swap-in-use</th>
<th>Verdict</th>
</tr>
</thead>
<tbody>
<tr>
<td>VM 100 (haos)</td>
<td>16 384 MiB</td>
<td>13 056 MiB</td>
<td>n/a</td>
<td>n/a</td>
<td>balloon disabled; HAOS actually uses what it has</td>
</tr>
<tr>
<td>CT 101 (shepard)</td>
<td>160 000 MiB</td>
<td><strong>7.2 GiB</strong></td>
<td><strong>15.6 GiB</strong></td>
<td>706 MiB</td>
<td><strong>wildly over-sized</strong> — peak is 10 % of cap</td>
</tr>
<tr>
<td>CT 102 (adguard)</td>
<td>512 MiB</td>
<td>343 MiB</td>
<td><strong>509 MiB (99 %)</strong></td>
<td>19 MiB</td>
<td><strong>under-sized</strong> — at the cap, AdGuardHome alone is 350 MiB</td>
</tr>
<tr>
<td>CT 103 (backrest)</td>
<td>512 MiB</td>
<td>89 MiB</td>
<td>305 MiB</td>
<td>16 MiB</td>
<td>fine</td>
</tr>
<tr>
<td>CT 104 (docker/AI)</td>
<td>128 000 MiB</td>
<td>18.8 GiB</td>
<td><strong>29.3 GiB</strong></td>
<td><strong>9.3 GiB</strong></td>
<td>real workload, but currently swapping — likely starved by CT 101</td>
</tr>
<tr>
<td>CT 105 (nextcloud)</td>
<td>8 192 MiB</td>
<td>2.1 GiB</td>
<td>3.5 GiB</td>
<td>53 MiB</td>
<td>fine</td>
</tr>
<tr>
<td>CT 108 (zoraxy)</td>
<td>2 048 MiB</td>
<td>271 MiB</td>
<td>463 MiB</td>
<td>25 MiB</td>
<td>fine; could halve</td>
</tr>
</tbody>
</table>
<p><strong>Sum of declared caps ≈ 290 GiB on a 62 GiB host.</strong> Sum of actual peaks ≈ 49 GiB — totally fits. CT 101's 160 GB cap is the entire problem: it's a phantom that scares the scheduler without using anything close to that.</p>
<h3 id="concrete-ct-101-picture">Concrete CT 101 picture<a class="headerlink" href="#concrete-ct-101-picture" title="Permanent link">&para;</a></h3>
<p>12 cores, load avg 8.5, ~9 Docker containers (Shepard frontend/backend, Keycloak, Neo4j, MongoDB, MongoExpress, TimescaleDB, Caddy, home-showcase-collector). Peak RSS 15.6 GiB.</p>
<p>→ Drop memory cap to <strong>32 GiB</strong> (2× peak headroom). No reboot required for LXC memory changes.</p>
<h3 id="concrete-ct-104-picture">Concrete CT 104 picture<a class="headerlink" href="#concrete-ct-104-picture" title="Permanent link">&para;</a></h3>
<p>16 cores, load avg 8.0, ~65 Docker containers including Immich (with ML/vectorchord), ComfyUI (image-gen), LobeChat, n8n, Daytona, LiteLLM, Vaultwarden, Paperless-ngx+AI, Karakeep, Memos, Gotify, Garage S3, plus a forest of MCP servers, Speaches (OpenVINO using the Arc iGPU). 128 GiB of 200 GiB rootfs used.</p>
<p>Peak RSS 29.3 GiB, but <strong>9.3 GiB sitting in swap</strong> — under memory pressure. Two paths:
1. <strong>Recommended</strong>: cut CT 101 first, then CT 104's pressure mostly disappears on its own. Re-measure peak after CT 101 is fixed. Likely safe to cap at <strong>48 GiB</strong> then.
2. Leave the 128 GiB cap as a generous ceiling — harmless once CT 101 is sane.</p>
<h3 id="other-guests">Other guests<a class="headerlink" href="#other-guests" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>CT 102 (AdGuard)</strong>: hitting its cap. Bump to <strong>1 GiB</strong> to give DNS room to breathe. (DNS is the single point of failure for the LAN — don't squeeze it.) <code>pct set 102 -memory 1024</code>.</li>
<li><strong>CT 108 (Zoraxy)</strong>: cap <strong>1 GiB</strong> is plenty.</li>
<li><strong>VM 100 (HAOS)</strong>: 13 GiB actually used. Enable ballooning so the host can reclaim idle RAM:
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>qm<span class="w"> </span><span class="nb">set</span><span class="w"> </span><span class="m">100</span><span class="w"> </span>-balloon<span class="w"> </span><span class="m">4096</span>
</code></pre></div>
This leaves memory=16384 as a ceiling but lets the host shrink it under pressure.</li>
<li>KSM is already active (10 255 shared pages) — fine.</li>
<li>zram swap: keep size at <code>ram / 2</code> until CT 101 is fixed; reduce to <code>ram / 4</code> afterwards.</li>
</ul>
<hr />
<h2 id="3-zfs-tuning">3. ZFS tuning<a class="headerlink" href="#3-zfs-tuning" title="Permanent link">&para;</a></h2>
<h3 id="pool">Pool<a class="headerlink" href="#pool" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Setting</th>
<th>Current</th>
<th>Recommend</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>autotrim</code></td>
<td><strong>off</strong></td>
<td><code>on</code></td>
<td>QLC needs trim; weekly fstrim alone is OK but autotrim is "free"</td>
</tr>
<tr>
<td><code>ashift</code></td>
<td>12</td>
<td>keep</td>
<td>correct for NVMe</td>
</tr>
<tr>
<td><code>atime</code></td>
<td><strong>on</strong> (relatime)</td>
<td><strong>off</strong></td>
<td>unused on a hypervisor; reduces write amp on QLC</td>
</tr>
<tr>
<td><code>xattr</code></td>
<td>sa</td>
<td>keep</td>
<td>already optimal</td>
</tr>
<tr>
<td><code>compression</code></td>
<td>on (lz4)</td>
<td>keep</td>
<td>helping (1.61× on HAOS disk)</td>
</tr>
<tr>
<td><code>dnodesize</code></td>
<td>legacy</td>
<td><code>auto</code></td>
<td>minor; only matters with millions of small files</td>
</tr>
<tr>
<td><code>recordsize</code> (rpool)</td>
<td>128 K</td>
<td>keep for general</td>
<td>tune per-dataset (see below)</td>
</tr>
</tbody>
</table>
<h3 id="arc">ARC<a class="headerlink" href="#arc" title="Permanent link">&para;</a></h3>
<p><code>/etc/modprobe.d/zfs.conf</code> currently caps <code>zfs_arc_max=6669991936</code> (≈ 6.2 GiB).
- After §2 sizing is done, raise this to 16 GiB: <code>options zfs zfs_arc_max=17179869184</code> and <code>zfs_arc_min=4294967296</code>.
- Apply live without reboot: <code>echo 17179869184 &gt; /sys/module/zfs/parameters/zfs_arc_max</code>.</p>
<h3 id="per-dataset">Per-dataset<a class="headerlink" href="#per-dataset" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>VM zvols</strong> (<code>rpool/data/vm-*</code>): default <code>volblocksize</code> is 16 K — fine. HAOS disk uses <code>cache=writethrough</code>; on ZFS, <strong>switch to <code>cache=none</code></strong> (or unset) — writethrough doubles the sync cost on top of ZFS's own integrity guarantees.</li>
<li><strong>CT 104 (<code>subvol-104-disk-0</code>, Docker + image-gen)</strong>: keep <code>recordsize=128K</code>. The workload is dominated by large model files and image outputs, not small-file DB traffic — shrinking the record size would hurt, not help.</li>
<li><strong>Nextcloud (<code>subvol-105-disk-1</code>)</strong>: leave at 128 K (mixed sizes, mostly larger files).</li>
</ul>
<h3 id="pool-features">Pool features<a class="headerlink" href="#pool-features" title="Permanent link">&para;</a></h3>
<p><code>zpool upgrade rpool</code> was run during this audit and enabled <code>redaction_list_spill</code> + <code>raidz_expansion</code>. Other disabled features (<code>fast_dedup</code>, <code>longname</code>, <code>large_microzap</code>, <code>dynamic_gang_header</code>, <code>block_cloning_endian</code>, <code>physical_rewrite</code>) can be enabled with another <code>zpool upgrade rpool</code> — only do this if you do not need to roll back to an older ZFS.</p>
<h3 id="commands">Commands<a class="headerlink" href="#commands" title="Permanent link">&para;</a></h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a>zpool<span class="w"> </span><span class="nb">set</span><span class="w"> </span><span class="nv">autotrim</span><span class="o">=</span>on<span class="w"> </span>rpool
<a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a>zfs<span class="w"> </span><span class="nb">set</span><span class="w"> </span><span class="nv">atime</span><span class="o">=</span>off<span class="w"> </span>rpool
<a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a><span class="c1"># (optional, once memory is sane):</span>
<a id="__codelineno-1-4" name="__codelineno-1-4" href="#__codelineno-1-4"></a><span class="nb">echo</span><span class="w"> </span><span class="s1">&#39;options zfs zfs_arc_max=17179869184&#39;</span><span class="w"> </span>&gt;<span class="w"> </span>/etc/modprobe.d/zfs.conf
<a id="__codelineno-1-5" name="__codelineno-1-5" href="#__codelineno-1-5"></a>update-initramfs<span class="w"> </span>-u<span class="w"> </span>-k<span class="w"> </span>all
</code></pre></div>
<hr />
<h2 id="4-storage-vm-disk-options">4. Storage &amp; VM disk options<a class="headerlink" href="#4-storage-vm-disk-options" title="Permanent link">&para;</a></h2>
<h3 id="vm-100-haos">VM 100 (<code>haos</code>)<a class="headerlink" href="#vm-100-haos" title="Permanent link">&para;</a></h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a><span class="gd">- scsi0: local-zfs:vm-100-disk-1,cache=writethrough,discard=on,size=32G,ssd=1</span>
<a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a><span class="gi">+ scsi0: local-zfs:vm-100-disk-1,cache=none,discard=on,iothread=1,size=32G,ssd=1</span>
</code></pre></div>
<ul>
<li><code>cache=none</code> (or remove cache entirely) — let ZFS manage caching.</li>
<li><code>iothread=1</code> with <code>virtio-scsi-pci</code> controller — already using <code>virtio-scsi-pci</code>, just add iothread.</li>
<li>Already has <code>discard=on</code> and <code>ssd=1</code></li>
</ul>
<h3 id="lxc-local-zfs-storage">LXC <code>local-zfs</code> storage<a class="headerlink" href="#lxc-local-zfs-storage" title="Permanent link">&para;</a></h3>
<ul>
<li><code>sparse 1</code> is set ✔ — thin-provisioned.</li>
<li>All CTs are on <code>local-zfs</code> rootfs; OK.</li>
</ul>
<h3 id="unas-share-current-state-measured">UNAS share — current state (measured)<a class="headerlink" href="#unas-share-current-state-measured" title="Permanent link">&para;</a></h3>
<p>Backend: <code>192.168.1.31</code> (looks like a UniFi NAS — exports <code>/volume/.../.unifi-drive/storage/.data</code>, the only NFS export listed is restricted to four allowed clients: the host <code>.20</code>, CT 104 <code>.40</code>, plus <code>.60</code> and <code>172.30.33.1</code>).</p>
<p>Two parallel mounts on the host pointed at the <strong>same backing data</strong>:</p>
<table>
<thead>
<tr>
<th>Mount</th>
<th>Type</th>
<th>Options (key bits)</th>
<th>Consumers</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/mnt/pve/unas</code></td>
<td>NFS <strong>v3</strong></td>
<td><code>proto=tcp, mountproto=udp, rsize/wsize=1M, hard, relatime, timeo=600</code></td>
<td>CT 103 (backrest), CT 104 (docker) — bind-mounted to <code>/mnt/pve/unas</code> inside</td>
</tr>
<tr>
<td><code>/mnt/pve/unas_smb</code></td>
<td>CIFS <strong>v3.1.1</strong></td>
<td><code>cache=strict, actimeo=1, soft, rsize/wsize=4M, uid/gid=33</code></td>
<td>CT 105 (nextcloud) — bind-mounted to <code>/mnt/pve/unas</code> inside</td>
</tr>
</tbody>
</table>
<p><strong>Issues:</strong></p>
<ol>
<li><strong>CT 105 is on CIFS to the same data CT 104 uses via NFS.</strong> Pure duplication. Nextcloud does massive amounts of <code>stat()</code> traffic; <code>actimeo=1</code> on the CIFS mount forces <em>every</em> metadata lookup to hit the wire, which is slow.</li>
<li><strong>NFS is v3, not v4.x.</strong> v4 is preferred unless the UDM doesn't export it. v4 fixes locking, removes the separate mountd dance, and supports session trunking.</li>
<li><strong><code>mountproto=udp</code></strong> under packet loss can intermittently fail to (re)mount. Set <code>mountproto=tcp</code>.</li>
<li><strong><code>hard</code> mount with no <code>intr</code> equivalent.</strong> If UNAS goes away, anything blocked on it hangs the calling process indefinitely. For non-critical use cases (Nextcloud, but <strong>not</strong> backrest), <code>soft,timeo=100,retrans=3</code> is friendlier — Backrest backups should stay <code>hard</code>.</li>
<li><strong>CT 105 cannot mount NFS directly</strong> because the UNAS export only allows IPs <code>.20/.40/.60/.172.30.33.1</code><code>.41</code> (CT 105) is missing. So either keep the host-side bind-mount approach (correct) or have UNAS export to <code>.41</code> too.</li>
<li><strong>The bind-mount approach is correct</strong> for unprivileged CTs that can't run NFS/CIFS clients themselves. Don't change that pattern.</li>
</ol>
<p><strong>Recommended consolidation:</strong></p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a><span class="c1"># 1. Probe whether the NAS speaks NFSv4</span>
<a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a>mount<span class="w"> </span>-t<span class="w"> </span>nfs<span class="w"> </span>-o<span class="w"> </span><span class="nv">vers</span><span class="o">=</span><span class="m">4</span>.2,proto<span class="o">=</span>tcp<span class="w"> </span><span class="m">192</span>.168.1.31:/var/nfs/shared/storage<span class="w"> </span>/mnt/test
<a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a><span class="c1"># if it works:</span>
<a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a>pvesm<span class="w"> </span><span class="nb">set</span><span class="w"> </span>unas<span class="w"> </span>--options<span class="w"> </span><span class="nv">vers</span><span class="o">=</span><span class="m">4</span>.2,proto<span class="o">=</span>tcp,hard,noatime
<a id="__codelineno-3-5" name="__codelineno-3-5" href="#__codelineno-3-5"></a><span class="c1"># (this re-mounts on next access; or unmount/remount /mnt/pve/unas)</span>
<a id="__codelineno-3-6" name="__codelineno-3-6" href="#__codelineno-3-6"></a>
<a id="__codelineno-3-7" name="__codelineno-3-7" href="#__codelineno-3-7"></a><span class="c1"># 2. Switch CT 105 to the NFS bind-mount</span>
<a id="__codelineno-3-8" name="__codelineno-3-8" href="#__codelineno-3-8"></a>pct<span class="w"> </span><span class="nb">set</span><span class="w"> </span><span class="m">105</span><span class="w"> </span>--mp0<span class="w"> </span>/mnt/pve/unas,mp<span class="o">=</span>/mnt/pve/unas
<a id="__codelineno-3-9" name="__codelineno-3-9" href="#__codelineno-3-9"></a><span class="c1"># (CT 105 currently uses unas_smb → unas. New line bind-mounts the NFS mount.)</span>
<a id="__codelineno-3-10" name="__codelineno-3-10" href="#__codelineno-3-10"></a><span class="c1"># Then verify nextcloud-aio still sees uid/gid 33 properly — NFS uses host UIDs,</span>
<a id="__codelineno-3-11" name="__codelineno-3-11" href="#__codelineno-3-11"></a><span class="c1"># whereas CIFS was forcing uid=33. May need to chown on the NAS or add an idmap.</span>
<a id="__codelineno-3-12" name="__codelineno-3-12" href="#__codelineno-3-12"></a>
<a id="__codelineno-3-13" name="__codelineno-3-13" href="#__codelineno-3-13"></a><span class="c1"># 3. Drop the CIFS storage once CT 105 is migrated</span>
<a id="__codelineno-3-14" name="__codelineno-3-14" href="#__codelineno-3-14"></a>pvesm<span class="w"> </span>remove<span class="w"> </span>unas_smb<span class="w"> </span><span class="c1"># if it exists as PVE storage</span>
<a id="__codelineno-3-15" name="__codelineno-3-15" href="#__codelineno-3-15"></a><span class="c1"># or remove the entry from /etc/pve/storage.cfg</span>
</code></pre></div>
<p><strong>Notes on perf:</strong></p>
<ul>
<li>1 GbE NIC ceiling is ~118 MB/s. NFS rsize/wsize=1M already saturates that.</li>
<li>If you have or add a 2.5 GbE / 10 GbE link on either side, also enable jumbo frames (MTU 9000) on <code>vmbr0</code>, the host NIC, and UNAS. That alone can ~double bulk-read throughput.</li>
<li>For Nextcloud's small-file stats: <code>actimeo=60</code> (NFS) or <code>cache=loose,actimeo=60</code> (CIFS, if you stay on it) — dramatically cuts roundtrips at the cost of slightly stale directory listings.</li>
</ul>
<hr />
<h2 id="5-cpu-boot-kernel">5. CPU / boot / kernel<a class="headerlink" href="#5-cpu-boot-kernel" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Item</th>
<th>State</th>
<th>Recommend</th>
</tr>
</thead>
<tbody>
<tr>
<td>Governor</td>
<td><code>performance</code></td>
<td>keep</td>
</tr>
<tr>
<td>HWP EPP</td>
<td><code>default</code></td>
<td>set to <code>balance_performance</code> if you want some idle savings without latency cost: <code>echo balance_performance &gt; /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference</code></td>
</tr>
<tr>
<td><code>intel_iommu=on iommu=pt</code></td>
<td>set ✔</td>
<td>keep</td>
</tr>
<tr>
<td>GPU passthrough (<code>i915.force_probe=!7dd5 xe.force_probe=7dd5</code>)</td>
<td>set for Arc Xe (Meteor Lake)</td>
<td>keep</td>
</tr>
<tr>
<td><code>nvme_core.default_ps_max_latency_us=0</code></td>
<td>set ✔</td>
<td>disables NVMe power-save — good for stability, costs ~1 W idle</td>
</tr>
<tr>
<td><code>kernel.numa_balancing</code></td>
<td>0</td>
<td>correct for single socket</td>
</tr>
<tr>
<td>Old kernels installed</td>
<td>6.17.13-4 (current) + 7.0.0-3</td>
<td>keep both for now; remove 7.0.0-3 once you've booted 7.0.2-5 successfully after the pending upgrade</td>
</tr>
</tbody>
</table>
<h3 id="hybrid-core-scheduling">Hybrid-core scheduling<a class="headerlink" href="#hybrid-core-scheduling" title="Permanent link">&para;</a></h3>
<p>The 155H has P-cores (cores 011), E-cores (1217), LP-E cores (1821). Linux 6.x with <code>intel_pstate=active</code> handles ITD/HWP well; no manual pinning is needed for current workloads. If a CT becomes latency-sensitive, you can pin it with <code>cpuset</code> via <code>lxc.cgroup2.cpuset.cpus</code> (P-cores only).</p>
<hr />
<h2 id="6-reliability-spof">6. Reliability / SPOF<a class="headerlink" href="#6-reliability-spof" title="Permanent link">&para;</a></h2>
<h3 id="single-disk-is-the-biggest-risk">Single disk is the biggest risk<a class="headerlink" href="#single-disk-is-the-biggest-risk" title="Permanent link">&para;</a></h3>
<ul>
<li>Current: 1× CT2000P3PSSD8 (QLC, DRAM-less). 19 % wear in (check <code>power_on_hours</code>).</li>
<li><strong>Recommendation:</strong> add a second 2 TB NVMe (ideally a TLC drive with DRAM — Crucial T500, WD SN770, Samsung 990 Pro, Solidigm P44 Pro). Then:
<div class="highlight"><pre><span></span><code><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a>zpool<span class="w"> </span>attach<span class="w"> </span>rpool<span class="w"> </span>nvme-CT2000P3PSSD8_2429E8BBCFB4-part3<span class="w"> </span>/dev/disk/by-id/&lt;new-disk&gt;-part3
</code></pre></div>
(requires partitioning the new disk to match — <code>sgdisk -R</code> from the existing). Pool becomes a mirror with full self-heal.</li>
<li>If a second internal slot isn't available, at minimum increase backup frequency for CT 101/104 (the irreplaceable data).</li>
</ul>
<h3 id="boot-redundancy">Boot redundancy<a class="headerlink" href="#boot-redundancy" title="Permanent link">&para;</a></h3>
<p><code>proxmox-boot-tool kernel list</code> shows one bootloader entry. After §6 mirror is set up, run <code>proxmox-boot-tool init /dev/&lt;new-disk&gt;-partN</code> so either disk can boot.</p>
<hr />
<h2 id="7-backups-high-priority-silent-risk">7. Backups (HIGH PRIORITY — silent risk)<a class="headerlink" href="#7-backups-high-priority-silent-risk" title="Permanent link">&para;</a></h2>
<p><code>/etc/pve/storage.cfg</code>:
<div class="highlight"><pre><span></span><code><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a>nfs: unas
<a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a> prune-backups keep-all=1
</code></pre></div></p>
<p><strong><code>keep-all=1</code> means backups are never deleted automatically.</strong> UNAS already holds 2 TB. Set a real policy, e.g.:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a>pvesm<span class="w"> </span><span class="nb">set</span><span class="w"> </span>unas<span class="w"> </span>--prune-backups<span class="w"> </span>keep-last<span class="o">=</span><span class="m">3</span>,keep-daily<span class="o">=</span><span class="m">7</span>,keep-weekly<span class="o">=</span><span class="m">4</span>,keep-monthly<span class="o">=</span><span class="m">6</span>
</code></pre></div>
<p>Also: there is <strong>no <code>vzdump</code> job configured</strong> in <code>/etc/pve/jobs.cfg</code>. Backups are either manual or driven from CT 103 (Backrest). Recommend a scheduled <code>vzdump</code> job for at least VM 100 and CT 101/104 <em>in addition to</em> Backrest, so PVE-native restores remain trivial.</p>
<hr />
<h2 id="8-apt-repositories-cleanup">8. APT / repositories cleanup<a class="headerlink" href="#8-apt-repositories-cleanup" title="Permanent link">&para;</a></h2>
<p>State today:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a>/etc/apt/sources.list.d/
<a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a>├── ceph.list # all lines commented — fine but consider deleting the file
<a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a>├── proxmox.sources # pve-no-subscription (modern deb822) ← keep
<a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a>├── pve-enterprise.list.bak # backup, safe to remove
<a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a>├── pve-enterprise.sources # Enabled: false ← keep as-is or remove
<a id="__codelineno-7-6" name="__codelineno-7-6" href="#__codelineno-7-6"></a>├── pve-install-repo.list # pve-no-subscription duplicate
<a id="__codelineno-7-7" name="__codelineno-7-7" href="#__codelineno-7-7"></a>└── pve-no-subscription.list # pve-no-subscription duplicate
</code></pre></div>
<p><code>pve-install-repo.list</code> and <code>pve-no-subscription.list</code> duplicate what <code>proxmox.sources</code> already declares. APT deduplicates fetches but the duplication is a foot-gun (one of them will go stale on the next PVE major version transition). Recommended cleanup:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a>rm<span class="w"> </span>/etc/apt/sources.list.d/pve-install-repo.list
<a id="__codelineno-8-2" name="__codelineno-8-2" href="#__codelineno-8-2"></a>rm<span class="w"> </span>/etc/apt/sources.list.d/pve-no-subscription.list
<a id="__codelineno-8-3" name="__codelineno-8-3" href="#__codelineno-8-3"></a>rm<span class="w"> </span>/etc/apt/sources.list.d/pve-enterprise.list.bak
<a id="__codelineno-8-4" name="__codelineno-8-4" href="#__codelineno-8-4"></a><span class="c1"># keep proxmox.sources and pve-enterprise.sources (already disabled)</span>
<a id="__codelineno-8-5" name="__codelineno-8-5" href="#__codelineno-8-5"></a>apt<span class="w"> </span>update
</code></pre></div>
<p>Also: there are <strong>9 pending upgrades</strong> including <code>pve-manager 9.1.18</code> (you're on 9.1.11) and a kernel update. Run <code>apt update &amp;&amp; apt full-upgrade</code> at a convenient window.</p>
<h3 id="unattended-upgrades-configured-2026-05-20">Unattended-upgrades (configured 2026-05-20)<a class="headerlink" href="#unattended-upgrades-configured-2026-05-20" title="Permanent link">&para;</a></h3>
<p>The host previously had a cron line <code>0 2 * * * apt-get update &amp;&amp; apt-get upgrade -y</code> that was silently no-op'ing on every kernel / PVE point release — <code>apt-get upgrade</code> refuses to install new dependencies, which PVE updates always introduce.</p>
<p>Replaced with <code>unattended-upgrades</code> in a conservative profile:</p>
<table>
<thead>
<tr>
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/etc/apt/apt.conf.d/52unattended-upgrades-pve</code></td>
<td>local policy — origins allowlist + email + reboot policy</td>
</tr>
<tr>
<td><code>/etc/apt/apt.conf.d/20auto-upgrades</code></td>
<td>enables the daily update-list + unattended-upgrade run</td>
</tr>
</tbody>
</table>
<p><strong>Auto-applied:</strong>
- <code>origin=Debian,codename=trixie,label=Debian</code> (stable main)
- <code>origin=Debian,codename=trixie-security,label=Debian-Security</code>
- <code>origin=Debian,codename=trixie-updates</code> (stable point updates)</p>
<p><strong>Held for manual <code>apt full-upgrade</code></strong> (intentionally — review release notes first):
- <code>origin=Proxmox,...</code> — pve-manager, kernels, qemu-server, all PVE components</p>
<p><strong>Settings:</strong>
- <code>Automatic-Reboot "false"</code> — kernel updates require a manual reboot
- <code>Remove-Unused-Dependencies "true"</code> — autoremove orphans after upgrades
- <code>AutoFixInterruptedDpkg "true"</code> — resume after crash mid-upgrade
- <code>Mail "notify@home.box"</code>, <code>MailReport "on-change"</code> — alerts on actual changes</p>
<p><strong>Triggered by:</strong>
- <code>apt-daily.timer</code> (daily ~07:00) — refresh package lists
- <code>apt-daily-upgrade.timer</code> (daily ~06:00) — apply unattended upgrades</p>
<p><strong>Caveat: mail delivery isn't reaching you yet.</strong> Postfix is up but has <code>relayhost = (none)</code> — change notifications get delivered locally to <code>/var/mail/notify</code> on the host, not to your inbox. Set up a smart-host relay (Gmail/Postmark/etc.) if you want the mails to actually land. Until then, check <code>/var/log/unattended-upgrades/unattended-upgrades.log</code> for history.</p>
<p><strong>Verify any time:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a>unattended-upgrade<span class="w"> </span>--dry-run<span class="w"> </span>--debug<span class="w"> </span><span class="m">2</span>&gt;<span class="p">&amp;</span><span class="m">1</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-E<span class="w"> </span><span class="s2">&quot;Allowed origins|would be upgraded|pkgs that look&quot;</span>
<a id="__codelineno-9-2" name="__codelineno-9-2" href="#__codelineno-9-2"></a>systemctl<span class="w"> </span>list-timers<span class="w"> </span>apt-daily-upgrade.timer
<a id="__codelineno-9-3" name="__codelineno-9-3" href="#__codelineno-9-3"></a>tail<span class="w"> </span>/var/log/unattended-upgrades/unattended-upgrades.log
</code></pre></div></p>
<hr />
<h2 id="9-networking">9. Networking<a class="headerlink" href="#9-networking" title="Permanent link">&para;</a></h2>
<ul>
<li>Single bridge <code>vmbr0</code> on <code>enp86s0</code> — no VLAN aware (<code>bridge-vlan-aware yes</code>). If you ever want to segment guests by VLAN, add it now (no impact on existing guests as long as you don't tag them):
<div class="highlight"><pre><span></span><code><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a>bridge-vlan-aware yes
<a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a>bridge-vids 2-4094
</code></pre></div></li>
<li><code>net.core.rmem_max</code> / <code>wmem_max</code> are at distro defaults (208 KiB). With a 1 GbE NIC the impact is small (link is already saturated at NFS rsize=1M), but with future 2.5/10 GbE bump to 16 MiB:
<div class="highlight"><pre><span></span><code><a id="__codelineno-11-1" name="__codelineno-11-1" href="#__codelineno-11-1"></a>cat<span class="w"> </span>&gt;/etc/sysctl.d/99-net.conf<span class="w"> </span><span class="s">&lt;&lt;&#39;EOF&#39;</span>
<a id="__codelineno-11-2" name="__codelineno-11-2" href="#__codelineno-11-2"></a><span class="s">net.core.rmem_max=16777216</span>
<a id="__codelineno-11-3" name="__codelineno-11-3" href="#__codelineno-11-3"></a><span class="s">net.core.wmem_max=16777216</span>
<a id="__codelineno-11-4" name="__codelineno-11-4" href="#__codelineno-11-4"></a><span class="s">net.ipv4.tcp_rmem=4096 87380 16777216</span>
<a id="__codelineno-11-5" name="__codelineno-11-5" href="#__codelineno-11-5"></a><span class="s">net.ipv4.tcp_wmem=4096 65536 16777216</span>
<a id="__codelineno-11-6" name="__codelineno-11-6" href="#__codelineno-11-6"></a><span class="s">EOF</span>
<a id="__codelineno-11-7" name="__codelineno-11-7" href="#__codelineno-11-7"></a>sysctl<span class="w"> </span>--system
</code></pre></div></li>
<li>TCP congestion control is <code>cubic</code>. <code>bbr</code> is generally better for mixed workloads — change only if you measure a problem.</li>
<li><code>wlo1</code> is present but unused — confirm and disable in BIOS or <code>iface wlo1 inet manual</code> (already done). No action.</li>
</ul>
<hr />
<h2 id="10-container-specific-issues">10. Container-specific issues<a class="headerlink" href="#10-container-specific-issues" title="Permanent link">&para;</a></h2>
<h3 id="ct-104-docker-ai-image-gen-48-gib-cap-16-cores-gpu-passthrough">CT 104 (docker / AI / image-gen) — 48 GiB cap, 16 cores, GPU passthrough<a class="headerlink" href="#ct-104-docker-ai-image-gen-48-gib-cap-16-cores-gpu-passthrough" title="Permanent link">&para;</a></h3>
<p>Measured: 18.8 GiB current, peak 29.3 GiB, <strong>9.3 GiB in swap</strong>, load 8.0, ~65 Docker containers (Immich + ML, ComfyUI, LobeChat, n8n, Daytona, LiteLLM, Vaultwarden, Paperless+AI, many MCP servers, Speaches-OpenVINO).</p>
<ul>
<li><code>cores: 16</code> is justified by the workload (load avg 8 across 16 = ~50 % avg). Don't drop.</li>
<li>128 GiB cap is generous but currently 9 GiB is in swap — this is <strong>memory pressure from CT 101's phantom cap</strong>, not from CT 104's own needs. Fix CT 101 first, re-measure, then either lower CT 104 to 48 GiB or leave it.</li>
<li>GPU pass-through (<code>/dev/dri/card1</code>, <code>renderD128</code>) confirmed visible inside CT and being used by Speaches via OpenVINO ✔</li>
<li>Keep <code>recordsize=128K</code> (large files dominate).</li>
<li><code>swap: 32000</code> is high — consider <code>swap: 8192</code>. Heavy CT swap-out on a QLC root SSD adds write amplification.</li>
<li>Bind-mounted <code>/mnt/pve/unas</code> (NFS) is the right choice ✔</li>
<li>Bonus: raise scheduler weight if image-gen latency matters: <code>pct set 104 -cpuunits 200</code>.</li>
</ul>
<h3 id="ct-101-shepard-docker-currently-160-gib-cap-peak-156-gib">CT 101 (shepard / docker) — currently 160 GiB cap, peak 15.6 GiB<a class="headerlink" href="#ct-101-shepard-docker-currently-160-gib-cap-peak-156-gib" title="Permanent link">&para;</a></h3>
<p>Workload: ~9 containers — Shepard frontend/backend, Keycloak, Neo4j, MongoDB, MongoExpress, TimescaleDB, Caddy, home-showcase-collector.</p>
<ul>
<li><strong>Drop memory cap to 32 GiB</strong>: <code>pct set 101 -memory 32768</code>. No restart needed.</li>
<li><code>cores: 12</code> is fine (load 8.5 — close to fully loaded, real work).</li>
<li><code>swap: 8192</code></li>
<li>This single change unblocks everything else.</li>
</ul>
<h3 id="ct-102-adguard-dns-undersized">CT 102 (adguard / DNS) — undersized<a class="headerlink" href="#ct-102-adguard-dns-undersized" title="Permanent link">&para;</a></h3>
<p>Measured: 343 MiB used at the <strong>512 MiB cap</strong>, AdGuardHome alone is 350 MiB RSS, the CT is one OOM event from killing the LAN's DNS.</p>
<ul>
<li><code>pct set 102 -memory 1024</code> (bump to 1 GiB).</li>
<li>Add <code>onboot: 1</code> (already set) + <code>startup: order=1</code> (boots first) + <code>protection: 1</code> (anti-fatfinger). This is the only DNS — treat it like infrastructure.</li>
</ul>
<h3 id="ct-103-backrest-fine">CT 103 (backrest) — fine<a class="headerlink" href="#ct-103-backrest-fine" title="Permanent link">&para;</a></h3>
<p>89 MiB used, peak 305 MiB. No changes needed.</p>
<h3 id="ct-105-nextcloud-privileged-cifs">CT 105 (nextcloud) — privileged + CIFS<a class="headerlink" href="#ct-105-nextcloud-privileged-cifs" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>Privileged container</strong> (no <code>unprivileged: 1</code>). For a public-facing app this is the wrong tradeoff. Migration: stop CT, <code>vzdump</code> backup, restore as unprivileged. Be ready to fix file ownership on the bind-mount afterwards (privileged UID 33 → unprivileged needs <code>lxc.idmap</code>).</li>
<li><strong>Uses CIFS bind-mount to UNAS</strong> with <code>actimeo=1</code> — see §4. Switch to NFS bind-mount, after confirming UID mapping (CIFS forces uid=33; NFS uses host UIDs as-is).</li>
<li>Memory cap 8 GiB, peak 3.5 GiB — fine as-is.</li>
<li>Runs Nextcloud AIO stack (mastercontainer-managed: apache, postgres, redis, imaginary, collabora, notify-push).</li>
</ul>
<h3 id="ct-108-zoraxy-slight-oversize">CT 108 (zoraxy) — slight oversize<a class="headerlink" href="#ct-108-zoraxy-slight-oversize" title="Permanent link">&para;</a></h3>
<p>Peak 463 MiB on a 2 GiB cap. Lower to 1 GiB if desired (cosmetic).</p>
<h3 id="ct-104-docker-stacks-inventory-optstacks">CT 104 — Docker stacks inventory (<code>/opt/stacks</code>)<a class="headerlink" href="#ct-104-docker-stacks-inventory-optstacks" title="Permanent link">&para;</a></h3>
<p>CT 104 keeps its Docker workloads in a <strong>git-tracked monorepo at <code>/opt/stacks/</code></strong> with one directory per stack, plus meta-docs (<code>PORTMAP.md</code>, <code>storage.md</code>, <code>volumes.md</code>, <code>docker-networks.md</code>, <code>todo.md</code>). Good practice — this is how to keep ~65 containers manageable. The other CTs (101, 105) don't have <code>/opt/stacks</code> — their compose files live elsewhere.</p>
<p><strong>Stack list (29 dirs):</strong></p>
<table>
<thead>
<tr>
<th>Stack</th>
<th>Status</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ai/</code></td>
<td><strong>active</strong> — large subtree</td>
<td>comfyui, lobehub, litellm, speaches, mcp-gateway, mcp-servers (many MCP yml files), searxng. Custom <code>syncstack.py</code> to manage cross-file project names.</td>
</tr>
<tr>
<td><code>arr-stack/</code></td>
<td>dormant (defined, not running)</td>
<td>rdtclient, prowlarr, audiobookshelf, shelfarr, flaresolverr</td>
</tr>
<tr>
<td><code>arcane/</code></td>
<td>dormant</td>
<td>Docker dashboard</td>
</tr>
<tr>
<td><code>daytona/</code></td>
<td>active (as <code>daytona-minimal</code>)</td>
<td>dev environments + runner + registry</td>
</tr>
<tr>
<td><code>dozzle/</code></td>
<td>dormant</td>
<td>container log viewer</td>
</tr>
<tr>
<td><code>gotify/</code></td>
<td>active</td>
<td>push notifications</td>
</tr>
<tr>
<td><code>homepage/</code></td>
<td>dormant</td>
<td>dashboard</td>
</tr>
<tr>
<td><code>immich/</code></td>
<td>active (5 containers)</td>
<td>photo platform + ML</td>
</tr>
<tr>
<td><code>karakeep/</code></td>
<td>active (3 containers)</td>
<td>bookmark mgr + chrome + meilisearch</td>
</tr>
<tr>
<td><code>memos/</code></td>
<td>active</td>
<td>notes</td>
</tr>
<tr>
<td><code>n8n/</code></td>
<td>active</td>
<td>pinned <code>2.20.11</code> (good — there's an explicit version-drift comment in the compose)</td>
</tr>
<tr>
<td><code>nexa/</code></td>
<td>dormant</td>
<td>(?)</td>
</tr>
<tr>
<td><code>paperless_ai/</code>, <code>paperless-ngx/</code></td>
<td>active (5 containers between them)</td>
<td>OCR pipeline</td>
</tr>
<tr>
<td><code>pocketid/</code></td>
<td>active</td>
<td>OIDC provider</td>
</tr>
<tr>
<td><code>proxy/</code></td>
<td>dormant</td>
<td>(?)</td>
</tr>
<tr>
<td><code>qdrant/</code></td>
<td>dormant</td>
<td>vector DB</td>
</tr>
<tr>
<td><code>shared-db/</code></td>
<td>active</td>
<td>shared-postgres + garage (S3-compatible) + pgadmin (defined)</td>
</tr>
<tr>
<td><code>streamio/</code></td>
<td>dormant</td>
<td>media</td>
</tr>
<tr>
<td><code>traccar/</code></td>
<td>active</td>
<td>GPS tracker</td>
</tr>
<tr>
<td><code>vaultwarden/</code></td>
<td>active</td>
<td>password mgr</td>
</tr>
<tr>
<td><code>vpn/</code></td>
<td>dormant</td>
<td>gluetun (intended VPN egress wrapper?)</td>
</tr>
<tr>
<td><code>backups/</code>, <code>docs/</code>, <code>scripts/</code></td>
<td>meta dirs (no compose)</td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Observations:</strong></p>
<ol>
<li><strong>~10 stacks are defined but dormant.</strong> No RAM/CPU cost while down, but their images sit on disk and the git repo accrues dead code. Either run them, document why they're parked, or <code>git rm</code> them — repo drift is the silent killer of "I know what's running" confidence.</li>
<li><strong>Stack name ≠ compose project name</strong> for the <code>ai/</code> and <code>daytona/</code> trees (multiple compose files per dir, different project names). The <code>syncstack.py</code> helper exists for this; just be aware that <code>docker compose -f</code> lookups by directory name don't match.</li>
<li><strong>Disk-reclaim potential (measured <code>docker system df</code>):</strong></li>
</ol>
<table>
<thead>
<tr>
<th>Asset</th>
<th>Total</th>
<th>Reclaimable</th>
</tr>
</thead>
<tbody>
<tr>
<td>Images</td>
<td>83 / 61.6 GB</td>
<td><strong>8.3 GB</strong></td>
</tr>
<tr>
<td>Build cache</td>
<td>105 entries / 9.0 GB</td>
<td><strong>4.6 GB</strong></td>
</tr>
<tr>
<td>Volumes</td>
<td>31 / 3.0 GB</td>
<td>940 MB (21 dangling)</td>
</tr>
<tr>
<td>Containers</td>
<td>60 active</td>
<td>0</td>
</tr>
</tbody>
</table>
<p><div class="highlight"><pre><span></span><code><a id="__codelineno-12-1" name="__codelineno-12-1" href="#__codelineno-12-1"></a>pct<span class="w"> </span><span class="nb">exec</span><span class="w"> </span><span class="m">104</span><span class="w"> </span>--<span class="w"> </span>docker<span class="w"> </span>system<span class="w"> </span>prune<span class="w"> </span>-a<span class="w"> </span>--volumes
<a id="__codelineno-12-2" name="__codelineno-12-2" href="#__codelineno-12-2"></a><span class="c1"># or non-destructively just the build cache:</span>
<a id="__codelineno-12-3" name="__codelineno-12-3" href="#__codelineno-12-3"></a>pct<span class="w"> </span><span class="nb">exec</span><span class="w"> </span><span class="m">104</span><span class="w"> </span>--<span class="w"> </span>docker<span class="w"> </span>builder<span class="w"> </span>prune<span class="w"> </span>-a
</code></pre></div>
~13 GB to recover. On a 200 GB rootfs that's 64 % full, this is meaningful.</p>
<ol>
<li><strong>Stack→Zoraxy mapping (§11b):</strong> when fronting via Zoraxy, the canonical service endpoints (per <code>PORTMAP.md</code>) are CT 104's IP <code>192.168.1.40</code> + port. Worth cross-referencing that file when setting up reverse-proxy entries.</li>
</ol>
<h3 id="general-lxc-hygiene">General LXC hygiene<a class="headerlink" href="#general-lxc-hygiene" title="Permanent link">&para;</a></h3>
<ul>
<li>Strip the giant HTML banners from the top of every <code>/etc/pve/lxc/*.conf</code> — pure noise in <code>pct config</code>.</li>
<li>All CTs except 105 are unprivileged ✔ with <code>nesting=1,keyctl=1</code> (appropriate for Docker).</li>
</ul>
<h3 id="anti-fat-finger-protection-protection-1-boot-order">Anti-fat-finger protection (<code>protection: 1</code>) + boot order<a class="headerlink" href="#anti-fat-finger-protection-protection-1-boot-order" title="Permanent link">&para;</a></h3>
<p>Applied across all critical guests (2026-05-20). <code>protection: 1</code> blocks <code>pct destroy</code> / "Remove" from the UI until manually unset — cheap insurance against the wrong-CT-deleted incident.</p>
<table>
<thead>
<tr>
<th>Guest</th>
<th>protection</th>
<th>startup order</th>
<th>Rationale</th>
</tr>
</thead>
<tbody>
<tr>
<td>CT 102 (AdGuard / DNS)</td>
<td></td>
<td><strong>order=1</strong> (boots first)</td>
<td>LAN-wide DNS — nothing resolves until this is up</td>
</tr>
<tr>
<td>CT 108 (Zoraxy / LE proxy)</td>
<td></td>
<td><strong>order=2</strong></td>
<td>Public-facing reverse proxy + LE; depends on DNS</td>
</tr>
<tr>
<td>CT 103 (Backrest)</td>
<td></td>
<td>default</td>
<td>Holds backup config and snapshot metadata</td>
</tr>
<tr>
<td>CT 104 (Docker / AI / image-gen)</td>
<td></td>
<td>default</td>
<td>Largest data footprint (200 G rootfs); 65 containers</td>
</tr>
<tr>
<td>CT 105 (Nextcloud)</td>
<td></td>
<td>default</td>
<td>User data</td>
</tr>
<tr>
<td>VM 100 (HAOS)</td>
<td></td>
<td>default</td>
<td>Home automation state</td>
</tr>
<tr>
<td>CT 101 (shepard)</td>
<td>❌ (left optional)</td>
<td>default</td>
<td>Currently a dev/iteration target; protect once stabilized: <code>pct set 101 -protection 1</code></td>
</tr>
</tbody>
</table>
<p>The <code>onboot: 1</code> flag was already set on all guests ✔ — they all auto-start on host reboot. The two <code>startup</code> ordered ones now also boot in the right sequence: DNS → Zoraxy → everything else in parallel.</p>
<p>To remove protection on a guest later: <code>pct set &lt;id&gt; -protection 0</code> (or <code>qm set 100 -protection 0</code>).</p>
<hr />
<h2 id="11a-dns-adguard-rewrites-site-wide-consistency">11a. DNS — AdGuard rewrites &amp; site-wide consistency<a class="headerlink" href="#11a-dns-adguard-rewrites-site-wide-consistency" title="Permanent link">&para;</a></h2>
<p><strong>Current state (measured):</strong></p>
<ul>
<li>AdGuard Home runs on CT 102 (<code>192.168.1.2</code>, hostname <code>dns</code>) — DNS on <code>0.0.0.0:53/tcp+udp</code>, admin UI on <code>:80</code>.</li>
<li>Upstream stack is well-configured: load-balanced mix of Telekom-DoT, NextDNS, DNS4EU, Quad9, Cloudflare, Google (DoH/DoQ where supported), bootstrap+fallback on Cloudflare.</li>
<li>All 6 LXCs resolve via <code>nameserver 192.168.1.2</code></li>
<li>Proxmox host (<code>192.168.1.20</code>) resolves via <code>192.168.1.2</code> ✔ — but its <code>search</code> domain is <code>box</code> (probably an install-time leftover; AdGuard's <code>local_domain_name</code> is <code>lan</code>).</li>
<li>HAOS VM is on <code>192.168.1.60</code> — DNS setting unknown without Home Assistant access; <strong>verify</strong>.</li>
<li><code>rewrites: []</code> and <strong><code>rewrites_enabled: false</code></strong> — no internal name resolution is happening today.</li>
</ul>
<h3 id="why-this-matters">Why this matters<a class="headerlink" href="#why-this-matters" title="Permanent link">&para;</a></h3>
<p>Without rewrites, you address everything by IP. That's brittle (IP changes break links), invisible in logs, and prevents nice tricks like split-horizon DNS for <code>nuclide.systems</code> (so the same name resolves to Zoraxy LAN-internally without going through your public IP / WAN hairpin).</p>
<h3 id="recommended-rewrite-set">Recommended rewrite set<a class="headerlink" href="#recommended-rewrite-set" title="Permanent link">&para;</a></h3>
<p>In AdGuard UI → Filters → DNS rewrites, <strong>enable rewrites</strong> and add:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-13-1" name="__codelineno-13-1" href="#__codelineno-13-1"></a># Split-horizon: public domain → Zoraxy on LAN
<a id="__codelineno-13-2" name="__codelineno-13-2" href="#__codelineno-13-2"></a>nuclide.systems → 192.168.1.4
<a id="__codelineno-13-3" name="__codelineno-13-3" href="#__codelineno-13-3"></a>*.nuclide.systems → 192.168.1.4
<a id="__codelineno-13-4" name="__codelineno-13-4" href="#__codelineno-13-4"></a>
<a id="__codelineno-13-5" name="__codelineno-13-5" href="#__codelineno-13-5"></a># Service-name shortcuts under the local_domain_name (`.lan`)
<a id="__codelineno-13-6" name="__codelineno-13-6" href="#__codelineno-13-6"></a>pve.lan → 192.168.1.20 # Proxmox UI
<a id="__codelineno-13-7" name="__codelineno-13-7" href="#__codelineno-13-7"></a>nuc.lan → 192.168.1.20 # host shorthand
<a id="__codelineno-13-8" name="__codelineno-13-8" href="#__codelineno-13-8"></a>dns.lan → 192.168.1.2 # AdGuard itself
<a id="__codelineno-13-9" name="__codelineno-13-9" href="#__codelineno-13-9"></a>zoraxy.lan → 192.168.1.4 # reverse proxy
<a id="__codelineno-13-10" name="__codelineno-13-10" href="#__codelineno-13-10"></a>shepard.lan → 192.168.1.49 # CT 101
<a id="__codelineno-13-11" name="__codelineno-13-11" href="#__codelineno-13-11"></a>docker.lan → 192.168.1.40 # CT 104
<a id="__codelineno-13-12" name="__codelineno-13-12" href="#__codelineno-13-12"></a>nextcloud.lan → 192.168.1.41 # CT 105
<a id="__codelineno-13-13" name="__codelineno-13-13" href="#__codelineno-13-13"></a>haos.lan → 192.168.1.60 # VM 100
<a id="__codelineno-13-14" name="__codelineno-13-14" href="#__codelineno-13-14"></a>unas.lan → 192.168.1.31 # NAS
<a id="__codelineno-13-15" name="__codelineno-13-15" href="#__codelineno-13-15"></a>router.lan → 192.168.1.1 # UniFi gateway
</code></pre></div>
<p>The split-horizon entries are the highest-value: once Zoraxy proxies <code>pve.nuclide.systems</code> (see §11b), the same URL works both from the public internet and from inside the LAN — with no NAT-loopback weirdness and with the LAN traffic never leaving the building.</p>
<p><strong>Edit the YAML directly if preferred</strong> (<code>/opt/AdGuardHome/AdGuardHome.yaml</code> inside CT 102), then restart AdGuard. The line <code>rewrites_enabled: false</code> must flip to <code>true</code>.</p>
<h3 id="verify-clients-are-actually-using-adguard">Verify clients are actually using AdGuard<a class="headerlink" href="#verify-clients-are-actually-using-adguard" title="Permanent link">&para;</a></h3>
<p>After rewrites are in, walk the inventory:</p>
<table>
<thead>
<tr>
<th>Client</th>
<th>Should use DNS</th>
<th>Check</th>
</tr>
</thead>
<tbody>
<tr>
<td>All 6 LXCs</td>
<td>✔ already at .2</td>
<td><code>pct exec &lt;id&gt; -- cat /etc/resolv.conf</code></td>
</tr>
<tr>
<td>Proxmox host</td>
<td>✔ already at .2</td>
<td><code>cat /etc/resolv.conf</code></td>
</tr>
<tr>
<td>HAOS VM (192.168.1.60)</td>
<td>unknown</td>
<td>HAOS UI → Settings → System → Network → check DNS servers; should be <code>192.168.1.2</code></td>
</tr>
<tr>
<td>Router (192.168.1.1, UniFi)</td>
<td><strong>DHCP-hands-out DNS to clients</strong> — must serve .2 as primary</td>
<td>UniFi: Settings → Networks → LAN → DHCP DNS: <code>192.168.1.2</code></td>
</tr>
<tr>
<td>IoT devices (Roborock at .64, others)</td>
<td>inherit via DHCP from router</td>
<td>once UniFi DHCP serves .2, every device that DHCP-renews picks it up. Force-renew or reboot stragglers.</td>
</tr>
<tr>
<td>Anything with hard-coded <code>1.1.1.1</code> / <code>8.8.8.8</code></td>
<td>bypassing the filter</td>
<td>grep service configs for upstream DNS — apps like Pi-hole-aware clients, some Smart TVs, Chromecasts</td>
</tr>
</tbody>
</table>
<h3 id="optional-hardening-once-the-rewrites-are-stable">Optional hardening once the rewrites are stable<a class="headerlink" href="#optional-hardening-once-the-rewrites-are-stable" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>DNSSEC</strong>: <code>enable_dnssec: true</code> (currently false). Most upstreams already validate, but flipping this on adds end-to-end checking.</li>
<li><strong>Block DoH bypass</strong>: AdGuard has a "Blocked services" preset for Cloudflare/Google DoH endpoints — useful so clients can't sidestep the filter.</li>
<li><strong>Resolve <code>box</code> search-domain confusion</strong>: edit <code>/etc/resolv.conf</code> (or set it via <code>/etc/network/interfaces</code>) on the host to <code>search lan</code> so it matches AdGuard's <code>local_domain_name</code>.</li>
<li><strong>Secondary DNS for resilience</strong>: CT 102 is currently the sole DNS — if it crashes, the LAN goes blind. Either add <code>protection: 1</code> (done above) and rely on it, or stand up a tiny secondary AdGuard on a different CT and configure UniFi DHCP to hand out both. (Out of scope for low-hanging fruit, but worth knowing.)</li>
<li><strong>Conditional forwarding for UNAS</strong>: if UNAS announces hostnames (mDNS or its own DNS), point AdGuard at it for the relevant zone via "Upstream DNS servers" using the <code>[/zone/]upstream</code> syntax.</li>
</ul>
<h3 id="action-checklist">Action checklist<a class="headerlink" href="#action-checklist" title="Permanent link">&para;</a></h3>
<ol>
<li>AdGuard UI → Filters → DNS rewrites: paste the table above.</li>
<li>AdGuard UI → Settings → DNS settings → <strong>enable</strong> "DNS rewrites".</li>
<li>UniFi: confirm DHCP option 6 = <code>192.168.1.2</code> (LAN clients get AdGuard).</li>
<li>HAOS: confirm Home Assistant has <code>192.168.1.2</code> set as DNS.</li>
<li>Force-renew DHCP leases on key clients (or just wait — most renew within 24 h).</li>
<li>After §11b is done, the public <code>pve.nuclide.systems</code> resolves to <code>.4</code> from inside the LAN automatically.</li>
</ol>
<hr />
<h2 id="11b-tls-certificates-for-the-proxmox-web-ui">11b. TLS certificates for the Proxmox web UI<a class="headerlink" href="#11b-tls-certificates-for-the-proxmox-web-ui" title="Permanent link">&para;</a></h2>
<p><strong>Current state:</strong> the PVE web UI on <code>https://192.168.1.20:8006</code> uses the self-signed certificate generated at install (<code>/etc/pve/local/pveproxy-ssl.pem</code> is absent → falls back to <code>pve-ssl.pem</code>). Every login throws a browser warning.</p>
<p><strong>The wider setup:</strong> Zoraxy (CT 108, <code>192.168.1.4</code>) already handles Let's Encrypt for <code>nuclide.systems</code> (the public domain for this host). So there are three sane options; pick <strong>A</strong> unless you have a reason not to.</p>
<h3 id="option-a-reverse-proxy-pve-through-zoraxy-recommended">Option A — Reverse-proxy PVE through Zoraxy (recommended)<a class="headerlink" href="#option-a-reverse-proxy-pve-through-zoraxy-recommended" title="Permanent link">&para;</a></h3>
<p>Pros: single source of LE truth (Zoraxy already renews); no DNS-plugin setup; no exposing the API; nice domain like <code>pve.nuclide.systems</code>. Cons: depends on Zoraxy being up (keep IP:8006 as fallback); needs WebSocket pass-through for the noVNC console and <code>xterm.js</code> shell.</p>
<ol>
<li><strong>Zoraxy host entry</strong></li>
<li>Domain: <code>pve.nuclide.systems</code> (or whatever subdomain)</li>
<li>Target: <code>https://192.168.1.20:8006</code></li>
<li><strong>Enable WebSocket support</strong> (mandatory — noVNC, xterm.js, task log streaming all use it)</li>
<li><strong>Skip backend TLS verification</strong> (PVE's cert is self-signed)</li>
<li>Enable HSTS once you've confirmed the setup works</li>
<li>Optionally restrict by source: only LAN + Cloudflare/Tailscale IPs</li>
<li><strong>DNS</strong>: add an A record <code>pve.nuclide.systems</code> → public IP (or split-horizon to <code>192.168.1.20</code> for LAN). Zoraxy will ACME-challenge via whichever method it's configured for (HTTP-01 or DNS-01).</li>
<li><strong>Keep <code>https://192.168.1.20:8006</code> reachable on LAN</strong> as an emergency fallback. Don't disable it.</li>
<li>Set the PVE redirect-to-HTTPS rules in Zoraxy on for both <code>:80</code> and <code>:443</code>.</li>
</ol>
<p><strong>Important caveat:</strong> the PVE Mobile app and the <code>pvesh</code> / API clients may not love going through a reverse proxy (they're picky about TLS SNI and cookie domains). Keep direct IP access available for API tooling, or test thoroughly.</p>
<h3 id="option-b-pves-built-in-acme-with-dns-01">Option B — PVE's built-in ACME with DNS-01<a class="headerlink" href="#option-b-pves-built-in-acme-with-dns-01" title="Permanent link">&para;</a></h3>
<p>Pros: no reverse proxy in the path; PVE renews itself; works for the API too. Cons: requires a DNS provider plugin (your registrar's API token), and an LE-acceptable FQDN that resolves publicly.</p>
<ol>
<li>Register an ACME account:
<div class="highlight"><pre><span></span><code><a id="__codelineno-14-1" name="__codelineno-14-1" href="#__codelineno-14-1"></a>pvenode<span class="w"> </span>acme<span class="w"> </span>account<span class="w"> </span>register<span class="w"> </span>default<span class="w"> </span>you@nuclide.systems
</code></pre></div></li>
<li>Configure a DNS plugin. PVE supports <code>acme-dns</code>, <code>cloudflare</code>, <code>route53</code>, <code>desec</code>, etc. via the <code>acme.sh</code> plugin set. Example for Cloudflare:
<div class="highlight"><pre><span></span><code><a id="__codelineno-15-1" name="__codelineno-15-1" href="#__codelineno-15-1"></a>pvenode<span class="w"> </span>acme<span class="w"> </span>plugin<span class="w"> </span>add<span class="w"> </span>dns<span class="w"> </span>cf<span class="w"> </span>--api<span class="w"> </span>cf<span class="w"> </span>--data<span class="w"> </span><span class="nv">CF_Token</span><span class="o">=</span>XXXXXXXX
</code></pre></div>
Replace <code>cf</code> plugin name to match whichever registrar you use for <code>nuclide.systems</code>.</li>
<li>Tell the node which domain(s) and how:
<div class="highlight"><pre><span></span><code><a id="__codelineno-16-1" name="__codelineno-16-1" href="#__codelineno-16-1"></a>pvenode<span class="w"> </span>config<span class="w"> </span><span class="nb">set</span><span class="w"> </span>--acme<span class="w"> </span><span class="nv">domains</span><span class="o">=</span>nuc.nuclide.systems
<a id="__codelineno-16-2" name="__codelineno-16-2" href="#__codelineno-16-2"></a>pvenode<span class="w"> </span>config<span class="w"> </span><span class="nb">set</span><span class="w"> </span>--acmedomain0<span class="w"> </span><span class="nv">domain</span><span class="o">=</span>nuc.nuclide.systems,plugin<span class="o">=</span>cf
</code></pre></div></li>
<li>Order:
<div class="highlight"><pre><span></span><code><a id="__codelineno-17-1" name="__codelineno-17-1" href="#__codelineno-17-1"></a>pvenode<span class="w"> </span>acme<span class="w"> </span>cert<span class="w"> </span>order
</code></pre></div>
PVE drops the cert at <code>/etc/pve/nodes/nuc/pveproxy-ssl.pem</code> and renews ~30 days before expiry via the <code>pve-daily-update</code> timer.</li>
</ol>
<h3 id="option-c-push-zoraxys-cert-into-pve">Option C — Push Zoraxy's cert into PVE<a class="headerlink" href="#option-c-push-zoraxys-cert-into-pve" title="Permanent link">&para;</a></h3>
<p>Only useful if A and B are off the table. Zoraxy stores its issued certs (location varies by Zoraxy version — typically under its data dir, e.g. <code>/opt/zoraxy/conf/certs/</code>). Cron a script that copies the active cert/key and concatenates them as <code>/etc/pve/local/pveproxy-ssl.pem</code> (cert + chain) and <code>/etc/pve/local/pveproxy-ssl.key</code>, then <code>systemctl reload pveproxy</code>. Brittle — only worth it if you must.</p>
<h3 id="recommended-path">Recommended path<a class="headerlink" href="#recommended-path" title="Permanent link">&para;</a></h3>
<p>Do <strong>A</strong> (reverse proxy through Zoraxy) for the web UI. It piggybacks on existing renewal. The mobile-app/API edge cases are usually fine if Zoraxy passes the WebSocket and preserves the <code>Host</code> header. If you later need full ACME on the node itself (e.g. you want valid TLS for <code>pvesh</code> and the API at the node FQDN too), layer <strong>B</strong> on top — they don't conflict.</p>
<h3 id="other-cts-with-web-uis-worth-fronting-via-zoraxy">Other CTs with web UIs worth fronting via Zoraxy<a class="headerlink" href="#other-cts-with-web-uis-worth-fronting-via-zoraxy" title="Permanent link">&para;</a></h3>
<p>While you're at it, route through Zoraxy for free LE:
- Backrest (CT 103) — currently IP-only
- AdGuard (CT 102) admin UI — <code>192.168.1.2:3000</code>
- Nextcloud (CT 105) — almost certainly already exposed; verify it terminates LE in Zoraxy and not internally
- Zoraxy itself (CT 108) — self-hosted, already TLS</p>
<p>For each, add a Zoraxy host entry, set a subdomain, and disable any local TLS / port-exposed listener that bypasses Zoraxy.</p>
<hr />
<h2 id="11-maintenance-observability">11. Maintenance / observability<a class="headerlink" href="#11-maintenance-observability" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Item</th>
<th>State</th>
<th>Recommend</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>lm-sensors</code></td>
<td>not installed</td>
<td><code>apt install lm-sensors &amp;&amp; sensors-detect --auto</code> for CPU/NVMe temps in the UI</td>
</tr>
<tr>
<td>Journal size</td>
<td>1.5 GiB</td>
<td>OK; cap at 1 GiB if you want predictability: <code>journalctl --vacuum-size=1G</code> and <code>SystemMaxUse=1G</code> in <code>journald.conf</code></td>
</tr>
<tr>
<td><code>fstrim.timer</code></td>
<td>active (weekly)</td>
<td>OK; <code>zfs trim</code> runs separately when autotrim=on</td>
</tr>
<tr>
<td>ZFS scrub</td>
<td>last run 2026-05-10, clean</td>
<td>default monthly timer is good</td>
</tr>
<tr>
<td>Subscription nag</td>
<td>not removed</td>
<td>If desired, <code>pve-no-nag</code> patch or the <code>proxmox-helper-scripts</code> line — purely cosmetic</td>
</tr>
<tr>
<td>Email alerts</td>
<td>(check <code>/etc/pve/user.cfg</code>)</td>
<td>configure <code>root@pam</code> email for failed scrub / failed backup notifications</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="13-update-management-current-model">13. Update management — current model<a class="headerlink" href="#13-update-management-current-model" title="Permanent link">&para;</a></h2>
<p>The host previously had <code>0 2 * * * apt-get update &amp;&amp; apt-get upgrade -y</code> (silently no-op'd on every PVE/kernel update) and a weekly <code>bash &lt;(wget tteck/.../update-lxcs-cron.sh)</code> cron that ran <code>dist-upgrade</code> across every LXC. Both removed 2026-05-20 and replaced with the structure below.</p>
<h3 id="layer-1-host-packages-debian-proxmox">Layer 1 — Host packages (Debian + Proxmox)<a class="headerlink" href="#layer-1-host-packages-debian-proxmox" title="Permanent link">&para;</a></h3>
<ul>
<li><code>unattended-upgrades 2.12</code> installed on the host.</li>
<li><code>/etc/apt/apt.conf.d/52unattended-upgrades-pve</code> allows only <code>Debian</code>, <code>Debian-Security</code>, <code>trixie-updates</code><strong>Proxmox origin held</strong> for manual review.</li>
<li>Triggered by <code>apt-daily.timer</code> and <code>apt-daily-upgrade.timer</code> (ship with <code>apt</code>, both <code>active enabled</code>).</li>
<li><code>Automatic-Reboot "false"</code> — kernel updates wait for a manual reboot.</li>
<li><code>Mail "notify@home.box"</code>, <code>MailReport "on-change"</code> — Postfix is up but <strong><code>relayhost = (none)</code></strong>, so mail is delivered locally to <code>/var/mail/notify</code> (not your inbox until you wire a smart-host).</li>
</ul>
<h3 id="layer-2-ct-os-packages-debian">Layer 2 — CT OS packages (Debian)<a class="headerlink" href="#layer-2-ct-os-packages-debian" title="Permanent link">&para;</a></h3>
<p><code>unattended-upgrades</code> deployed inside every CT (CT 104 already had it; 101/102/103/105/108/110 added 2026-05-20):</p>
<table>
<thead>
<tr>
<th>CT</th>
<th>u-u version</th>
<th>Distro</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>101</td>
<td>2.12</td>
<td>trixie</td>
<td>active</td>
</tr>
<tr>
<td>102</td>
<td>2.9.1</td>
<td>bookworm</td>
<td>active</td>
</tr>
<tr>
<td>103</td>
<td>2.9.1</td>
<td>bookworm</td>
<td>active</td>
</tr>
<tr>
<td>104</td>
<td>2.12</td>
<td>trixie</td>
<td>active</td>
</tr>
<tr>
<td>105</td>
<td>2.9.1</td>
<td>bookworm</td>
<td>active</td>
</tr>
<tr>
<td>108</td>
<td>2.12</td>
<td>trixie</td>
<td>active</td>
</tr>
<tr>
<td>110</td>
<td>2.12</td>
<td>trixie</td>
<td>active</td>
</tr>
</tbody>
</table>
<p>Per-CT allowlist is Debian-only — third-party repos (<code>docker.com</code>, <code>jotta.cloud</code>, <code>claude.ai</code>, <code>cli.github.com</code>, <code>dl.k6.io</code>) are <strong>excluded</strong> because they ship breaking changes outside Debian's freeze. Upgrade those with explicit <code>apt upgrade &lt;pkg&gt;</code>.</p>
<h3 id="layer-3-helper-script-app-binaries-adguard-zoraxy">Layer 3 — Helper-script app binaries (AdGuard, Zoraxy)<a class="headerlink" href="#layer-3-helper-script-app-binaries-adguard-zoraxy" title="Permanent link">&para;</a></h3>
<p>Each helper-scripts CT ships <code>/usr/bin/update</code> that re-<code>curl|bash</code>'s the community-scripts installer. Replaced with proper systemd timers using the apps' own update mechanisms:</p>
<table>
<thead>
<tr>
<th>CT</th>
<th>Timer</th>
<th>Schedule</th>
<th>Mechanism</th>
</tr>
</thead>
<tbody>
<tr>
<td>102 AdGuard</td>
<td><code>adguard-update.timer</code></td>
<td>Wed 03:30 (+15 m jitter)</td>
<td>native <code>AdGuardHome --update</code> flag</td>
</tr>
<tr>
<td>108 Zoraxy</td>
<td><code>zoraxy-update.timer</code></td>
<td>Wed 03:40 (+15 m jitter)</td>
<td>GitHub releases API, <strong>stable semver only (skips RCs)</strong>, binary swap + 30 s health check + auto-rollback</td>
</tr>
</tbody>
</table>
<p>Both log to <code>/var/log/{adguard,zoraxy}-update.log</code> and journal. Manual invoke: <code>systemctl start &lt;name&gt;-update.service</code>.</p>
<h3 id="layer-4-docker-engine-inside-cts">Layer 4 — Docker engine inside CTs<a class="headerlink" href="#layer-4-docker-engine-inside-cts" title="Permanent link">&para;</a></h3>
<p><code>docker-ce</code> updates in CT 101, 104, 105, 110 — held by the Debian-only allowlist. Apply with <code>apt upgrade docker-ce docker-ce-cli containerd.io</code> when you want them. Add <code>origin=Docker</code> to the allowlist if you want to auto-apply (not recommended; engine updates occasionally break running containers).</p>
<h3 id="layer-5-docker-images-the-75-containers">Layer 5 — Docker images (the ~75 containers)<a class="headerlink" href="#layer-5-docker-images-the-75-containers" title="Permanent link">&para;</a></h3>
<p><strong>Plan: deploy Diun on CT 109</strong> (ops LXC, see §16). Diun watches image tags on registries, posts to Gotify when a new image is available. Pulls remain manual (<code>docker compose pull &amp;&amp; up -d</code>) — protects against latest-tag drift like the n8n incident pinned in <code>/opt/stacks/n8n/docker-compose.yaml</code>.</p>
<p><strong>Layer 6 — Nextcloud-AIO</strong>: self-updates via the mastercontainer (CT 105). No external mechanism needed.</p>
<hr />
<h2 id="14-vm-100-haos-auto-restart-watchdog">14. VM 100 (HAOS) auto-restart watchdog<a class="headerlink" href="#14-vm-100-haos-auto-restart-watchdog" title="Permanent link">&para;</a></h2>
<p>Old approach: <code>*/5 * * * * /root/vm100.sh &gt; /dev/null</code> in cron. Script archived to <code>/root/vm100.sh.bak</code> on 2026-05-20.</p>
<p>Replaced with a systemd timer + oneshot:</p>
<ul>
<li><code>/usr/local/sbin/vm100-watchdog.sh</code> — only restarts on <code>status: stopped</code>; skips paused/prelaunch/transitional states; respects <code>/var/lock/qemu-server/lock-100.conf</code> so it doesn't race vzdump or migration</li>
<li><code>vm100-watchdog.service</code> (Type=oneshot)</li>
<li><code>vm100-watchdog.timer</code> (<code>OnUnitActiveSec=1min</code>, RandomizedDelaySec=15s)</li>
</ul>
<p>Recovery latency improved from 5 min → 1 min; logging structured in <code>journalctl -u vm100-watchdog</code>.</p>
<hr />
<h2 id="15-identity-pocket-id-on-its-own-ct">15. Identity — Pocket-ID on its own CT<a class="headerlink" href="#15-identity-pocket-id-on-its-own-ct" title="Permanent link">&para;</a></h2>
<h3 id="state-as-of-2026-05-20">State as of 2026-05-20<a class="headerlink" href="#state-as-of-2026-05-20" title="Permanent link">&para;</a></h3>
<p><strong>CT 110 "id"</strong> created at <code>192.168.1.5</code> as the dedicated IdP host. Pocket-ID was previously on CT 104 as one of ~65 docker containers; moved off because:</p>
<ul>
<li>Pocket-ID gates auth for everything else; sharing a host with image-gen workloads (9 GiB swap pressure on CT 104) means a single OOM/restart there knocks out SSO across the whole estate</li>
<li>Pattern match with CT 102 (DNS) and CT 108 (reverse proxy) — each infra-critical concern gets its own CT</li>
</ul>
<table>
<thead>
<tr>
<th>CT 110 setting</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Hostname / IP</td>
<td><code>id</code> / <code>192.168.1.5</code></td>
</tr>
<tr>
<td>Cores / RAM / rootfs</td>
<td>1 / 1 GB / 4 GB</td>
</tr>
<tr>
<td>Privilege</td>
<td>unprivileged, nesting=1, keyctl=1</td>
</tr>
<tr>
<td>Boot order</td>
<td><code>onboot=1</code>, <code>startup=order=3</code> (after DNS=1, Zoraxy=2)</td>
</tr>
<tr>
<td>Protection</td>
<td><code>protection: 1</code></td>
</tr>
<tr>
<td>Auto-updates</td>
<td>unattended-upgrades, Debian-only allowlist</td>
</tr>
<tr>
<td>Docker</td>
<td>29.5.1 + compose v5.1.3</td>
</tr>
</tbody>
</table>
<h3 id="duplication-procedure-used">Duplication procedure used<a class="headerlink" href="#duplication-procedure-used" title="Permanent link">&para;</a></h3>
<ol>
<li><code>sqlite3 pocket-id.db ".backup /tmp/pi-snap/pocket-id.db"</code> on CT 104 (online, no downtime to id.nuclide.systems)</li>
<li>tar everything except <code>*.db*</code>; restore the live snapshot as <code>pocket-id.db</code></li>
<li><code>pct pull</code><code>pct push</code> to CT 110</li>
<li>Adapted compose to drop the <code>shared_backend</code> external network reference (CT 110 uses default bridge)</li>
<li><code>docker compose up -d</code></li>
<li>Verified <code>http://192.168.1.5:11000/healthz</code> returns 200</li>
</ol>
<h3 id="zoraxy-cutover">Zoraxy cutover<a class="headerlink" href="#zoraxy-cutover" title="Permanent link">&para;</a></h3>
<p><code>id.nuclide.systems</code> upstream needs to change from <code>192.168.1.40:11000</code><code>192.168.1.5:11000</code>. Single-line config edit in Zoraxy + reload. Verified live in §11b once executed.</p>
<h3 id="secrets-rotation-list-deferred-to-cutover-day">Secrets-rotation list (deferred to cutover day)<a class="headerlink" href="#secrets-rotation-list-deferred-to-cutover-day" title="Permanent link">&para;</a></h3>
<ul>
<li><code>OIDC_CLIENT_SECRET</code> for the Arcane registration (exposed in chat transcript): rotate in Pocket-ID UI, update Arcane env, restart Arcane</li>
<li><code>ENCRYPTION_KEY</code> and <code>JWT_SECRET</code> in <code>/opt/stacks/arcane/docker-compose.yml</code>: move to <code>.env</code> (currently empty), regenerate, restart Arcane. Existing user sessions get invalidated — fine, ask everyone to log in again</li>
</ul>
<hr />
<h2 id="16-ct-109-ops-planned-observability-ops-lxc">16. CT 109 "ops" — planned observability + ops LXC<a class="headerlink" href="#16-ct-109-ops-planned-observability-ops-lxc" title="Permanent link">&para;</a></h2>
<p>Single LXC holding everything monitoring/ops-shaped. Sizing target: <strong>4 cores / 8 GiB RAM / 50 GiB rootfs</strong>, unprivileged, nesting=1. RAM bumped from 6 → 8 GiB to accommodate Loki. Disk bumped from 30 → 50 GiB for Loki log retention (30d) alongside Prometheus TSDB.</p>
<p>⚠️ <strong>IP conflict:</strong> inventory originally assigned <code>192.168.1.6</code> but CT 113 (db) took <code>.6</code> and CT 112 (secrets) took <code>.7</code>. CT 109 needs the next free infra IP — likely <code>.8</code> (verify against UniFi DHCP table before provisioning).</p>
<p><strong>Access model (initial):</strong> LAN-only. No Zoraxy routes until Tinyauth is deployed. Services reachable directly by IP.</p>
<h3 id="stack-to-deploy-on-ct-109">Stack to deploy on CT 109<a class="headerlink" href="#stack-to-deploy-on-ct-109" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Service</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>Prometheus</td>
<td>metrics TSDB, 30d retention</td>
</tr>
<tr>
<td>Loki</td>
<td>log aggregation backend — receives from Alloy agents on all hosts</td>
</tr>
<tr>
<td>Grafana</td>
<td>dashboards over Prometheus + Loki (unified metrics + log search)</td>
</tr>
<tr>
<td>Alertmanager + alertmanager-gotify-bridge</td>
<td>alert routing → Gotify</td>
</tr>
<tr>
<td>Arcane Manager</td>
<td>central docker management UI; edge agents on CT 101 + CT 104 + CT 110 (mTLS, agent-dialed-out)</td>
</tr>
<tr>
<td>Dozzle UI</td>
<td>live log tail (quick debugging); agents on CT 101 + CT 104 + CT 110. Complements Loki — Dozzle for live, Loki for historical/search</td>
</tr>
<tr>
<td><strong>Homarr</strong></td>
<td>unified dashboard, native Pocket-ID OIDC, Prometheus widget + Grafana iframe support</td>
</tr>
<tr>
<td>Diun</td>
<td>docker image update notifier → Gotify</td>
</tr>
<tr>
<td>Tinyauth</td>
<td>forward-auth gate for non-OIDC apps (Backrest, raw Dozzle, raw Prometheus, raw Grafana/Loki). OIDC client to Pocket-ID</td>
</tr>
<tr>
<td>docker-socket-proxy</td>
<td>local + remote (CT 101/104/110) — hardened read-only docker.sock for Homarr/Arcane discovery</td>
</tr>
<tr>
<td><strong>sshwifty</strong></td>
<td>web SSH client, multi-tab — multiple concurrent shells to different hosts (PVE, CT 104, CT 103, CT 111, etc.). LAN-only, port 8182. SSH key auth per host, no password prompt. Zoraxy + Tinyauth gate deferred.</td>
</tr>
<tr>
<td><strong>docs-server</strong></td>
<td>mkdocs Material site (<code>/docs</code> git repo → static HTML); migrating here from CT 111 where it currently runs at port 13080. LAN-only, no Zoraxy route.</td>
</tr>
</tbody>
</table>
<h3 id="sidecars-deployed-on-each-host">Sidecars deployed on each host<a class="headerlink" href="#sidecars-deployed-on-each-host" title="Permanent link">&para;</a></h3>
<p>Two agents per host — <strong>node-exporter</strong> (metrics) and <strong>Alloy</strong> (logs). Kept separate: node-exporter metric names are assumed by every Prometheus dashboard/alert; Alloy emitting compatible metrics adds validation risk for no gain.</p>
<table>
<thead>
<tr>
<th>Host</th>
<th>Sidecars</th>
</tr>
</thead>
<tbody>
<tr>
<td>PVE host</td>
<td>node-exporter, smartctl-exporter, pve-exporter, <strong>Alloy</strong> (journald → Loki: pve-manager, pveproxy, pvedaemon, LXC/VM lifecycle)</td>
</tr>
<tr>
<td>CT 101</td>
<td>node-exporter, cAdvisor, Dozzle agent, Arcane edge agent, docker-socket-proxy, <strong>Alloy</strong> (Docker logs + journald → Loki)</td>
</tr>
<tr>
<td>CT 103</td>
<td>node-exporter, <strong>Alloy</strong> (backrest.service journal + <code>/var/log/rclone-*.log</code> → Loki)</td>
</tr>
<tr>
<td>CT 104</td>
<td>node-exporter, cAdvisor, Dozzle agent, Arcane edge agent, docker-socket-proxy, intel_gpu_exporter, <strong>Alloy</strong> (Docker logs + journald → Loki)</td>
</tr>
<tr>
<td>CT 110</td>
<td>node-exporter, Dozzle agent, Arcane edge agent, <strong>Alloy</strong> (journald → Loki)</td>
</tr>
<tr>
<td>CT 111</td>
<td>node-exporter, intel_gpu_exporter, <strong>Alloy</strong> (journald + Coder/Gitea logs → Loki)</td>
</tr>
<tr>
<td>CT 113</td>
<td>node-exporter, postgres_exporter, <strong>Alloy</strong> (journald + postgres logs → Loki)</td>
</tr>
</tbody>
</table>
<h3 id="services-that-stay-where-they-are-not-on-ct-109">Services that stay where they are (NOT on CT 109)<a class="headerlink" href="#services-that-stay-where-they-are-not-on-ct-109" title="Permanent link">&para;</a></h3>
<ul>
<li>AdGuard (CT 102), Zoraxy (CT 108), Backrest (CT 103), Pocket-ID (CT 110) — each has its own CT for blast-radius reasons</li>
<li>All app stacks (Immich, Nextcloud, Shepard, n8n, ComfyUI, etc.)</li>
</ul>
<h3 id="migration-of-gotify-tier-b-schedule-when-ct-109-is-otherwise-stable">Migration of Gotify (Tier B — schedule when CT 109 is otherwise stable)<a class="headerlink" href="#migration-of-gotify-tier-b-schedule-when-ct-109-is-otherwise-stable" title="Permanent link">&para;</a></h3>
<p>Gotify currently runs in CT 104 docker stack at <code>gotify.nuclide.systems</code>. Moving to CT 109 isolates alerting from CT 104 outages but means updating env vars / webhook targets in ~10 places (MCP servers, Backrest webhooks). Plan: copy DB and app tokens via volume tar, deploy on CT 109, update Zoraxy upstream, then sweep dependents.</p>
<h3 id="migration-of-mcp-gateway-tier-b-split-control-workload">Migration of MCP Gateway (Tier B — split control / workload)<a class="headerlink" href="#migration-of-mcp-gateway-tier-b-split-control-workload" title="Permanent link">&para;</a></h3>
<p><code>/opt/stacks/ai/mcp-gateway/</code> on CT 104 is the OIDC-gated control plane for the ~20 MCP child containers. Decision (2026-05-20): move <strong>only the gateway</strong> to CT 109; <strong>the child MCP containers stay on CT 104</strong> (they're workload, not control plane).</p>
<p>Mechanics:
- Gateway on CT 109 uses <code>DOCKER_HOST=tcp://&lt;CT 104 socket-proxy&gt;:2375</code> (the docker-socket-proxy already planned for CT 104) instead of the bind-mounted <code>/var/run/docker.sock</code>. socket-proxy ACL must allow <code>containers, exec, images</code> (read+write).
- Migrate state files via volume tar: <code>config.json</code>, <code>agents.json</code>, <code>prompts/</code>, <code>usage.db</code>, <code>gateway_tokens.json</code>, <code>nc_user_creds.json</code>. Keep them on CT 109 local zfs, not UNAS (per-request latency matters).
- Pocket-ID redirect URI stays <code>https://mcp.nuclide.systems/sso/callback</code> — only Zoraxy's upstream flips from <code>192.168.1.40:8080</code> to the CT 109 IP.
- Don't touch the children; gateway still spawns them by name against CT 104's daemon.</p>
<p>Build-order slot: after Arcane + socket-proxy land in §16's checklist, before Tinyauth.</p>
<h3 id="arcane-specifics">Arcane specifics<a class="headerlink" href="#arcane-specifics" title="Permanent link">&para;</a></h3>
<ul>
<li>Existing <code>/opt/stacks/arcane/</code> on CT 104 has the Manager 80 % built: 41 MB SQLite DB carrying Pocket-ID OIDC client + admin user <code>fkrebs@nucli.de</code></li>
<li>Container currently stopped (~since May 19)</li>
<li><strong>Edge mode</strong> chosen for agents — agent dials out to Manager over HTTPS/gRPC with mTLS. Manager auto-generates the edge CA on first start; each agent enrolls with a token and gets a 1 yr client cert with auto-renewal</li>
<li>Same <code>ghcr.io/getarcaneapp/arcane:latest</code> image for Manager and agents; mode is env-driven (<code>ARCANE_EDGE_AGENT=true</code> on agents)</li>
<li>Intel iGPU is not in Arcane's GPU monitoring catalog (NVIDIA + AMD only). LXC-level passthrough to CT 104 stays untouched; only Arcane's GPU widgets will be blank for the Arc</li>
</ul>
<h3 id="build-order">Build order<a class="headerlink" href="#build-order" title="Permanent link">&para;</a></h3>
<ol>
<li>Create CT 109 (specs above)</li>
<li>Deploy node-exporter on host + Prometheus + Grafana first (start collecting baselines)</li>
<li>Deploy Arcane Manager with the existing DB restored from CT 104</li>
<li>Issue agent tokens, deploy Edge agents on CT 101/104/110</li>
<li>Deploy Dozzle Manager + agents</li>
<li>Deploy Diun</li>
<li>Deploy sshwifty (configure host list + SSH keys for PVE, CT 103, CT 104, CT 111; LAN-only port 8182)</li>
<li>Deploy Tinyauth (configure Pocket-ID client first)</li>
<li>Deploy Homarr</li>
<li>Front the lot via Zoraxy: <code>arcane.</code>, <code>dozzle.</code>, <code>grafana.</code>, <code>prom.</code>, <code>home.</code>, <code>shell.</code> <code>.nuclide.systems</code></li>
<li>Verify each through the Tinyauth gate where applicable</li>
<li>Add Diun watchlist + Alertmanager routing → Gotify</li>
</ol>
<hr />
<h2 id="17-lldp-unifi-topology-visibility">17. LLDP / UniFi topology visibility<a class="headerlink" href="#17-lldp-unifi-topology-visibility" title="Permanent link">&para;</a></h2>
<p>Investigated 2026-05-20:</p>
<ul>
<li><code>lldpd</code> is <strong>installed and active</strong> on the host (defaults: advertises + listens)</li>
<li>Existing <code>/usr/local/bin/update_interface_desc.sh</code> hourly cron consumes LLDP from neighbors and writes <code># PortDescr:</code> comments into <code>/etc/network/interfaces</code></li>
<li>Host is on switch <strong>port 10</strong> of the D-Link DGS-1210-28P (<code>SysName: dgs1210</code>, FW <code>6.32.008</code>).</li>
<li><code>/usr/local/bin/update_interface_desc.sh</code> was silently failing — <code>lldpcli</code> not in cron's PATH <strong>and</strong> the original logic only appended new lines, never replaced stale ones. Rewritten 2026-05-20:</li>
<li>PATH set inside the script (<code>/usr/sbin</code> included)</li>
<li>Uses <code>lldpcli show neighbors -f keyvalue</code> for machine-readable parsing</li>
<li>Single <code># LLDP: &lt;chassis&gt; :: &lt;port-descr&gt;</code> line per interface; legacy <code># PortDescr:</code> lines stripped</li>
<li>Idempotent (second run = no-op, no backup churn)</li>
<li>Daily-keyed backup <code>/etc/network/interfaces.bak.YYYYMMDD</code></li>
<li>Only logs on actual change</li>
<li>Old script preserved at <code>/usr/local/bin/update_interface_desc.sh.bak-2026-05-20</code></li>
</ul>
<p><strong>The UDM Pro won't see "nuc" in its topology view</strong> because LLDP frames use the Nearest-Bridge multicast (<code>01:80:c2:00:00:0e</code>) which any 802.1D-compliant switch terminates by spec — and the D-Link DGS-1210-28P sits between the host and the UDM. LLDP-MED is <strong>not</strong> a fix for this; it's for endpoint (VoIP/MFP) discovery, not transparent LLDP forwarding.</p>
<p>Remediation paths in order of effort:</p>
<ol>
<li><strong>Enable SNMP v2c/v3 on the DGS-1210</strong> + add as a Generic SNMP device in UniFi → UDM sees the switch and can map port↔MAC. Most practical for this stack.</li>
<li><strong>Add <code>snmpd</code> to the host</strong> + generic SNMP device in UniFi → CPU/mem/iface stats from Proxmox visible in UniFi (not in topology, but in monitoring).</li>
<li><strong>UniFi-managed switch between host and UDM</strong> — clean answer; requires hardware.</li>
</ol>
<p>D-Link DGS-1210 admin UI lives at <code>http://192.168.1.10/</code>. Verify the admin password is non-default — DGS-1210 ships with <code>admin</code>/blank or <code>admin</code>/<code>admin</code> on most firmware revisions. A flat-LAN switch with default creds is one of the easier vectors. <em>(Credentials redacted from this doc — check your password manager.)</em></p>
<p>Useful inspection from the host any time: <code>lldpcli show neighbors</code>.</p>
<hr />
<h2 id="18a-unas-access-uid-consistency-model-post-nfsv4-investigation">18a. UNAS access — UID consistency model (post-NFSv4 investigation)<a class="headerlink" href="#18a-unas-access-uid-consistency-model-post-nfsv4-investigation" title="Permanent link">&para;</a></h2>
<p><strong>Investigation result (2026-05-20):</strong> UNAS Pro advertises NFSv4 in <code>rpcinfo</code> but has no v4 export tree configured. Every v4 mount attempt returns <code>No such file or directory</code>. Ubiquiti has <strong>not</strong> announced NFSv4 support and the <a href="https://community.ui.com/questions/When-will-NFSv4-be-available-for-UNAS-Pro/b8f8b00b-c5ac-4e19-adc4-9e5cae06d35f">community thread asking for it</a> has no ETA. The official help center also confirms: <em>"UniFi Drive does not support certain NFS export options, such as <code>no_root_squash</code>"</em>. So root_squash + v3-only is the long-term reality.</p>
<h3 id="universal-uid-landscape-on-unas">Universal UID landscape on UNAS<a class="headerlink" href="#universal-uid-landscape-on-unas" title="Permanent link">&para;</a></h3>
<p>Every NFS client write lands as <strong>uid 977 / gid 988</strong> (UNAS's <code>all_squash</code> + <code>anon_uid=977</code>/<code>anon_gid=988</code>). The chown probe confirmed no client can change this from the host side. Files written via the legacy CIFS mount appear as <strong>uid 33</strong> to the CIFS client <em>but</em> are stored differently on UNAS — the CIFS <code>forceuid=33</code> mount option lies about ownership client-side.</p>
<h3 id="per-ct-access-pattern-canonical">Per-CT access pattern (canonical)<a class="headerlink" href="#per-ct-access-pattern-canonical" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>CT</th>
<th>Mount</th>
<th>Container uid</th>
<th>Effective on disk</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>103 backrest</td>
<td>NFS bind from host</td>
<td>root</td>
<td>squashes to 977</td>
<td>✓ consistent</td>
</tr>
<tr>
<td>104 docker</td>
<td>NFS bind from host</td>
<td>mostly root, n8n=1000 (latent)</td>
<td>squashes to 977</td>
<td>✓ for root containers; n8n latent if it ever writes to UNAS</td>
</tr>
<tr>
<td>105 nextcloud</td>
<td>CIFS today (forceuid=33) → <strong>NFS + bindfs</strong> target</td>
<td>uid 33 (www-data) inside Nextcloud, bindfs translates to 977 on disk</td>
<td>needs migration</td>
<td>🚨 still CIFS</td>
</tr>
</tbody>
</table>
<h3 id="convention-for-new-containers">Convention for new containers<a class="headerlink" href="#convention-for-new-containers" title="Permanent link">&para;</a></h3>
<p>Set <code>PUID=977 PGID=988</code> on any container that writes to UNAS. This pre-aligns with UNAS's enforced mapping and avoids latent permission issues (the n8n class). For images that don't support PUID/PGID, run them as root inside the container — root squashes to 977 cleanly.</p>
<h3 id="why-bindfs-for-ct-105-specifically">Why bindfs for CT 105 specifically<a class="headerlink" href="#why-bindfs-for-ct-105-specifically" title="Permanent link">&para;</a></h3>
<p>Nextcloud's PHP code hard-checks file ownership against <code>www-data</code> (uid 33). Without remap, NFS reads return uid 977 and Nextcloud refuses to operate normally. CIFS hides this with <code>forceuid=33</code>. NFS+bindfs achieves the same lie with the much faster NFS rail underneath — verified ~5× speed-up on metadata-heavy ops in the non-destructive test on 2026-05-20.</p>
<h3 id="trigger-event-to-revisit">Trigger event to revisit<a class="headerlink" href="#trigger-event-to-revisit" title="Permanent link">&para;</a></h3>
<p>Watch <a href="https://community.ui.com/RELEASES">community.ui.com/RELEASES</a> for a UniFi Drive release that adds:
- NFSv4 export option (would enable idmap)
- <code>no_root_squash</code> support (would enable server-side chown to specific uids)
- Configurable <code>anonuid</code>/<code>anongid</code> (would let us match a real uid)</p>
<p>Any of these would let us simplify the CT 105 stack.</p>
<hr />
<h2 id="18-homarr-inventory-services-to-include-on-the-dashboard">18. Homarr inventory — services to include on the dashboard<a class="headerlink" href="#18-homarr-inventory-services-to-include-on-the-dashboard" title="Permanent link">&para;</a></h2>
<p>Captured here so the eventual Homarr config can be assembled in one pass. Groups follow the existing <code>homepage.*</code> label convention used in compose files.</p>
<h3 id="group-infrastructure">Group: <code>infrastructure</code><a class="headerlink" href="#group-infrastructure" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Service</th>
<th>URL</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Proxmox UI</td>
<td><code>https://192.168.1.20:8006</code></td>
<td>until LE via Zoraxy lands, see §11b</td>
</tr>
<tr>
<td>AdGuard Home (CT 102)</td>
<td><code>http://192.168.1.2/</code> (UI on <code>:80</code>)</td>
<td>DNS + admin</td>
</tr>
<tr>
<td>Zoraxy (CT 108)</td>
<td><code>http://192.168.1.4:8000/</code></td>
<td>reverse proxy admin</td>
</tr>
<tr>
<td>Backrest (CT 103)</td>
<td><code>http://192.168.1.3:9898/</code></td>
<td>backup orchestration, will be fronted via Tinyauth + Zoraxy</td>
</tr>
<tr>
<td><strong>Pocket-ID (CT 110)</strong></td>
<td><code>https://id.nuclide.systems/</code></td>
<td>new home, 2026-05-20</td>
</tr>
</tbody>
</table>
<h3 id="group-network">Group: <code>network</code><a class="headerlink" href="#group-network" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Service</th>
<th>URL</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>UDM Pro</td>
<td><code>https://192.168.1.1/</code></td>
<td>UniFi controller</td>
</tr>
<tr>
<td><strong>D-Link DGS-1210-28P</strong></td>
<td><code>http://192.168.1.10/</code></td>
<td>core L2 switch; host on port 10</td>
</tr>
<tr>
<td>UNAS Pro</td>
<td><code>https://192.168.1.31/</code></td>
<td>UniFi NAS</td>
</tr>
</tbody>
</table>
<h3 id="group-ops-to-populate-when-ct-109-lands">Group: <code>ops</code> (to populate when CT 109 lands)<a class="headerlink" href="#group-ops-to-populate-when-ct-109-lands" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Service</th>
<th>URL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Grafana</td>
<td><code>https://grafana.nuclide.systems</code></td>
</tr>
<tr>
<td>Arcane</td>
<td><code>https://arcane.nuclide.systems</code></td>
</tr>
<tr>
<td>Dozzle</td>
<td><code>https://dozzle.nuclide.systems</code></td>
</tr>
<tr>
<td>Prometheus</td>
<td><code>https://prom.nuclide.systems</code> (gated by Tinyauth)</td>
</tr>
<tr>
<td>Alertmanager</td>
<td><code>https://alerts.nuclide.systems</code> (gated by Tinyauth)</td>
</tr>
</tbody>
</table>
<h3 id="group-apps-subset-long-list-fill-from-existing-homepage-labels-in-optstacks">Group: <code>apps</code> (subset — long list, fill from existing <code>homepage.*</code> labels in <code>/opt/stacks/*/</code>)<a class="headerlink" href="#group-apps-subset-long-list-fill-from-existing-homepage-labels-in-optstacks" title="Permanent link">&para;</a></h3>
<p>Immich, Nextcloud, Vaultwarden, Karakeep, Memos, Paperless-ngx, n8n, ComfyUI, LobeChat, LiteLLM, Traccar, Gotify, Speaches, Daytona, Searxng, Kroki, etc. Pull display labels and icons from the existing <code>homepage.name=</code> / <code>homepage.icon=</code> values per compose.</p>
<hr />
<h2 id="12-suggested-action-order">12. Suggested action order<a class="headerlink" href="#12-suggested-action-order" title="Permanent link">&para;</a></h2>
<ol>
<li><strong>Done 2026-05-20</strong> ✅:</li>
<li><strong>Memory / sizing</strong>: CT 101 160 → 32 GiB; CT 102 512 MiB → 1 GiB; HAOS balloon = 4 GiB</li>
<li><strong>Protection</strong>: CT 102 startup=1+protection; CT 108 startup=2+protection; CT 103/104/105/110/VM100 protection</li>
<li><strong>CT 110 (id)</strong> built at 192.168.1.5, Pocket-ID duplicated (online SQLite snapshot)</li>
<li><strong>ZFS</strong>: <code>autotrim=on</code>, <code>atime=off rpool</code></li>
<li><strong>Backups</strong>: retention set on <code>unas</code></li>
<li><strong>APT</strong>: duplicate sources removed; host <code>unattended-upgrades</code> deployed (Debian-only)</li>
<li><strong>Per-CT u-u</strong>: deployed to all 7 CTs with Debian-only allowlist</li>
<li><strong>Cron cleanup</strong>: removed weekly tteck-LXC-update curl-pipe-bash; removed daily broken <code>apt-get upgrade -y</code>; replaced <code>/root/vm100.sh</code> cron with <code>vm100-watchdog.timer</code> (1 min, lock-aware)</li>
<li><strong>Self-updaters</strong>: AdGuard (<code>--update</code> flag) Wed 03:30; Zoraxy (GitHub stable releases + rollback) Wed 03:40</li>
<li><strong><code>zpool upgrade rpool</code></strong> ran during the audit (enabled <code>redaction_list_spill</code>, <code>raidz_expansion</code>)</li>
<li><strong>Today with a maintenance window</strong>:</li>
<li><strong>Cut Zoraxy over to CT 110</strong> for <code>id.nuclide.systems</code> (single upstream edit; rollback path = revert one line)</li>
<li><code>apt full-upgrade</code> (kernel 7.0.0-3 → 7.0.2-5, pve-manager 9.1.11 → 9.1.18) + reboot</li>
<li>VM 100 disk options (<code>cache=none</code>, <code>iothread=1</code>) — §4. Requires VM stop/start.</li>
<li>Switch CT 105 from CIFS → NFS bind-mount (§4). Test UID mapping.</li>
<li>Drop the <code>unas_smb</code> storage once CT 105 is migrated.</li>
<li><strong>This week</strong>:</li>
<li>Re-measure CT 104 peak RSS after fixing CT 101 — likely safe to drop to 48 GiB.</li>
<li>Convert CT 105 to unprivileged (backup → restore as unprivileged).</li>
<li>Raise ARC cap to 16 GiB.</li>
<li>Probe NFSv4 against UNAS; switch if supported.</li>
<li><strong>Wire Postfix relayhost</strong> (Gmail/Postmark/your SMTP) so unattended-upgrades + zfs-zed + cron failures actually mail you.</li>
<li><strong>Rotate Backrest plan</strong> to back up real data (currently still pointed at <code>/media/data-dir</code> — a 50 KB test file from August 2025); see §7.</li>
<li><strong>Medium term</strong>:</li>
<li><strong>Build CT 109</strong> ops LXC (§16) — Prometheus + Grafana + Arcane Manager + Dozzle + Homarr + Diun + Tinyauth</li>
<li>Migrate Gotify from CT 104 to CT 109 (~30 min of env-var updates)</li>
<li><strong>Rotate exposed secrets</strong> that appeared in this transcript: Arcane <code>OIDC_CLIENT_SECRET</code>, <code>ENCRYPTION_KEY</code>, <code>JWT_SECRET</code>; Immich <code>IMMICH_API_KEY</code></li>
<li>Backrest: enable auth, redesign plans to cover all data tiers (§7), front via Tinyauth for OIDC</li>
<li><strong>Next purchase window</strong>:</li>
<li>Second NVMe → mirror <code>rpool</code> (§6)</li>
<li><code>proxmox-boot-tool init</code> on the new disk</li>
<li>2.5 GbE NIC + matching switch port to UNAS for image-gen / backup speed</li>
<li>UniFi-managed switch between host and UDM (or accept SNMP-only visibility from UniFi)</li>
</ol>
<hr />
<hr />
<h2 id="19-changes-applied-2026-05-20-session-2">19. Changes applied 2026-05-20 (session 2)<a class="headerlink" href="#19-changes-applied-2026-05-20-session-2" title="Permanent link">&para;</a></h2>
<h3 id="optimizations-executed">Optimizations executed<a class="headerlink" href="#optimizations-executed" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>#</th>
<th>Item</th>
<th>Command / action</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>CT 101 protection + boot order</td>
<td><code>pct set 101 -protection 1 -startup order=10</code></td>
<td></td>
</tr>
<tr>
<td>4</td>
<td>CT 104 memory cap 128→48 GiB</td>
<td><code>pct set 104 -memory 49152</code></td>
<td>✅ (swap 32→8 deferred: still 10.4 GB in use)</td>
</tr>
<tr>
<td>5</td>
<td>CT 102 rootfs 2→4 GiB</td>
<td><code>pct resize 102 rootfs 4G</code></td>
<td>✅ now 28% used</td>
</tr>
<tr>
<td>A</td>
<td>VM 100 disk: cache=none + iothread=1</td>
<td><code>qm set 100 -scsi0 ...,cache=none,iothread=1</code> + <code>scsihw virtio-scsi-single</code></td>
<td>✅ HAOS healthy</td>
</tr>
<tr>
<td>C</td>
<td>Host apt full-upgrade</td>
<td>kernel 7.0.0→7.0.2-5, pve-manager 9.1.11→9.1.18</td>
<td>✅ installed; <strong>reboot pending</strong></td>
</tr>
</tbody>
</table>
<h3 id="pocket-id-migration-completed">Pocket-ID migration completed<a class="headerlink" href="#pocket-id-migration-completed" title="Permanent link">&para;</a></h3>
<ul>
<li><code>id.nuclide.systems</code> Zoraxy proxy cutover confirmed: <code>192.168.1.40:11000</code><code>192.168.1.5:11000</code></li>
<li>CT 104 <code>/opt/stacks/pocketid/</code> directory fully removed (data migrated to CT 110 2026-05-20)</li>
<li>Backup: <code>/opt/zoraxy/conf/proxy/id.nuclide.systems.config.bak-pre-ct110</code> (keep as rollback)</li>
</ul>
<h3 id="proxmox-oidc-via-pocket-id">Proxmox OIDC via Pocket-ID<a class="headerlink" href="#proxmox-oidc-via-pocket-id" title="Permanent link">&para;</a></h3>
<p>Realm <code>pocket-id</code> added; user <code>fkrebs@nucli.de@pocket-id</code> mapped to Administrator role.</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-18-1" name="__codelineno-18-1" href="#__codelineno-18-1"></a>pveum<span class="w"> </span>realm<span class="w"> </span>add<span class="w"> </span>pocket-id<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-18-2" name="__codelineno-18-2" href="#__codelineno-18-2"></a><span class="w"> </span>--type<span class="w"> </span>openid<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-18-3" name="__codelineno-18-3" href="#__codelineno-18-3"></a><span class="w"> </span>--issuer-url<span class="w"> </span>https://id.nuclide.systems<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-18-4" name="__codelineno-18-4" href="#__codelineno-18-4"></a><span class="w"> </span>--client-id<span class="w"> </span>38469e7e-1fff-4841-83a9-74bf38d847eb<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-18-5" name="__codelineno-18-5" href="#__codelineno-18-5"></a><span class="w"> </span>--client-key<span class="w"> </span>&lt;secret&gt;<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-18-6" name="__codelineno-18-6" href="#__codelineno-18-6"></a><span class="w"> </span>--username-claim<span class="w"> </span>email<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-18-7" name="__codelineno-18-7" href="#__codelineno-18-7"></a><span class="w"> </span>--comment<span class="w"> </span><span class="s2">&quot;Pocket-ID OIDC&quot;</span>
<a id="__codelineno-18-8" name="__codelineno-18-8" href="#__codelineno-18-8"></a>
<a id="__codelineno-18-9" name="__codelineno-18-9" href="#__codelineno-18-9"></a>pveum<span class="w"> </span>user<span class="w"> </span>add<span class="w"> </span>fkrebs@nucli.de@pocket-id
<a id="__codelineno-18-10" name="__codelineno-18-10" href="#__codelineno-18-10"></a>pveum<span class="w"> </span>aclmod<span class="w"> </span>/<span class="w"> </span>--users<span class="w"> </span>fkrebs@nucli.de@pocket-id<span class="w"> </span>--roles<span class="w"> </span>Administrator
</code></pre></div>
<p>OIDC client inserted directly into Pocket-ID SQLite (API key stored as SHA-256 hash — not reversible):</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-19-1" name="__codelineno-19-1" href="#__codelineno-19-1"></a>DB: /opt/stacks/pocketid/data/pocket-id.db on CT 110
<a id="__codelineno-19-2" name="__codelineno-19-2" href="#__codelineno-19-2"></a>Table: oidc_clients
<a id="__codelineno-19-3" name="__codelineno-19-3" href="#__codelineno-19-3"></a>client_id: 38469e7e-1fff-4841-83a9-74bf38d847eb
<a id="__codelineno-19-4" name="__codelineno-19-4" href="#__codelineno-19-4"></a>name: Proxmox VE
<a id="__codelineno-19-5" name="__codelineno-19-5" href="#__codelineno-19-5"></a>callback_urls: [&quot;https://192.168.1.20:8006&quot;]
</code></pre></div>
<p><strong>To add future OIDC clients without UI access:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-20-1" name="__codelineno-20-1" href="#__codelineno-20-1"></a>python3<span class="w"> </span>-c<span class="w"> </span><span class="s2">&quot;</span>
<a id="__codelineno-20-2" name="__codelineno-20-2" href="#__codelineno-20-2"></a><span class="s2">import uuid, secrets, bcrypt, json, datetime</span>
<a id="__codelineno-20-3" name="__codelineno-20-3" href="#__codelineno-20-3"></a><span class="s2">client_id = str(uuid.uuid4())</span>
<a id="__codelineno-20-4" name="__codelineno-20-4" href="#__codelineno-20-4"></a><span class="s2">secret_plain = secrets.token_urlsafe(32)</span>
<a id="__codelineno-20-5" name="__codelineno-20-5" href="#__codelineno-20-5"></a><span class="s2">secret_hash = bcrypt.hashpw(secret_plain.encode(), bcrypt.gensalt(rounds=10)).decode()</span>
<a id="__codelineno-20-6" name="__codelineno-20-6" href="#__codelineno-20-6"></a><span class="s2">print(f&#39;id={client_id}&#39;)</span>
<a id="__codelineno-20-7" name="__codelineno-20-7" href="#__codelineno-20-7"></a><span class="s2">print(f&#39;secret={secret_plain}&#39;)</span>
<a id="__codelineno-20-8" name="__codelineno-20-8" href="#__codelineno-20-8"></a><span class="s2">print(f&#39;hash={secret_hash}&#39;)</span>
<a id="__codelineno-20-9" name="__codelineno-20-9" href="#__codelineno-20-9"></a><span class="s2">&quot;</span>
<a id="__codelineno-20-10" name="__codelineno-20-10" href="#__codelineno-20-10"></a><span class="c1"># Then INSERT into oidc_clients with the hash, use secret_plain in the app config</span>
<a id="__codelineno-20-11" name="__codelineno-20-11" href="#__codelineno-20-11"></a><span class="c1"># callback_urls and logout_callback_urls are JSON arrays stored as BLOB</span>
<a id="__codelineno-20-12" name="__codelineno-20-12" href="#__codelineno-20-12"></a><span class="c1"># credentials field is &#39;{}&#39; for standard clients</span>
</code></pre></div></p>
<p><strong>Note on Pocket-ID API keys:</strong> The <code>key</code> column in <code>api_keys</code> stores a SHA-256 hash of the real key (64-char hex). The plaintext key is only shown once at creation time in the UI. If lost, create a new one — there is no recovery path.</p>
<p><strong>Login flow:</strong> In PVE web UI, select realm <code>pocket-id</code> at login. You will be redirected to <code>https://id.nuclide.systems</code> for authentication and returned to PVE. The <code>email</code> claim is used as the PVE username.</p>
<hr />
<h2 id="audit-footnote-side-effects-of-this-run">Audit footnote — side effects of this run<a class="headerlink" href="#audit-footnote-side-effects-of-this-run" title="Permanent link">&para;</a></h2>
<ul>
<li><code>zpool upgrade rpool</code> was executed (not <code>-n</code>). Enabled features: <code>redaction_list_spill</code>, <code>raidz_expansion</code>. Safe on current ZFS version; the pool can no longer be imported by ZFS releases that pre-date these features. No data risk.</li>
<li>No other state changes were made by this audit.</li>
</ul>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "../..", "features": ["navigation.tabs", "navigation.sections", "navigation.expand", "navigation.top", "search.highlight", "search.suggest", "content.code.copy"], "search": "../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../assets/javascripts/bundle.79ae519e.min.js"></script>
</body>
</html>