โŸจ Global Topology โŸฉ

Two WAN links into a FortiGate edge, a local Proxmox cluster, a cloud node, a public DMZ, and a second homelab, all stitched together by a Tailscale control plane so management never crosses the public internet.

Edge โ†’ Core โ†’ Sites

WAN Uplinks

Fiber 2.5 GbE

Primary

Cable 950/40

Backup

Edge

FortiGate SD-WAN

Failover ยท SLA steering ยท IPS / UTM ยท threat feeds

Resolution & Core

Technitium DNS

โ†’ Unbound

Pi-hole

โ†’ Unbound

Core Switch

VLAN trunk ยท LACP

Sites & Segments

Local PVE Cluster

chromium ยท palladium ยท osmium ยท prometheus ยท rhenium ยท 10.0.0.0/24

DMZ ยท thallium

nginx + Docker ยท public

Cloud PVE1

cktech-nginx ยท 10.10.10.0/24

Azure ยท ckel-web-01

public web ยท CrowdSec bouncer

Second Homelab

Lab40 VLAN โ†’ Lab-40F 90G ยท Hyper-V ร—2 + Veeam

Backup & DR

Synology NAS

Active Backup ยท M365 / Workspace

Proxmox Backup Server

dedup ยท encrypted

Wasabi S3

offsite ยท 3-2-1

Control Plane Overlay

๐Ÿ”— The Tailscale mesh spans every site. All infra comms (SSH, LAPI, Wazuh, dashboards) ride the tailnet with strict ACLs and per-session SSH auth.

Internal LAN / cluster Public surface / DMZ Cloud Second site Backup / DR

The edge is a FortiGate bonding a 2.5 GbE fiber line with a cable circuit into one resilient pipe, where SD-WAN health checks fail traffic over if a link drops and SLA-based steering keeps latency-sensitive workloads on the best path. Behind it, a dual-resolver DNS setup and a VLAN-trunked core fan out to the local Proxmox cluster on 10.0.0.0/24, a cloud Proxmox node on its own 10.10.10.0/24 SDN, a public DMZ, and a separate second homelab on its own VLAN and firewall.

The piece that ties it all together is Tailscale. Rather than exposing management interfaces, every site joins one tailnet and all administrative traffic (SSH, the CrowdSec LAPI, the Wazuh indexer, Grafana and Prometheus) rides that overlay with strict ACLs and authenticated SSH on every session. The only things deliberately facing the public internet are a hardened web front door and a single threat-feed endpoint, both isolated from the internal estate.

โŸจ The FortiGate Edge โŸฉ

One FortiGate 90G doing real enterprise work. SD-WAN, server and link load balancing, full deep-SSL inspection, and Layer-7 filtering all run without giving up line-rate throughput.

Speeds & Feeds ยท Line-Rate With the Brain On

  • โšก 2.5 Gbps symmetric internet on the WAN1 fiber (static block), sustained with Full SSL Inspection + IPS + Fortinet threat protection all enabled.
  • ๐Ÿ”€ The 10 GbE WAN port repurposed as the LAN uplink โ†’ 10 G UniFi switch โ†’ 2.5 G access switches; a 1 GbE LAN port redefined as WAN2, the cable backup for SD-WAN failover.
  • ๐Ÿงฎ 10 G east-west between the Arch workstation and the entire PVE cluster + backup hosts, VLAN-segmented on the switched fabric.
  • ๐Ÿ“ถ 2ร— U7 Pro XG delivering โ‰ˆ1.5 Gbps symmetric Wi-Fi; Guest50 and IOT60 client-isolated via the UniFi Network Controller.

SD-WAN

Failover & SLA Steering

FortiGate SD-WAN failover and SLA steering LAN and VLAN traffic hits the SD-WAN rules, which are driven by Performance SLA health probes and steer egress out to the internet through the two members of the wan-failover-link zone, WAN1 fiber (2.5 GbE primary) and WAN2 cable (~1 GbE backup, a redefined LAN port). SD-WAN zone ยท wan-failover-link LAN + VLANs 10.0.0.0/24 ยท VLAN 10โ€“80 SD-WAN Rules SLA steering ยท app-awaresource / dest rules Performance SLA probes: latencyjitter / loss WAN1 ยท Fiber 2.5 GbE ยท primarystatic block WAN2 ยท Cable ~1 GbE ยท backupredefined LAN port Internet
Dotted = Performance SLA feeding the steering decision

Load Balancing ยท 1 of 2

SD-WAN Link Load Balancing

SD-WAN link load balancing across both WAN members New LAN sessions are spread by the SD-WAN load-balance algorithm across both active members, with roughly 70 percent on the WAN1 fiber link and 30 percent on the WAN2 cable link by bandwidth. This aggregates egress to the internet rather than failing over. Both WAN members active โ‰ˆ70% โ‰ˆ30% Sessions (LAN) new connections Load-balance algo volume ratio ยท sessionssource-dest-IP ยท spillover WAN1 ยท Fiber โ‰ˆ70% by bandwidth WAN2 ยท Cable โ‰ˆ30% by bandwidth Internet
Both WAN members carry traffic at once, split by bandwidth ratio. This is aggregation, not failover.

Load Balancing ยท 2 of 2

Server Load Balancing (VIP โ†’ Pool)

