# Bumblesloth Homelab — System Reference
 
> **Purpose:** This file is shared project knowledge for all homelab-related conversations. It describes the full infrastructure so that any discussion (game servers, cloud, networking, etc.) has the context it needs.
 
---
 
## Hardware
 
| Component | Detail |
|-----------|--------|
| Machine | HP ProDesk 600 G6 SFF |
| CPU | Intel i5-10500 (6 cores / 12 threads) |
| RAM | 16 GB DDR4 UDIMM (288-pin desktop, not SO-DIMM) |
| Storage | 512 GB NVMe SSD (single drive) |
| GPU | Integrated Intel UHD 630 (no dedicated GPU) |
| Expansion | 1× free PCIe x16 slot, 1× 3.5" bay |
| Form factor | Small Form Factor (SFF) — limits internal expansion |
 
---
 
## Operating System
 
- **OS:** Ubuntu Server 24.04 LTS (HWE kernel, minimized install, no snaps)
- **User:** `bumblesloth`
- **Hostname:** `slothhive`
- **IPv6:** Disabled system-wide (`/etc/sysctl.d/99-disable-ipv6.conf`) and in Docker (`daemon.json`)
---
 
## Network Configuration
 
### Local Network
| Setting | Value |
|---------|-------|
| IP | `192.168.178.30` (DHCP with static lease via FRITZ!Box MAC binding) |
| Subnet | `192.168.178.x` |
| Netplan config | `/etc/netplan/50-cloud-init.yaml` (dhcp4: no — eno1 enslaved to br0) + `/etc/netplan/99-bridge.yaml` |
| Network bridge | `br0` — bridges `eno1` for VM LAN access; inherits `eno1` MAC (`e8:d8:d1:d3:05:bd`) |
 
### Router
| Setting | Value |
|---------|-------|
| Current router | FRITZ!Box 5690 Pro (Deutsche Telekom fiber, SFP module) |
| DHCP DNS | AdGuard Home pushed via DHCP to all LAN devices (`192.168.178.30`) |
| DynDNS | Configured directly on FRITZ!Box — auto-updates ipv64.net on IP change |
 
### Public Access
| Setting | Value |
|---------|-------|
| Public IP | Dynamic (DynDNS managed) |
| DynDNS provider | ipv64.net |
| Base domain | `bumblesloth.ipv64.net` |
| A record | `bumblesloth.ipv64.net` → public IP (auto-updated by FRITZ!Box) |
 
### DNS — AdGuard Home
- **Wildcard rewrite:** `*.bumblesloth.ipv64.net → 192.168.178.30` (covers all subdomains on LAN)
- **Desktop DNS (CachyOS):** received automatically via FRITZ!Box DHCP — no manual override needed
- All devices on LAN receive AdGuard Home as DNS via FRITZ!Box DHCP
### DNS Records (CNAME → bumblesloth.ipv64.net)
| Subdomain | Service |
|-----------|---------|
| `cloud.bumblesloth.ipv64.net` | Nextcloud |
| `photos.bumblesloth.ipv64.net` | Immich |
| `actual-budget.bumblesloth.ipv64.net` | Actual Budget |
| `adguard.bumblesloth.ipv64.net` | AdGuard Home |
| `mealie.bumblesloth.ipv64.net` | Mealie |
| `git.bumblesloth.ipv64.net` | Gitea |
| `foundry.bumblesloth.ipv64.net` | Foundry VTT |
| `matrix.bumblesloth.ipv64.net` | Matrix/Synapse |
| `jellyfin.bumblesloth.ipv64.net` | Jellyfin |
| `paperless.bumblesloth.ipv64.net` | Paperless-NGX |
| `homeassistant.bumblesloth.ipv64.net` | Home Assistant |
 
### Firewall (UFW)
```
22/tcp        — SSH
80/tcp        — HTTP (Nginx Proxy Manager)
443/tcp       — HTTPS (Nginx Proxy Manager)
222/tcp       — Gitea SSH
53/tcp+udp    — AdGuard Home DNS
2456:2458/udp — Valheim
15636/udp     — Enshrouded (query)
15637/udp     — Enshrouded (game)
ufw route allow in on br0  — VM bridge traffic (required for KVM VM LAN access)
ufw route allow out on br0 — VM bridge traffic (required for KVM VM LAN access)
```
 
