← Case studies

Case study 03 · 5 min read

The Homelab Platform

One hypervisor, six VMs, four domains and about 90 services, run by me

Role
Designed, built and operated it myself
When
~2021 – present
Stack
XCP-ngXen OrchestranginxLet's EncryptCloudflareDocker ComposeKubuntuSambaNFSGiteaDozzlentfyVaultwarden

Addresses, topology details and credentials are left out on purpose. The config samples below are sanitized.

TL;DR
  • One XCP-ng host runs six VMs, each with a single job: edge proxy, two app hosts, GPU, NAS and the hypervisor's own management VM.
  • A dedicated nginx VM terminates TLS for ~130 virtual hosts across four domains with 91 Let's Encrypt certificates, plus raw TCP proxying.
  • Every project runs in its own Docker Compose stack with its own network and database, so a broken migration or a bad app stays contained.

The problem

I've been running my own servers for about five years. It started with game servers for friends, then a single Ubuntu box serving PHP sites straight out of /var/www. Every new project added one more thing to that box, until "the server" was a list of things that would all break together.

What I wanted:

  • a place to run everything I build and use, from production web apps to GPU workloads
  • clear boundaries, so one project's mistake stays inside that project
  • enough structure that adding the next site is routine instead of an evening of hand-editing configs

This page is the platform as it stands today. The evolution timeline covers how it got here.

~90
self-hosted services
~130
nginx virtual hosts
91
Let's Encrypt certificates
4
domains

Architecture

How a request reaches an app
Visitor
then
Cloudflare DNS, edge TLS
then
Proxy VM nginx, ~130 vhosts
then
App VM Docker Compose stack
then
App container
Proxy VM
then
TCP stream proxy git over SSH, game servers
then
Service VM
What runs on the one hypervisor host
XCP-ng host Xeon E5-2620 v3 · 128 GB DDR4
then
Proxy VM nginx + certbot
XCP-ng host
then
dylanbanta.com VM Docker Compose stacks
XCP-ng host
then
sigmusdewn.com VM Docker Compose stacks
XCP-ng host
then
GPU VM 3060 Ti + 2× Tesla M60 passthrough
XCP-ng host
then
NAS VM Kubuntu · Samba + NFS
XCP-ng host
then
XOA VM Xen Orchestra

The host

One physical server runs XCP-ng:

  • a 2014-era Intel Xeon E5-2620 v3
  • 128 GB of DDR4
  • three GPUs passed through to a single VM: an RTX 3060 Ti for AI workloads (ComfyUI, Open WebUI) and two Tesla M60s that handle video encoding

Xen Orchestra runs in its own VM to manage the host. I have other machines, but this is the one that matters.

The VMs

Each VM has one job:

VM Job
Proxy VM The edge. nginx terminates TLS and routes every subdomain to the right backend
dylanbanta.com VM Docker Compose stacks for dylanbanta.com projects
sigmusdewn.com VM Docker Compose stacks for sigmusdewn.com projects
GPU VM Everything that needs a GPU, whichever domain it belongs to
NAS VM Samba shares for Windows machines, NFS exports for Linux machines and the other VMs
XOA VM Xen Orchestra, the management plane for the hypervisor

Splitting the app hosts by domain means a bad deploy or a runaway container on one side doesn't take down the other. Putting every GPU workload on one VM means the passthrough only has to be configured once.

The edge