FortiGate server load balancing to a backend pool Clients reach a FortiGate Virtual Server VIP on :443 that offloads SSL, applies persistence and a health monitor, and distributes connections across three nginx real servers in the DMZ30 pool using round-robin or least-connections. DMZ30 real-server pool VIP :443 Clients Internet ยท HTTPS Virtual Server (VIP) WAN1 ยท SSL offloadpersistence ยท health monitorround-robin / least-conn web-01 ยท nginx 10.0.30.7 web-02 ยท nginx 10.0.30.8 web-03 ยท nginx 10.0.30.9
Virtual Server VIP offloads SSL and balances across a health-checked nginx pool with persistence.

Deep inspection

Full SSL Inspection ยท Controlled MITM

FortiGate full SSL inspection as a controlled man-in-the-middle The FortiGate sits between client and origin, terminating two separate TLS sessions. It presents the client a certificate re-signed by its own CKEL_ROOT_CA (trusted by managed endpoints) while validating the origin's real certificate, decrypting traffic in the middle so it can run IPS, antivirus, application control, web filtering and DLP before re-encrypting. TLS session 1re-signed cert (FortiGate CA) TLS session 2real cert ยท CA-store check Client / Browser trusts CKEL_ROOT_CA FortiGate ยท Full SSL decrypt โ†’ inspect โ†’ re-encrypttwo independent TLS sessions Origin Server real cert validated Plaintext inspection IPS ยท AV ยท AppCtrl ยท WebFilter ยท DLP
Two TLS sessions: the FortiGate re-signs with CKEL_ROOT_CA to the client while validating the origin's real cert. Dotted = decrypted payload handed to inspection

Application layer

How Layer-7 Filtering Works

FortiGate Layer 7 application-layer filtering After decryption, the Layer 7 inspection engine fans the application-layer payload out to parallel modules (application control, web filtering, IPS, antivirus, DNS filtering and DLP), and the combined result drives a single policy verdict to allow, block, log or shape the flow. Decrypted L7 payload application-layer data L7 inspection engine deep packet inspection App Control apps ยท any port Web Filter URL / category IPS exploit sigs Antivirus file / malware DNS Filter domain category DLP data patterns Policy verdict allow ยท block ยท log ยท shape
Decrypted payload fans out to every L7 module in parallel; their combined result is one policy verdict.

The edge is a single FortiGate 90G pushed well past a typical home deployment. SD-WAN bonds a 2.5 GbE fiber primary with a 1 GbE cable backup in the wan-failover-link zone, where Performance SLA health probes drive both automatic failover and application-aware steering. When configured for it, SD-WAN also spreads sessions across both links at once by bandwidth ratio for aggregate throughput.

On the application side, a Virtual Server offloads SSL and load-balances across a health-checked nginx pool, while Full SSL Inspection acts as a controlled man-in-the-middle: the FortiGate terminates two TLS sessions, re-signing to managed clients with its own CKEL_ROOT_CA while validating each origin's real certificate. That decrypted stream is what makes Layer-7 filtering possible. Application control, web filtering, IPS, antivirus, DNS filtering and DLP all inspect the payload in the clear before it is re-encrypted and a single policy verdict is applied. The notable part is doing all of this at 2.5 Gbps symmetric with inspection fully enabled.

โŸจ Virtualization & Compute โŸฉ

A nine-node Proxmox cluster for server workloads with HA failover, ZFS everywhere, and segmented SDN, where KVM runs Windows, Linux, and macOS (via OpenCore) guests, so any OS can be spun up and tested on real hardware, with VFIO passthrough for GPU-bound work.

Proxmox VE Cluster

Corosync quorum ยท HA ยท live migration

ZFS Storage

NVMe mirrors ยท RAIDz2 ยท zstd ยท scrubs

SDN

VLAN-aware bridges ยท VXLAN/EVPN ยท LACP

Cloud-Init / IaC

golden templates ยท Terraform ยท Ansible

Pre-copy memory migration

Live Migration

Proxmox live migration via pre-copy memory transfer A running VM moves from cluster node pve-a to pve-b with sub-second downtime. Its memory is pre-copied over the migration network while the guest keeps running, dirtied pages are re-copied in converging rounds, then a brief stop-and-copy ships the final pages plus CPU and device state and the VM resumes on the target. Shared or ZFS-replicated storage means the virtual disk is never copied. โ†ป rounds no disk copy migration network converged switchover Running VM ยท pve-a active RAMvCPU + device state 1 ยท Memory pre-copy copy all RAM โ†’ pve-bVM keeps running 2 ยท Dirty-page rounds re-copy changed pagesiterate until converged 3 ยท Stop-and-copy pause VM ยท sub-secondfinal pages + CPU state 4 ยท Resume on pve-b VM live on targetsource instance freed Shared / ZFS storage disk stays putZFS send if node-local
dotted = storage stays put (shared / ZFS-replicated)

The compute layer is a nine-node Proxmox VE cluster spanning everything from an EPYC server to small-form-factor boxes, tied together with Corosync quorum and HA so guests live-migrate and restart automatically when a node drops. Storage is ZFS end to end. NVMe mirrors and RAIDz2 pools run compression and scheduled scrubs, and their snapshots feed both replication between nodes and the backup chain.

Windows guests

VirtIO drivers ยท GPU passthrough when needed

Linux guests

any distro ยท cloud-init golden templates

macOS guests

OpenCore + KVM ยท cross-platform testing

Because everything runs on KVM, the cluster can boot virtually any operating system, from Windows and any Linux distro to macOS via OpenCore. A guest is available for testing on essentially any OS environment on real hardware, without dedicated machines or dual-booting.

Workflow

VFIO GPU Passthrough

Enable IOMMU on the host

amd_iommu=on ยท iommu=pt ยท isolate the IOMMU group