### Router Port Forwarding
| Port(s) | Protocol | Target | Service |
|---------|----------|--------|---------|
| 80 | TCP | 192.168.178.30 | NPM (HTTP) |
| 443 | TCP | 192.168.178.30 | NPM (HTTPS) |
| 222 | TCP | 192.168.178.30 | Gitea SSH |
| 2456–2458 | UDP | 192.168.178.30 | Valheim |
| 15636 | UDP | 192.168.178.30 | Enshrouded (query) |
| 15637 | UDP | 192.168.178.30 | Enshrouded (game) |
 
### Remote Access
- **Tailscale** mesh VPN installed on slothhive, iPhone, and MacBook; MagicDNS enabled for hostname-based access
- **CachyOS desktop:** `/etc/hosts` entry `192.168.178.30 slothhive` for local SSH
---
 
## KVM Virtualisation
 
Home Assistant OS runs as a KVM virtual machine on slothhive, alongside Docker services.
 
### Installation
```bash
sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients virtinst bridge-utils ovmf
sudo usermod -aG libvirt bumblesloth
```
 
### Network Bridge
The VM needs a bridge interface to get its own LAN IP. Configured via `/etc/netplan/99-bridge.yaml`:
```yaml
network:
  version: 2
  ethernets:
    eno1:
      dhcp4: false
  bridges:
    br0:
      interfaces: [eno1]
      dhcp4: true
      macaddress: e8:d8:d1:d3:05:bd
      parameters:
        stp: false
        forward-delay: 0
```
`/etc/netplan/50-cloud-init.yaml` sets `eno1` to `dhcp4: false` to avoid conflict.
 
### Bridge Kernel Settings
```
/etc/sysctl.d/99-bridge.conf:
  net.bridge.bridge-nf-call-iptables = 0
  net.bridge.bridge-nf-call-ip6tables = 0
 
/etc/modules-load.d/br_netfilter.conf:
  br_netfilter
 
/etc/sysctl.d/99-ipforward.conf:
  net.ipv4.ip_forward = 1
```
 
### Common virsh Commands
```bash
virsh list --all          # List all VMs and state
virsh start hass          # Start HA VM
virsh shutdown hass       # Graceful shutdown
virsh destroy hass        # Force stop
virsh reboot hass         # Reboot
virsh autostart hass      # Enable autostart on boot
virsh dominfo hass        # VM details
virsh domifaddr hass      # VM IP address
virsh attach-device hass --file device.xml --persistent  # Attach USB device
```
 
---
 
## Docker Infrastructure
 
### Installation
- Docker CE from official Docker repository (not distro packages)
- Docker Compose plugin (`docker compose`, not `docker-compose`)
- User `bumblesloth` in `docker` and `www-data` groups
### Networks
| Network | Type | Purpose |
|---------|------|---------|
| `proxy-net` | External, user-defined | Connects NPM to all web-facing services |
| `nc-internal` | Bridge (Nextcloud stack) | Isolates Nextcloud DB/Redis |
| `paperless-internal` | Bridge (Paperless stack) | Isolates Paperless DB |
| `matrix-internal` | Bridge (Matrix stack) | Isolates Synapse DB |
| `default` (per stack) | Auto-created | Internal communication within a compose stack |
 
> **Security model:** Services behind NPM on `proxy-net` don't need UFW rules or host port exposure — traffic flows 443/80 → NPM → container internally.
 
