# 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
```
**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 `
- `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
# files:cleanup
# maintenance:mode --on / --off
# maintenance:repair
# trashbin:cleanup --all-users
# app:list, app:enable , app:disable
# security:bruteforce:reset
```
### 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 `. 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