Bind GPU to vfio-pci

blacklist host driver ยท stub the device at boot

q35 + OVMF guest

PCIe passthrough ยท GPU + audio function ยท reset handling

Guest = near bare-metal GPU

RTX 4090 inference VM ยท isolated compute node

For workloads that need real hardware, VFIO passthrough isolates a GPU's IOMMU group, binds it to vfio-pci, and hands it straight into a q35/OVMF guest, so a full RTX 4090 runs inside a VM at close to bare-metal speed as a dedicated inference node.

โŸจ Arch Workstation ยท High-Perf Virtualization โŸฉ

Separate from the cluster: a single Arch Linux daily driver (Ryzen 9950X3D ยท RTX 5090) where KVM + VFIO passthrough hands a real GPU to a Windows guest, and Looking Glass mirrors it back as a lossless, near bare-metal desktop.

Arch Linux (KVM + VFIO) โ†’ Windows guest ยท lossless video

Looking Glass

Looking Glass with VFIO GPU passthrough on the Arch workstation A Windows guest gets a real GPU via VFIO passthrough, so 3D apps and CUDA run at near bare-metal speed. The guest renders to its passthrough GPU; the Looking Glass host app copies each frame into an IVSHMEM shared-memory region, and the Looking Glass client on the host reads it straight from RAM, delivering a lossless desktop with no video encode and no network hop. Keyboard and mouse input is fed back into the guest. Windows guest VM Arch Linux host ยท KVM + VFIO input ยท keyboard / mouse render write frames read frames Windows guest q35 ยท OVMFpassthrough GPU LG host app captures framebufferruns in guest IVSHMEM shared memoryzero-copy frames LG client runs on hostreads from RAM Arch host desktop lossless ยท ~no latencyhigh-perf Windows apps
dotted = keyboard / mouse fed back to the guest ยท frames are uncompressed, never leave RAM

The daily driver runs Arch Linux as the KVM host. VFIO passthrough isolates the GPU's IOMMU group, binds it to vfio-pci, and hands it into a q35/OVMF Windows guest, so the VM drives a real GPU at close to bare-metal speed.

Looking Glass then copies each guest frame through an IVSHMEM shared-memory region into a client on the Arch host. The result is an uncompressed, low-latency desktop with no video encode and no network hop, so 3D apps and CUDA workloads inside Windows run at effectively bare-metal performance, in a window right alongside Linux.

Why it works ยท clean IOMMU groups

GPU Split: vfio-pci vs host

GPU and IOMMU split on the Arch workstation Clean IOMMU groups let the discrete RTX 5090 and its HDMI-audio function be stubbed to vfio-pci at boot and handed whole to the Windows guest, while the CPU's Radeon integrated GPU keeps the native amdgpu host driver and drives the Arch desktop, including the Looking Glass client. Isolated โ†’ passed through to the VM Retained โ†’ drives the Arch host bind PCIe handoff load RTX 5090 (dGPU) VGA + HDMI audio fnown IOMMU group Radeon iGPU Raphael ยท 9950X3Down IOMMU group vfio-pci stubbed at bootdevice isolated amdgpu native host driver Windows guest q35 ยท OVMFfull RTX 5090 Arch host desktop + LG clientiGPU output
isolated โ†’ Windows guest retained โ†’ Arch host

The split only works because the GPUs land in clean IOMMU groups. The discrete RTX 5090, along with its HDMI-audio function, is stubbed to vfio-pci at boot and handed whole to the guest, while the CPU's Radeon iGPU keeps the native amdgpu driver and drives the Arch desktop, so the host still has a display to render the Looking Glass client into.

This is the same approach on both the cluster and the workstation: I spec motherboards with proper IOMMU support up front so devices land in clean groups on their own. ACS-override patches work and have been reliable enough when I've needed them, but choosing hardware that groups correctly removes the need for them entirely. The payoff is fewer kernel patches, cleaner upgrades, and predictable passthrough.

โŸจ Backup & Disaster Recovery โŸฉ

Every dataset rides a 3-2-1 footing, and the restores are tested, not assumed. Each platform has its own chain, all converging on encrypted offsite copies in Wasabi.

What 3-2-1 means

  • 3 copies means the live data plus two backups, so a single loss is never fatal.
  • 2 media types means on-disk ZFS/NVMe plus a separate backup appliance (PBS / Synology / Veeam repo), so one failure mode can't take both.
  • 1 offsite means a geo-separated, encrypted copy in Wasabi that survives a site-level disaster.

The unofficial +1: restores are rehearsed on a schedule. A backup you haven't restored is a hope, not a recovery plan.

The rule, visualized

3-2-1 Backup Strategy

The 3-2-1 backup rule One production dataset is copied three times across two media types with at least one copy held offsite in Wasabi, and every copy feeds a scheduled restore-test step so recovery is proven. on-site ยท 2 media types 1 offsite Production data VMs ยท /home ยท /dataMicrosoft 365 ยท SaaS โ‘  on-site primary live storageNVMe ยท ZFS โ‘ก on-site backup PBS ยท restic ยท Veeamseparate media โ‘ข offsite copy Wasabi S3 regiongeo-separated Tested restores scheduled restore drillsrecovery proven

Daily driver

Arch ยท btrfs / snapper / restic