### Directory Structure
```
~/homelab/
├── nginx-proxy-manager/    # Reverse proxy + SSL
│   ├── docker-compose.yml
│   ├── data/
│   └── letsencrypt/
├── adguard-home/           # DNS resolver + ad blocking
│   └── docker-compose.yml
├── nextcloud/              # File sync & collaboration
│   ├── docker-compose.yml
│   ├── app-data/
│   ├── db-data/            # MariaDB data
│   └── user-data/
├── immich/                 # Photo & video management
│   ├── docker-compose.yml
│   ├── .env
│   ├── library/
│   └── postgres/
├── paperless/              # Document management
│   ├── docker-compose.yml
│   └── db-data/            # PostgreSQL data
├── jellyfin/               # Media server
│   └── docker-compose.yml
├── mealie/                 # Recipe manager
│   └── docker-compose.yml
├── gitea/                  # Self-hosted Git
│   ├── docker-compose.yml
│   └── data/
├── actual-budget/          # Personal finance
│   ├── docker-compose.yml
│   └── data/
├── foundry/                # Foundry VTT
│   └── docker-compose.yml
├── matrix/                 # Matrix/Synapse chat
│   └── docker-compose.yml
├── enshrouded/             # Enshrouded game server
│   ├── docker-compose.yml
│   └── enshrouded-data/
├── valheim/                # Valheim game server
│   ├── docker-compose.yml
│   ├── config/
│   │   ├── worlds_local/
│   │   ├── backups/
│   │   └── bepinex/
│   │       └── plugins/
│   └── data/
└── cups/                   # AirPrint bridge for Epson ET-2810
    └── docker-compose.yml
```
 
### Homelab Management Script
`/usr/local/bin/homelab` — supports `up`, `down`, `status`
 
**Design:** AdGuard Home and NPM are hardcoded as first/last. All other Docker services live in the `STACKS` array. The Home Assistant VM is managed separately via `virsh` commands within the same script.
 
**Startup order:** adguard-home → nginx-proxy-manager (3s delay) → HA VM → STACKS in order  
**Shutdown order:** STACKS in reverse → HA VM → nginx-proxy-manager → adguard-home
 
### Auto-start on Boot
- **Docker services:** Systemd service at `/etc/systemd/system/homelab.service` — enabled, runs `homelab up` after Docker is ready on every boot.
- **HA VM:** `virsh autostart hass` — VM starts independently on boot via libvirt.
---
 
## Services
 
### Nginx Proxy Manager
| Setting | Value |
|---------|-------|
| Container | `jc21/nginx-proxy-manager:latest` |
| Ports | 80 (HTTP), 443 (HTTPS), 81 (admin, local only) |
| Network | `proxy-net` |
| Admin UI | `http://192.168.178.30:81` |
| Features | SSL via Let's Encrypt, Force SSL, HTTP/2, Block Common Exploits, WebSockets |
 
All web services are proxied through NPM. Game servers use direct UDP port forwarding, not NPM.
 
### AdGuard Home
| Setting | Value |
|---------|-------|
| Role | LAN DNS resolver, ad/tracker blocking |
| URL | `https://adguard.bumblesloth.ipv64.net` |
| Admin UI | `http://192.168.178.30:3000` |
| Port | 53 (bound to host) |
| Location | `~/homelab/adguard-home/` |
| Network | `proxy-net` |
| Wildcard rewrite | `*.bumblesloth.ipv64.net → 192.168.178.30` |
 
### Home Assistant
| Setting | Value |
|---------|-------|
| Role | Home automation, smart home integration |
| URL | `https://homeassistant.bumblesloth.ipv64.net` |
| Type | **KVM Virtual Machine** (not Docker) — runs Home Assistant OS 18.0 |
| VM name | `hass` |
| VM IP | `192.168.178.43` (DHCP via FRITZ!Box) |
| VM disk | `/var/lib/libvirt/images/haos.img` (64 GB) |
| VM RAM | 4096 MB |
| VM vCPUs | 2 |
| NPM forward | `192.168.178.43:8123` (by IP, not container name) |
| NPM features | WebSockets enabled, Force SSL |
| Autostart | `virsh autostart hass` |
 
**ZBT-2 USB Passthrough:**
| Setting | Value |
|---------|-------|
| Device | Nabu Casa ZBT-2 Thread/Matter border router |
| USB ID | `303a:831a` |
| Host path | `/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_94A990D07314-if00` → `/dev/ttyACM0` |
| Passthrough method | `virsh attach-device hass ~/zbt2.xml --persistent` |
| Mode | Thread border router (OpenThread Border Router add-on in HA OS) |
 