All public traffic goes through Cloudflare to one nginx VM. It holds:

  • ~130 virtual hosts across four domains (sigmusdewn.com, dylanbanta.com, binaryprosperity.com, and an older dylanbanta.com set I'm retiring)
  • 91 Let's Encrypt certificates, issued and renewed by certbot
  • TCP stream proxying for non-HTTP services, such as git over SSH and game servers
  • a WebSocket upgrade map for services that keep long-lived connections, like push notifications
  • a few vhosts behind basic auth, and a proxy cache

On September 29, 2026 I removed TLS 1.0 and 1.1 globally and checked every vhost afterwards. The edge now accepts only TLS 1.2 and 1.3.

One detail I like: the proxy VM's /etc/nginx is a symlink to a folder on the NAS, mounted over NFS. That folder is the live configuration, not a backup. Every app host and my workstation can read the same config, and the provisioning tools I built write vhosts there directly.

Storage

The NAS is a Kubuntu VM sitting on the host's hardware RAID array (about 6 TB usable with parity). It exposes three 2 TB areas:

  • the Luterra project data
  • a public share
  • the server data that the app VMs mount

A separate 1 TB disk is shared as a network-wide Users folder, so my Documents and other folders stay in sync across Windows and Linux machines.

Networking

I'll keep this general on purpose. The network uses VLANs for segmentation, WireGuard for remote access, and Cloudflare Tunnel (cloudflared) where a service shouldn't be reachable any other way.

Sample configuration

A typical proxied vhost. Real names and ports are replaced with placeholders, but the shape matches production:

server {
    server_name app.example.com;

    location / {
        proxy_pass http://app-host:10xxx/;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }

    location /admin/ {
        # Admin tools are LAN-only: allow the LAN range, deny all
        include snippets/lan-only.conf;
        proxy_pass http://app-host:10yyy/;
    }

    listen 443 ssl;
    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
}

server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}

The global TLS policy after the September cleanup:

# http {} block
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

Raw TCP services go through the stream module instead of an HTTP vhost:

stream {
    # git over SSH to the Gitea host
    server {
        listen 222;
        proxy_pass git-host:22xx;
    }

    # a game server
    server {
        listen 25565;
        proxy_pass game-host:25xxx;
    }
}

Key decisions

XCP-ng over the alternatives

I wanted a type-1 hypervisor that I could manage from a web UI and script against, without a license wall in front of features I'd actually use. XCP-ng with Xen Orchestra gave me that, and it has been dependable for years.

A self-built NAS instead of TrueNAS

I ran TrueNAS (both CORE and SCALE) for a while, then replaced it with a plain Kubuntu VM running Samba and NFS. TrueNAS is good software, but I didn't like how much it hid from me or its appliance-style limits. I wanted to own the configuration and know exactly what every share was doing.

If I built it again, I'd use Ubuntu Server without a desktop environment. The desktop doesn't cost much, but it's nothing the NAS needs.

One proxy VM in front of everything

Every public request goes through the same place, so:

  • TLS policy is enforced in one spot (the TLS 1.0/1.1 removal was one change, then a check of every vhost)
  • certificates are managed in one spot
  • I can see every route the platform exposes in one directory

Each project in its own Compose stack

Every project gets its own Docker Compose stack with its own network, bind-mounted volumes and its own database. A broken migration or a compromised app stays inside its stack. This is also what made automated provisioning possible later: once every site has the same shape, a template can generate the next one. See dlt and dev-llc.

My own toolchain

I run my own ops toolchain:

  • two Gitea instances for source control
  • Dozzle for container logs, behind a docker-socket-proxy so the log viewer never gets raw Docker socket access
  • ntfy for push alerts
  • osTicket for tracking requests
  • Vaultwarden for secrets, with update and backup scripts
  • Dashy and Portainer for at-a-glance views

What broke and how I fixed it

  • Old TLS versions were still enabled. An audit for this portfolio found TLS 1.0 and 1.1 still turned on globally. I disabled them, then checked every vhost, because a vhost can override the global setting.
  • Lots of small outages. Running about 90 services for five years means fixing a steady stream of small problems: containers that don't come back after a reboot, certificates that fail to renew, a mount that wasn't ready when a stack started. None of them make a good story on their own. Together they're most of what I know about operations.

Also running

Not everything here is something I built. Plenty of it is good existing software, run well. Those are listed in the self-hosted apps section below.

Self-hosted apps

Productivity & ops

I build from scratch a lot, but I also know when an existing tool is the right call.

  • n8n: workflow automation, with pgvector and Redis
  • Gitea: two instances, one per domain
  • Vaultwarden: password vault, with update and backup scripts
  • Dozzle: container logs, behind a docker-socket-proxy
  • ntfy: push notifications for my own services
  • osTicket: helpdesk, wired to ntfy
  • Portainer: container management
  • Dashy: homelab dashboard
  • Excalidraw: self-hosted, plus a fork with a storage backend
  • Kanboard: project boards
  • Nextcloud: file sync
  • Cal.com: consultation booking
  • Umami: privacy-friendly analytics
  • Actual: budgeting
  • Habitica: habit tracking
  • VERT: file conversion in the browser
  • Webtop: a desktop in the browser
  • Guacamole: browser-based remote desktop
  • Remotely: remote support
  • Jupyter: notebooks

Media & books

  • AzuraCast: internet radio
  • Navidrome: music streaming
  • MusicBrainz Picard: music tagging
  • Download pipeline: automated music downloads that feed my dj app
  • Calibre: ebook library
  • Kavita: reading server
  • Audiobookshelf: audiobooks

Game servers

Years of operating multi-user services for real players: mods, backups, CVE patching and TCP proxying.

  • Minecraft: my own modpack, run for years
  • FiveM / QBCore: GTA V roleplay server
  • SCP: Secret Laboratory: dedicated server
  • Foundry VTT: tabletop RPG server

History

  • WordPress: several sites, 2022–24, before I moved to Laravel