Arch Linux btrfs, snapper, and restic backup chain The btrfs root uses flat subvolumes. snapper keeps an automated thirty-day window of local snapshots for instant rollback, while a systemd-timed restic job ships encrypted file-level copies of /home and /data to a Synology NAS that HyperBackup mirrors offsite to Wasabi. local ยท btrfs snapshots Synology ยท 2nd media snapshot daily ยท encrypted offsite Arch root ยท btrfs @ ยท @home ยท @log@pkg ยท @snapshots snapper timeline + cleanup timers~30-day local snapshots Instant rollback boot into snapshotpre/post pacman restic ยท encrypted /home + /data file-leveldaily ยท keep 7dยท4wยท6m Synology NAS restic repo target2nd media ยท on-site HyperBackup NAS โ†’ cloud sync Wasabi S3 offsite ยท encrypted
snapper = instant local rollback ยท restic = encrypted file-level offsite

Cluster + cloud node

Proxmox ยท PBS / ZFS / Wasabi

Proxmox cluster backup and offsite chain Proxmox guests on ZFS feed two on-site copies. Node-to-node ZFS replication provides warm standby, and Proxmox Backup Server keeps incremental, deduplicated, client-encrypted snapshots. Both the local PBS and the offsite cloud PVE node sync to Wasabi for the geo-separated copy. on-site cluster warm nightly sync ยท offsite offsite Proxmox guests VMs ยท LXC on ZFS ZFS replication node โ†’ nodewarm standby Proxmox Backup Server incremental ยท dedupclient-encrypted Cloud PVE node offsite memberown backup job Wasabi S3 offsite ยท dedupencrypted at rest
ZFS replication = warm standby ยท PBS = incremental, deduplicated, encrypted

Hyper-V lab

Veeam ยท image backups โ†’ Wasabi

Hyper-V lab backup chain with Veeam Hyper-V checkpoint and VSS integration lets Veeam take application-consistent full image backups daily plus incrementals, kept hot in a local repo for roughly forty-five days of fast restore. Older restore points age out to a Wasabi capacity tier, encrypted client-side and stored deduplicated. on-site ยท hot tier snapshot age out encrypt โ†’ offsite Hyper-V lab VM hosts ยท checkpointsVSS app-consistent Veeam B&R full image ยท daily+ incrementals Local repo hot ยท fast restore~45-day retention Aging tier restore points > 45doffload to cloud Wasabi S3 client-encrypteddedup ยท at rest
full daily images + incrementals ยท hot ~45 days ยท aged points offload to Wasabi

Client-side + at rest

How the Encryption Works

Client-side encryption with encryption at rest restic and Veeam encrypt data on the host with keys that never leave it, so only ciphertext travels over TLS to Wasabi, which adds its own server-side encryption at rest and stores the data deduplicated. The provider never holds a key or sees plaintext. key stays client-side read ciphertext ยท TLS Wasabi only ever stores ciphertext, with no provider-held keys and no plaintext Plaintext data VMs ยท files ยท images Client-side encrypt AES-256 on hostbefore it leaves Wasabi ยท at rest server-side encryptiondedup ยท ciphertext only Encryption key restic / Veeamclient-held
dotted = key never leaves the host ยท provider stores ciphertext only

The Arch daily driver runs btrfs with a flat subvolume layout, and snapper keeps an automated ~30-day window of local snapshots. Pre/post-pacman and timeline snapshots make a bad upgrade a one-command rollback. A systemd-timed restic job then takes encrypted, file-level copies of /home and /data to a Synology NAS, which HyperBackup mirrors offsite to Wasabi.

The Proxmox cluster leans on Proxmox Backup Server for incremental, deduplicated, client-encrypted guest backups, with ZFS replication between nodes for warm standby; PBS and the offsite cloud PVE node both sync to Wasabi. The Hyper-V lab uses Veeam for application-consistent full image backups daily plus incrementals, kept hot locally for ~45 days before older restore points age out to a Wasabi capacity tier.

Privacy is non-negotiable: restic and Veeam encrypt client-side, so keys never leave the host and only ciphertext crosses the wire. Wasabi adds its own encryption at rest and stores everything deduplicated. The provider never holds a key or sees plaintext.

โŸจ RAID & Redundancy โŸฉ

Redundancy is the first line of defense. It keeps data online when a disk dies, before any backup is needed. Every tier is built to lose drives without losing data, all of it checksummed and scrubbed against silent corruption.

Built to lose disks

Storage Redundancy Tiers

Storage redundancy tiers A primary storage host runs a RAIDZ2 pool with double parity that survives two simultaneous drive losses; the rest of the cluster runs ZFS mirrors (RAID1) that survive one drive per vdev with fast resilver; and the Synology NAS runs SHR, with btrfs under the hood, for flexible mixed-drive redundancy with block checksums and bit-rot self-healing. Everything is checksummed and scrubbed to catch silent corruption. parity mirror hybrid Storage host 6ร— NVMe / SASone big pool RAIDZ2 double parity2 parity drives Survives 2 drives lost at onceno downtime Cluster nodes 2ร— NVMe eachper-node pool ZFS mirror ยท RAID1 full copy per vdevfast resilver Survives 1 drive per mirrorrebuild from twin Synology NAS mixed-size drivesbackup target SHR ยท btrfs flexible redundancyblock checksums Survives 1 drive + bit-rot detectionself-healing ZFS & btrfs checksum every block, and scheduled scrubs catch and repair silent corruption before it spreads
parity (RAIDZ2) mirror (RAID1) SHR / btrfs

The primary storage host runs a RAIDZ2 pool with ZFS double parity, so it can lose two drives at once and stay online, which matters most on the wide, high-capacity vdevs where a second disk can fail during a rebuild. The rest of the cluster runs ZFS mirrors (RAID1): each vdev keeps a full copy, so a failed drive resilvers fast from its twin with minimal performance hit, the right trade for the smaller NVMe pools backing VMs.