**ZBT-2 passthrough XML (`~/zbt2.xml`):**
```xml
<hostdev mode='subsystem' type='usb' managed='yes'>
  <source>
    <vendor id='0x303a'/>
    <product id='0x831a'/>
  </source>
</hostdev>
```
 
**Integrations configured:**
- LG webOS TV (`192.168.178.22`) — added manually
- iPhone Companion app — connected via `https://homeassistant.bumblesloth.ipv64.net`
- ZBT-2 Thread border router — OpenThread Border Router add-on
**Notes:**
- HA OS runs as a KVM VM because the OpenThread Border Router add-on requires HA OS (not available in HA Container)
- VM uses `br0` bridge for LAN access — gets its own IP directly from FRITZ!Box DHCP
- NPM proxies to VM by IP (`192.168.178.43`) since the VM is not on `proxy-net`
- `trusted_proxies` config not needed in HA OS (handled differently than HA Container)
- mDNS auto-discovery doesn't work across Docker bridge — iOS Companion app must use manual URL
### Nextcloud
| Setting | Value |
|---------|-------|
| Role | Documents, file sync, collaboration |
| URL | `https://cloud.bumblesloth.ipv64.net` |
| Containers | `nextcloud-app` (Nextcloud), `nextcloud-db` (MariaDB), `nextcloud-redis` (Redis) |
| Networks | `proxy-net`, `nc-internal` |
| NPM forward | `nextcloud-app:80` |
| PHP memory | 1024M (`/usr/local/etc/php/conf.d/memory-limit.ini`) |
 
**Users:** root (admin, no data), kevin
 
**Cron:**
```
*/5 * * * * docker exec -u www-data nextcloud-app php -f /var/www/html/cron.php
```
Background jobs set to "Cron" in admin settings.
 
**Notes:**
- iOS/macOS CalDAV login can trigger brute-force protection; reset with `occ security:bruteforce:reset <ip>`
- `bumblesloth` user is in `www-data` group for host-level access to user-data directory
- Passwords in docker-compose.yml contain `$` special characters — escaped as `$$` to prevent Docker Compose variable interpolation warnings
### Immich
| Setting | Value |
|---------|-------|
| Role | Photo & video management, AI tagging, face recognition |
| URL | `https://photos.bumblesloth.ipv64.net` |
| Containers | `immich_server`, `immich_machine_learning`, `immich_redis`, `immich_postgres` |
| Networks | `proxy-net` + `default` (server), `default` only (others) |
| NPM forward | `immich_server:2283` |
 
**Users:** Kevin, mother
 
### Paperless-NGX
| Setting | Value |
|---------|-------|
| Role | Document management, OCR, tagging |
| URL | `https://paperless.bumblesloth.ipv64.net` |
| Containers | `paperless`, `paperless-db` (PostgreSQL), `paperless-redis`, `paperless-gotenberg`, `paperless-tika`, `paperless-gpt`, `ollama` |
| Networks | `proxy-net`, `paperless-internal` |
| NPM forward | `paperless:8000` |
 