The Synology NAS uses SHR (Synology Hybrid RAID) with btrfs under the hood, which adds flexible redundancy across mixed-size drives (no wasted capacity from matching the smallest disk) plus btrfs block checksums and self-healing. Across all three, ZFS and btrfs checksum every block and scheduled scrubs catch and repair bit rot before it spreads. Redundancy isn't a substitute for the 3-2-1 backups above. It's what keeps everything serving while a disk gets replaced.

โŸจ GPU Compute & CUDA โŸฉ

CUDA workloads run on native hosts, GPU passthrough VMs, and containers. Model requirements and available capacity guide placement across the daily-use GPUs and Spark cluster.

Same toolchain, any context

CUDA, Delivered Three Ways

CUDA delivered three ways The same NVIDIA driver and CUDA toolkit reaches workloads three ways: bare-metal on the Arch desktop, inside a VFIO passthrough guest VM (the same mechanism Looking Glass uses), and inside containers via the NVIDIA Container Toolkit. All three paths converge on the same frameworks: PyTorch, vLLM, SGLang, and ComfyUI. native via VM via container Arch desktop RTX 5090bare-metal Native driver + toolkit nvidia + cudaruns on the metal VFIO guest VM passthrough GPUq35 ยท OVMF Guest CUDA stack driver inside the VMsame as bare-metal Container Docker / Podmanrootless NVIDIA Container Toolkit injects GPU + libsno driver in image CUDA workloads PyTorch ยท vLLMSGLang ยท ComfyUI
converges on one set of frameworks

Workload placement

CUDA Across the GPU Systems

CUDA across the Proxmox cluster Workloads are placed according to model requirements and available GPU capacity. The RTX 5090, RTX 4090, and two-node DGX Spark cluster are the primary daily systems. RTX 3070 and RTX 2060 resources support smaller workloads and CI. AMD and integrated GPUs handle work outside the CUDA stack. submit CUDA job train ยท inferbatch compute Workload placement model size ยท GPU memoryavailable capacity RTX 4090 node 24 GB ยท heavy compute RTX 3070 node 8 GB ยท mid inference 2ร— RTX 2060 nodes CI ยท light inference 2ร— DGX Spark 200 Gb/s ยท distributed serving RTX 5090 (desktop) daily inference ยท image/video AMD (RX 570) and iGPU-only nodes sit outside the CUDA pool and take non-CUDA work
NVIDIA / CUDA nodes DGX Spark pair

CUDA reaches workloads through three deployment paths: native execution on the Arch desktop, a VFIO passthrough guest with its own GPU driver, or containers with the NVIDIA Container Toolkit. PyTorch, vLLM, SGLang, and ComfyUI use the GPU through the software environment configured for each workload. Moving a workload still requires compatible drivers, dependencies, and GPU support.

The RTX 5090, RTX 4090, and two-node DGX Spark cluster are my primary daily compute systems. The smaller cards support lighter inference, testing, and CI. I allocate work around model memory requirements and available capacity; concurrent jobs still compete for the same physical GPU resources. AMD and integrated GPUs handle work outside the CUDA stack.

Container GPU sharing

NVIDIA Container Toolkit

The toolkit exposes GPU devices to containers on the Arch host or inside a GPU passthrough VM. Workload concurrency depends on the cardโ€™s available memory and compute capacity.

One GPU, many containers

Single GPU โ†’ Docker Fan-Out

One GPU shared by many containers via the NVIDIA Container Toolkit The Proxmox host blacklists nouveau and the nvidia driver and binds the card to vfio-pci, passing it whole into a Linux VM running the nvidia-open driver. Inside the VM, Docker and the NVIDIA Container Toolkit inject that single GPU into many containers at once, so PyTorch, vLLM or SGLang, ComfyUI, Jupyter and CI all share one physical card. passthrough GPU visible Proxmox host nouveau + nvidia blacklistedGPU โ†’ vfio-pci Linux VM nvidia-open driverq35 ยท OVMF Docker + Container Toolkit GPU + driver integration--gpus all PyTorch train / infer vLLM / SGLang LLM serving ComfyUI ยท SD image / video gen Jupyter ยท CI notebooks ยท tests Other GPU workloads capacity permitting
Docker + NVIDIA Container Toolkit all containers share one card

Driver placement

Where the GPU Driver Lives

Where the GPU driver lives across host, VM, and containers The physical GPU is bound to vfio-pci on the Proxmox host, which carries no GPU driver because nouveau and the nvidia driver are blacklisted. It passes through to a Linux VM that holds the one and only GPU driver, nvidia-open. Containers ship with no driver at all. The NVIDIA Container Toolkit injects the device and CUDA libraries at runtime, keeping the images portable. vfio-pci passthrough toolkit Physical GPU one card Proxmox host drivers blacklistedno GPU driver Linux VM nvidia-open driverโ˜… the only driver Containers no driver in imagelibs injected at run The GPU driver lives in exactly one place, the VM. The host stays out of the way; container images stay portable.

A Proxmox passthrough VM receives a physical GPU through vfio-pci and runs the NVIDIA driver inside the guest. Docker and the NVIDIA Container Toolkit then expose that GPU to selected containers. The same container pattern works directly on the Arch host for native GPU workloads.

The kernel driver stays on the container host, whether that host is a VM or a physical machine. Images carry application dependencies and their required CUDA runtime, while the toolkit provides device access and driver integration. Containers separate software environments; GPU memory and compute still need capacity planning.

AI / Request path

One gateway. Connected compute.

Hermes, Open WebUI, OpenCode, Jarvis, and applications reach model services through LiteLLM. The gateway routes requests; the inference endpoints run the models.

Centralized AI request routing Users, OpenCode, Jarvis and applications call LiteLLM. Hermes and LiteLLM run in separate Proxmox VMs. Open WebUI provides browser chat in an LXC container and sends model requests up to LiteLLM. LiteLLM routes API requests to GPU-backed vLLM endpoints. The DGX Spark pair is one distributed deployment with an API on the primary node. PROXMOX SERVICESGPU COMPUTE Clients Users ยท OpenCodeJarvis ยท Applications Hermes Agent runtime ยท VMTools & automation LiteLLM Central model gateway ยท VMScoped API routing Open WebUI Chat interface ยท LXC containerModel access through LiteLLM RTX 5090 workstation vLLM inference endpoint RTX 4090 system vLLM inference endpoint RTX 3070 vLLM inference endpoint 2 ร— RTX 2060 vLLM endpoints ยท secondary GPU CI 2 ร— DGX Spark ยท GB10 vLLM API on primary nodeWorker over dedicated RDMA Model requests Centralized AI request routing Users, OpenCode, Jarvis and applications call LiteLLM. Hermes and LiteLLM run in separate Proxmox VMs. Open WebUI provides browser chat in an LXC container and sends model requests up to LiteLLM. LiteLLM routes API requests to GPU-backed vLLM endpoints. The DGX Spark pair is one distributed deployment with an API on the primary node. Clients Users ยท OpenCode ยท JarvisApplications Hermes Agent runtime & automationSeparate Proxmox VM LiteLLM Central model gatewaySeparate Proxmox VM Open WebUI Chat interface ยท LXC containerModel access through LiteLLM RTX 5090 workstation vLLM inference endpoint RTX 4090 system vLLM inference endpoint RTX 3070 vLLM inference endpoint 2 ร— RTX 2060 vLLM endpoints ยท secondary GPU CI 2 ร— DGX Spark ยท GB10 vLLM API on primary nodeWorker over dedicated RDMA SELECTED vLLM ENDPOINT
API requests flow toward the selected backend; responses return to the client. Endpoint availability depends on the loaded workload.

Hermes

A separate agent VM manages context, instructions, memory, and tool orchestration. Model calls are one step in that workflow.

LiteLLM

The model gateway provides named routes and scoped API access. Inference stays on the selected vLLM backend, with SGLang also used in the serving stack.

Open WebUI

Browser chat runs in its own LXC container and sends model requests through the gateway. Tailscale provides private connectivity to services.

Distributed inference / DGX Spark

Two Sparks. One model deployment.

The primary node hosts the vLLM API and coordinates distributed inference with the worker. LiteLLM sends client requests to the primary; the pair exchanges inference data over a separate high-speed link.

A distributed model across two GB10 nodes Client API traffic enters the DGX Spark primary from LiteLLM. A separate bidirectional 200 gigabit per second RoCE RDMA connection joins the primary and worker. Each has 128 GB unified memory, for 256 GB aggregate across two nodes, not one transparent memory pool. LiteLLM Client API requests DGX Spark ยท Primary GB10 ยท vLLM API128 GB unified memory DGX Spark ยท Worker GB10 ยท distributed inference128 GB unified memory 200 Gb/s RoCE / RDMA 256 GB aggregate unified memory across two nodes A distributed model across two GB10 nodes Client API traffic enters the DGX Spark primary from LiteLLM. A separate bidirectional 200 gigabit per second RoCE RDMA connection joins the primary and worker. Each has 128 GB unified memory, for 256 GB aggregate across two nodes, not one transparent memory pool. LiteLLM Client API requests DGX Spark ยท Primary GB10 ยท vLLM API128 GB unified memory DGX Spark ยท Worker GB10 ยท distributed inference128 GB unified memory 200 Gb/s RoCE / RDMA
Cyan: client API traffic. Teal: the dedicated, bidirectional 200 Gb/s ConnectX-7 RoCE/RDMA link between the two Sparks.

Each Spark has 128 GB of unified memory, giving the pair 256 GB in aggregate. Distributed serving assigns model work to both nodes; the machines retain separate memory. Runtime overhead, model format, and context requirements determine usable capacity.

The ConnectX-7 connection is rated at 200 Gb/s, or 25 GB/s before overhead. RoCE carries RDMA traffic over Ethernet, and NCCL handles collective communication for distributed GPU work. That dedicated path carries node-to-node exchanges while the regular network carries client requests and management traffic. The rated link speed is separate from measured inference throughput.

NVIDIAโ€™s ConnectX-7 networking reference โ†—

Agent execution

Inference is one step in the work.

Hermes assembles context, calls the model through LiteLLM, and handles the next action. Tool permissions and approvals govern execution; results return to the agent for the next step.

The agent execution loop Hermes sends context to LiteLLM and a selected vLLM endpoint. Model output returns to Hermes, where permissions and approvals govern tool execution. Tool results return to the agent for the next step. Hermes Instructions ยท memory ยท context LiteLLM โ†’ vLLM Model request & response Tools & integrations Approved actions ยท results Execution controls Permissions ยท human review The agent execution loop Hermes sends context to LiteLLM and a selected vLLM endpoint. Model output returns to Hermes, where permissions and approvals govern tool execution. Tool results return to the agent for the next step. Hermes Instructions ยท memory ยท context LiteLLM โ†’ vLLM Model request & response Execution controls Permissions ยท human review Tools & integrations Approved actions ยท results
Model responses inform the workflow. Tool execution remains controlled by the agent and integration layer.