**Backup:** PostgreSQL dump + document export → rsync to USB (`/mnt/backup-usb`, btrfs) nightly at 2 AM  
**Manual sync to Proton Drive:** Fish function `paperless-backup` on CachyOS desktop (rclone automation pending upstream fix — PRs #9080/#9081)
 
**OCR workflow:**
- Automated metadata (title, tags, date) via paperless-gpt + `qwen2.5:7b`
- Manual OCR for difficult documents via Open WebUI + `llama3.2-vision:11b`; PDFs converted with `pdftoppm` (poppler)
- Claude (vision) as fallback for manual transcription
### Jellyfin
| Setting | Value |
|---------|-------|
| Role | Media streaming |
| URL | `https://jellyfin.bumblesloth.ipv64.net` |
| Network | `proxy-net` |
| NPM forward | `jellyfin:8096` |
| Hardware accel | Intel QSV via `/dev/dri` (Intel UHD 630) |
| Media path | `/srv/media/` (host mount) |
 
### Mealie
| Setting | Value |
|---------|-------|
| Role | Recipe manager |
| URL | `https://mealie.bumblesloth.ipv64.net` |
| Network | `proxy-net` |
| Backend | SQLite |
 
### Gitea
| Setting | Value |
|---------|-------|
| Role | Self-hosted Git |
| URL | `https://git.bumblesloth.ipv64.net` |
| Network | `proxy-net` |
| Backend | SQLite |
| SSH | Port 222 (UFW + FRITZ!Box port forwarding configured) |
| NPM forward | `gitea:3000` |
| Features | WebSockets enabled, Block Common Exploits enabled |
 
### Actual Budget
| Setting | Value |
|---------|-------|
| Role | Personal finance (replaced Firefly III) |
| URL | `https://actual-budget.bumblesloth.ipv64.net` |
| Network | `proxy-net` |
| NPM forward | `actual-budget:5006` |
 
### Foundry VTT
| Setting | Value |
|---------|-------|
| Role | TTRPG virtual tabletop |
| URL | `https://foundry.bumblesloth.ipv64.net` |
| Network | `proxy-net` |
| NPM | WebSockets must be enabled in NPM proxy host |
 
### Matrix / Synapse
| Setting | Value |
|---------|-------|
| Role | Private self-hosted chat |
| URL | `https://matrix.bumblesloth.ipv64.net` |
| Server name | `matrix.bumblesloth.ipv64.net` (permanent — chosen to avoid .well-known delegation) |
| User IDs | `@kevin:matrix.bumblesloth.ipv64.net` |
| Containers | `synapse`, `matrix-db` (PostgreSQL), `coturn` |
| Networks | `proxy-net`, `matrix-internal` |
| NPM | WebSockets must be enabled |
| Federation | Disabled |
| Registration | Manual only |
 
### Enshrouded (Game Server)
| Setting | Value |
|---------|-------|
| Image | `mornedhels/enshrouded-server:latest` |
| Container | `enshrouded` |
| Ports | UDP 15636 (query), UDP 15637 (game) |
| Backups | Every 6 hours, max 4 kept |
| Max players | 8 |
| Update cron | Mondays at 4 AM |
 
**Force update:** `docker compose exec enshrouded supervisorctl start enshrouded-force-update`  
**Note:** Server version may lag behind client after patches — Keen Games publishes dedicated server updates separately. Use force update command if version mismatch occurs.
 
### Valheim (Game Server)
| Setting | Value |
|---------|-------|
| Image | `ghcr.io/lloesche/valheim-server` |
| Container | `valheim` |
| Ports | UDP 2456–2458 |
| Mod framework | BepInEx (disabled — glibc incompatibility; vanilla only) |
| Auto-update | Only when idle |
| Backups | Every 6 hours, max 10 kept, max 7 days |
| Public | false (private server) |
| World | `BumbleWorld` (in `./config/worlds_local/`) |
 
**World switching:** Change `WORLD_NAME` in docker-compose.yml, run `docker compose down && docker compose up -d`.  
**Adding mods:** Drop `.dll` files in `./config/bepinex/plugins/`, restart container.
 
### CUPS / AirPrint Bridge
| Setting | Value |
|---------|-------|
| Role | AirPrint bridge for Epson ET-2810 printer |
| Image | `drpsychick/airprint-bridge:latest` |
| Container | `cups` |
| Location | `~/homelab/cups/` |
 
---
 
## Common Commands
 
### Docker
```bash
docker ps                        # List running containers
docker compose up -d             # Start stack (from its directory)
docker compose down              # Stop stack
docker compose logs -f           # Follow logs
docker compose restart           # Restart stack
docker stats                     # Live resource usage
```
 
### Homelab Script
```bash
homelab up       # Start all services in order
homelab down     # Stop all services in reverse order
homelab status   # Show status of all stacks + HA VM
```
 
### KVM / Home Assistant VM
```bash
virsh list --all                 # Show all VMs and state
virsh start hass                 # Start HA VM
virsh shutdown hass              # Graceful shutdown
virsh reboot hass                # Reboot VM
virsh domifaddr hass             # Show VM IP address
virsh dominfo hass               # Show VM details
virsh attach-device hass ~/zbt2.xml --persistent  # Attach ZBT-2 USB dongle
```
 
### Enshrouded
```bash
docker compose exec enshrouded supervisorctl start enshrouded-force-update  # Force game update
docker compose exec enshrouded supervisorctl restart enshrouded-server       # Restart server
```
 
### Nextcloud (OCC)
```bash
docker exec -u www-data nextcloud-app php occ [command]
# Useful commands:
#   user:list, user:add
#   files:scan --all / files:scan <user>
#   files:cleanup
#   maintenance:mode --on / --off
#   maintenance:repair
#   trashbin:cleanup --all-users
#   app:list, app:enable <app>, app:disable <app>
#   security:bruteforce:reset <ip>
```
 
### System
```bash
df -h /                    # Disk usage
htop                       # System resources
sudo ufw status            # Firewall rules
crontab -l                 # Scheduled tasks
rsync -avh --progress      # File transfers to slothhive over SSH
```
 
---
 
## Known Issues & Notes
 
1. **IPv6 disabled** — Docker Hub pulls fail over IPv6. Disabled system-wide in sysctl and in Docker daemon.json.
2. **BepInEx glibc error** — BepInEx doorstop requires GLIBC 2.33+; container has older version. Valheim runs vanilla.
3. **Nextcloud brute force** — iOS/macOS app login can trigger rate limiting. Reset with `occ security:bruteforce:reset <ip>`. Use app passwords if 2FA is enabled.
4. **Hairpin NAT** — Accessing public-facing services via domain from inside the LAN may not work on some routers. AdGuard wildcard rewrite handles this on the LAN.
5. **Single SSD** — All data on one 512 GB NVMe. Future plan: NAS for bulk storage, keep databases on SSD.
6. **Proton Drive rclone automation** — Blocked by upstream bug (PRs #9080/#9081 unmerged). Paperless backups synced manually via `paperless-backup` Fish function on CachyOS desktop.
7. **Matrix server name is permanent** — `matrix.bumblesloth.ipv64.net` used directly; changing it would require a full rebuild.
8. **WebSockets** — Must be explicitly enabled in NPM for Foundry VTT and Matrix/Synapse (and Home Assistant).
9. **DDR4 UDIMM (288-pin)** — HP ProDesk 600 G6 SFF requires standard desktop DIMMs, not SO-DIMM. Important for future RAM upgrades.
10. **DynDNS auto-update** — FRITZ!Box handles updates automatically, but after a power outage verify the public IP matches ipv64.net if services are unreachable externally.
11. **Nextcloud DB passwords** — Contain `$` special characters; escaped as `$$` in docker-compose.yml to avoid Docker Compose variable interpolation warnings.
12. **HA OS requires KVM VM** — The OpenThread Border Router add-on (required for ZBT-2 Thread mode) is only available in HA OS, not HA Container. KVM VM coexists with Docker on the same host without conflict.
13. **HA VM network** — VM gets its IP via DHCP from FRITZ!Box through the `br0` bridge. NPM proxies to the VM by IP (`192.168.178.43`), not container name. If the VM IP changes, update NPM proxy host.
14. **br0 bridge required for VM** — KVM VM LAN access requires `br0` network bridge. UFW must allow routed traffic on `br0` (`ufw route allow in/out on br0`). Bridge netfilter must be disabled (`bridge-nf-call-iptables = 0`) to avoid Docker iptables conflicts.
15. **HA VM disk size** — VM disk expanded to 64 GB (from default 32 GB) to accommodate HA Core Docker image download during initial setup.
16. **mDNS limitations** — Auto-discovery (for iOS Companion app, LG TV, etc.) does not work across Docker bridge or into the KVM VM from LAN devices. Use manual IP/URL for all HA integrations.
---
 
## Future Plans
 
- [ ] Proton Drive rclone automation once upstream PRs merge
- [ ] NAS integration (NFS mount for bulk media/backup storage)
- [ ] Resolve BepInEx GLIBC compatibility for Valheim modding
- [ ] Navidrome (music streaming)
- [ ] Confirm macOS CalDAV resolution (brute force reset + app password)
- [ ] Add IKEA Matter over Thread devices via ZBT-2 border router