ComfyUI provides the separate image and video workflow layer, while n8n connects scheduled jobs and service integrations. These workloads use the same broader infrastructure without making every task an LLM request.

Explore the daily AI workflow โ†’

โŸจ Security โŸฉ

A distributed CrowdSec engine that turns a detection anywhere into enforcement everywhere, a FortiGate perimeter that consumes it, resilient DNS, and a management plane that lives entirely on Tailscale.

Distributed engine

Multi-Server CrowdSec

Detection ยท log processors

PVE / Linux hosts

parse logs ยท match scenarios

Edge nginx

bouncer + parser

Azure website

nginx bouncer ยท sensor + enforce

Central ยท Local API (single source of truth)

CrowdSec LAPI

10.0.0.23:8080 ยท Postgres ยท profiles โ†’ decisions ยท + CAPI & console lists

Enforcement ยท bouncers

FortiGate

external threat feed

nginx bouncers

edge + Azure

PVE firewall

nftables ipset (planned)

All processor โ†” LAPI โ†” bouncer traffic rides Tailscale Log processors share nothing directly. The LAPI unifies the estate

Detection โ†’ enforcement loop

Public Threat Feed Pipeline

Inputs

Edge agents / bouncers

over Tailscale

Azure site attacks

reported as sensor

CAPI + console lists

community blocklists

Engine

CrowdSec LAPI

union of all decisions ยท ~75k entries

Mirror ยท thallium (public DMZ)

blocklist-mirror :41412

bouncer ยท refresh 10s โ†’ nginx (wildcard cert)

Published

threat.cktechnology.io/crowdsec.txt

public ยท plaintext IP/CIDR list

Consumed

FortiGate external feed (pull)

address object โ†’ IPv4 DENY policy

An attack on the public Azure site is blocked at the FortiGate for every other service, closing the loop.

Resilient resolution

DNS + SD-WAN Failover

Clients

LAN clients

DNS forced by FortiGate

Resolvers (health-check failover)

Technitium

primary

Pi-hole

secondary

Recursion

Unbound

recursive ยท DNSSEC validating

FortiGate SD-WAN fails DNS over from one resolver to the other automatically.

Isolated by design (public)

Kasm Workspaces

Ephemeral Kali Linux

disposable pentest containers ยท external ranges (e.g. TryHackMe) over OpenVPN

Browser isolation

browser-in-a-browser ยท right-click โ†’ open in Kasm ยท safe link detonation

A public remote VM hosting a hacking and sandbox lab, kept isolated from the internal estate.

Remote support ยท split exposure

ScreenConnect (self-hosted on PVE1)

Self-hosted services

ScreenConnect

ConnectWise Control ยท PVE1 VM ยท RMM / remote support

Hudu

self-hosted docs & runbooks ยท Tailscale-only

Split exposure

Relay (screlay) ยท public

session traffic only ยท agents/clients connect in

Web / Admin ยท Tailscale only

management console never faces the internet

Guardrails + resilience

PVE firewall + nftables

DC / node / guest rules ยท admin hidden behind the tailnet

Cloud fallback

cktech.screenconnect.com ยท if the on-prem instance is down

Only the relay is public. The admin console is Tailscale-only, the same split-exposure pattern used across the estate.

Access Model ยท Everything Over Tailscale

๐Ÿ”— All infra control-plane comms (CrowdSec LAPI, Wazuh manager & indexer, Grafana, Prometheus, and SSH) ride the Tailscale mesh with strict ACLs and per-session SSH auth. Hosts firewall with nftables, SSH is key-only, and the only deliberate public surfaces are the threat-feed endpoint, the isolated Kasm VM, and the ScreenConnect session relay. Every admin console stays on the tailnet.

Security runs as a distributed CrowdSec deployment: log processors across the hosts and edge parse logs and ship alerts to a central LAPI, a Postgres-backed, LAPI-only Security Engine that is the single source of truth, while bouncers query it for decisions. Because the processors don't talk to each other, the central API is what makes a detection on one box actionable everywhere, and all of that traffic stays on Tailscale.

The engine's full decision union (local bans plus the CAPI community feed and console lists, around 75k entries) is mirrored on the public thallium box and served as a plaintext list at threat.cktechnology.io/crowdsec.txt, which the FortiGate pulls in as an external threat feed and enforces with a DENY policy. An attack on the public Azure website is reported by its bouncer, lands in the feed, and is blocked at the perimeter for every other service, closing the detection-to-enforcement loop. DNS is just as deliberate: Technitium and Pi-hole both front a validating Unbound resolver, with FortiGate SD-WAN failing over between them so name resolution survives a resolver going down.

โŸจ Observability ยท Heimdall โŸฉ

Two independent pipelines for logs and metrics, plus security overlays, unified in Grafana and reachable only over the tailnet.

Pipeline 1

Logs ยท syslog-ng โ†’ Loki

Senders

FortiGate

udp/tcp :5514 ยท key=value

RFC3164

udp/tcp :514

RFC5424

tcp :601

Ingest ยท syslog-ng (native)

syslog-ng

tag source_type / app / host ยท JSON-escape ยท disk-buffer

Store

Loki :3100

HTTP push ยท low-cardinality labels ยท 30d

/var/log/remote

raw file archive (redundant)

View

Grafana

Loki Explorer ยท FortiGate dashboards (LogQL logfmt)

Pipeline 2

Metrics ยท Exporters โ†’ Prometheus

Exporters (file-based SD)

node_exporter

host CPU/mem/disk/net

cAdvisor

per-container

Proxmox + edge

targets/*.yml ยท hot reload

Scrape & store

Prometheus :9090

15s scrape ยท 30d / 40GB

Outputs

Alertmanager :9093

infra.yml rules โ†’ routing

Grafana

node + cluster dashboards

Overlays

Security ยท CrowdSec & Wazuh

Sources (read-only, over Tailscale)

CrowdSec LAPI :6060

cs_* metrics โ†’ Prometheus

Wazuh indexer :9200

OpenSearch ยท wazuh-alerts-4.x

Unified pane

Grafana

CrowdSec Overview ยท Wazuh SIEM (MITRE)

Dashboards bound to the tailnet. Nothing publicly exposed

Heimdall runs two independent pipelines. Logs flow through a native syslog-ng that tags each stream with low-cardinality labels and pushes to Loki over HTTP, while also writing a raw file archive as a redundant copy; FortiGate gets a dedicated port and its key=value logs are parsed at query time with LogQL. Metrics are pull-based: Prometheus scrapes node_exporter, cAdvisor, and the Proxmox and edge hosts via file-based service discovery, so adding a target is a file edit and a hot reload, with alerts routed through Alertmanager.

Security telemetry rides alongside as read-only overlays, with CrowdSec LAPI metrics feeding Prometheus and the Wazuh indexer mounted as an OpenSearch datasource, so a single Grafana instance covers host health, cluster capacity, firewall traffic, and MITRE-mapped detections. Like the rest of the management plane, every piece of it is reachable only over Tailscale.

โŸจ Follow the Data โŸฉ

The same Heimdall pipelines as true flow graphs, with branches, merges, and labeled or observe-only edges, and animation tracing the direction each stream actually travels.

Flow 1

Logs ยท syslog-ng โ†’ Loki

Log pipeline: syslog senders to syslog-ng to Loki to Grafana FortiGate, RFC3164 and RFC5424 senders feed two syslog-ng sources, which tag and rewrite streams that merge into a JSON-escape step, then branch to the Loki HTTP destination and a redundant on-disk file archive. Loki is visualized in Grafana. syslog-ng (native) RFC3164 senders udp/tcp :514 RFC5424 senders tcp :601 FortiGate udp/tcp :5514 source s_net_standard source s_fortigate flags(no-hostname) rewrite r_tag_standard source_type=syslogapp=PROGRAM rewrite r_tag_fortigate source_type=fortigateapp=firewall rewrite r_loki_escape JSON-escape message destination d_loki http POST /loki/api/v1/pushbatch 300 / 2000msdisk-buffer 512MB reliable destination d_remote_files /var/log/remote/HOST/PROGRAM.log Loki :3100 Grafana Loki Explorer ยท FortiGate dashboards

Flow 2

Metrics ยท Exporters โ†’ Prometheus

Metrics pipeline: exporters to Prometheus to Alertmanager and Grafana Seven exporters fan in to Prometheus, with the CrowdSec LAPI scraped over a dotted observe-only edge. Prometheus evaluates infra.yml rules that route to Alertmanager, and feeds Grafana dashboards. node_exporter :9100 host CPU/mem/disk/net cAdvisor :8085 per-container Loki :3100 /metrics Prometheus self :9090 Proxmox exporters file-SD proxmox-*.yml Edge hosts file-SD node-*.yml CrowdSec LAPI 10.0.0.23:6060 Prometheus :9090 15s scrape ยท 30d / 40GB alerts/infra.yml TargetDown ยท NodeHighCpuNodeHighMemory ยท NodeDiskFillingUpCrowdsecScraperDown Grafana dashboards node + cluster panels Alertmanager :9093 infra.yml โ†’ routing
Dotted = observe-only scrape (CrowdSec LAPI)

Flow 3

Security Overlays ยท CrowdSec & Wazuh

Security overlays: CrowdSec and Wazuh into Grafana The CrowdSec LAPI is scraped by Prometheus for cs_* metrics and surfaced in a Grafana CrowdSec Overview, while the Wazuh indexer is read directly as an OpenSearch datasource for a Grafana Wazuh SIEM dashboard. Both sources are observe-only. External Prometheus scrape :6060cs_* metrics OpenSearch datasourcewazuh-alerts-4.x-YYYY.MM.DD CrowdSec LAPI 10.0.0.23 (central) Wazuh indexer 10.0.0.25 (OpenSearch) Prometheus Grafana CrowdSec Overview Grafana Wazuh SIEM (MITRE)
Read-only sources on the left feed Prometheus and Grafana on the right.

Flow 4

Network & Deployment Topology

Deployment topology: workstation repo to Heimdall over LAN and tailnet The repo on the workstation is rsynced to /opt/heimdall, brought up with docker compose as core containers, and its conf.d is copied to the native syslog-ng directory. The stack and syslog-ng live on the LAN, with the tailnet reaching the stack over a dotted overlay. Workstation (Arch) LAN 10.0.0.0/24 + 10.10.10.0/24 rsync (deploy.sh) docker compose up -d copy conf.d/* ~/arch/heimdall-stack source of truth /opt/heimdall on Heimdall Tailnet 100.x core containers docker compose stack /etc/syslog-ng/conf.d native
Dotted = overlay / copy (Tailnet ยท conf.d)

Need This Kind of Infrastructure?

From Proxmox clusters and FortiGate edges to private AI, distributed security, and full observability, I design and operate resilient systems end to end.