home-serv — Infrastructure Reference
Primary storage and services host. Runs the container fleet, the AI/ML stack, the bulk storage array, and serves as the backup target for the environment.
Host
| Hostname | home-serv |
| OS | Ubuntu 22.04 LTS |
| CPU | Intel i5-12600K (16 threads) |
| RAM | 32 GB |
| GPU | Intel Arc A770 16 GB |
| LAN | 10.0.4.200 |
| Root | /dev/nvme1n1p2 — 1.9 TB (/boot on root, not a separate partition) |
| Bulk NVMe | /dev/nvme0n1p1 → /mnt/storage, 1.9 TB |
| Container management | Dockge, stacks under /home/jeremy/docker/stacks/ |
Kernel tuning
Set permanently via /etc/sysctl.d/:
vm.swappiness=10vm.max_map_count=262144
Docker daemon
/etc/docker/daemon.json:
{
"dns": ["1.1.1.1", "8.8.8.8"],
"bip": "172.19.0.1/24",
"default-address-pools": [
{ "base": "172.20.0.0/16", "size": 24 },
{ "base": "172.21.0.0/16", "size": 24 },
{ "base": "172.22.0.0/16", "size": 24 }
]
}One external Docker network is created manually outside any stack:
docker network create ai-netai-net carries qdrant, librechat-rag-api, and librechat.
Storage architecture
Backup array
| Device | /dev/md0 — RAID 1 mirror |
| Members | 2 × 6 TB WDC WD60EZRX, external USB dock |
| Mount | /mnt/backup, ~5.2 TB usable |
| fstab | UUID=<redacted> /mnt/backup ext4 defaults,nofail 0 2 |
Contents:
/mnt/backup/bbs/— all Borg Backup Server state in a single bind mount: MariaDB, ClickHouse, Borg repositories, SSH host keys, server config. The container is fully reconstructable from this directory alone./mnt/backup/rear/{home-serv,sec-serv}/— bootable rescue ISO plus system tarball per server. Independent of BBS and survives its teardown. home-serv writes locally withBACKUP=NETFS; sec-serv pushes here over rsync-via-SSH as root.
Bulk storage pool — MergerFS + SnapRAID
Three data disks and one parity disk, all 6 TB Seagate IronWolf, ext4. Pool mounts at /mnt/media, ~17 TB usable.
| Mount | Label | Role | Reserved blocks |
|---|---|---|---|
/mnt/disk1 | media1 | data | 5% |
/mnt/disk2 | media2 | data | 5% |
/mnt/disk3 | media3 | data | 0% |
/mnt/parity1 | — | parity | 5% |
Device letters are not stable across reboots. Always resolve by UUID or label.
MergerFS fstab entry:
/mnt/disk1:/mnt/disk2:/mnt/disk3 /mnt/media mergerfs \
defaults,allow_other,use_ino,category.create=mfs,minfreespace=50G,\
x-systemd.requires-mounts-for=/mnt/disk1,\
x-systemd.requires-mounts-for=/mnt/disk2,\
x-systemd.requires-mounts-for=/mnt/disk3 0 0
x-systemd.requires-mounts-for guarantees every branch is mounted before MergerFS starts. Without it, a slow-enumerating USB drive produces a pool silently missing a disk.
All four pool entries carry nofail. Every disk on this host is USB-attached — without nofail, a single failed enumeration drops a headless machine to an emergency console.
Branch management. MergerFS 2.33.3. Branches change live through the xattr control interface; mount -o remount does not work for branch changes and fails with fuse: mountpoint is not empty.
sudo getfattr -n user.mergerfs.branches /mnt/media/.mergerfs
sudo setfattr -n user.mergerfs.branches -v '+>/mnt/diskN=RW' /mnt/media/.mergerfsSnapRAID. Config at /etc/snapraid.conf. Parity file at /mnt/parity1/snapraid.parity; data disks d1/d2/d3; content file mirrored to all three data drives. Single-drive failure is recoverable with snapraid fix. Two simultaneous drive failures exceed single-parity coverage.
Parity constraint. Parity must be greater than or equal to the largest data drive. All four disks are 6 TB, so the array sits at its limit — adding a data disk larger than 6 TB requires role-swapping parity onto the new disk and rebuilding parity first.
USB attachment
Both /mnt/backup RAID 1 members and all four pool drives sit behind ASMedia ASM1153E SATA bridges (174c:55aa) on Genesys Logic USB 3.1 hubs. There are no internally-attached spinning disks on this host. The four pool drives share a single hub at 4-1.4.
Stability mitigations live in the GRUB cmdline rather than a kernel package, so they survive kernel upgrades automatically:
usbcore.autosuspend=-1 usb-storage.quirks=174c:55aa:u
usb-storage.quirks matches by VID:PID, so any new drive on the same bridge chipset inherits the fix without a cmdline change.
Verify topology after adding a drive:
for d in sdc sdd sde sdf; do
echo -n "$d: "
readlink -f /sys/block/$d | grep -oE 'usb[0-9]+(/[0-9.-]+)+' | head -1
doneNetwork
Traffic flow
flowchart LR
User[Browser or mobile]
CF[Cloudflare edge]
CFD[cloudflared daemon]
SVC[home-serv services]
User -->|HTTPS 443| CF
CF -->|outbound QUIC tunnel| CFD
CFD --> SVCExternal access is inbound-free. Cloudflared holds an outbound QUIC tunnel to the Cloudflare edge; no ports are forwarded at the perimeter.
Mesh
home-serv is a member of a four-machine Tailscale mesh alongside sec-serv, jeremy-pc, and jeremy-lt. Other LAN hosts: 10.0.4.110 (sec-serv), 10.0.4.201 (cyberlab).
Exposure tiers
| Tier | Binding | Reach |
|---|---|---|
| 1 | 127.0.0.1 | localhost only |
| 2 | via tunnel | public through Cloudflare Tunnel |
| 3 | 0.0.0.0 | LAN and Tailscale reachable |
| 4 | no host port | Docker-internal only |
Services
Notes, knowledge and reading
| Service | Hostname | Host port | Container |
|---|---|---|---|
| BookStack | wiki.lab.example | 0.0.0.0:3004 | bookstack + bookstack_db |
| SilverBullet | silverbullet.lab.example | 0.0.0.0:3005 | silverbullet |
| Blinko | blinko.lab.example | 0.0.0.0:1111 | blinko-website + blinko-postgres |
| Karakeep | bookmarks.lab.example | 0.0.0.0:3008 | karakeep + karakeep-chrome + karakeep-meilisearch |
| FreshRSS | rss.lab.example | 0.0.0.0:8088 | freshrss |
| Anki sync | anki.lab.example | 127.0.0.1:27701 | anki-sync |
| InkSpire | journal.lab.example | 127.0.0.1:3009 | inkspire + inkspire-db |
| OpenBooks | openbooks.lab.example | 0.0.0.0:5079 | openbooks |
Files and documents
| Service | Hostname | Host port | Container |
|---|---|---|---|
| Paperless-ngx | paperless.lab.example | 0.0.0.0:8000 | paperless-ngx-webserver-1 + db + broker (TOTP MFA enabled) |
| Stirling-PDF | pdf.lab.example | 0.0.0.0:8899 | stirling-pdf (started on demand) |
| ConvertX | convertx.lab.example | 0.0.0.0:8897 | convertx |
| Nextcloud AIO | nextcloud.lab.example | 0.0.0.0:8082 | nextcloud-aio-apache + 9 sub-containers |
| Reactive Resume | resume.lab.example | 127.0.0.1:8106 | reactive-resume + db + printer |
AI and ML
| Service | Hostname | Host port | Type |
|---|---|---|---|
| Ollama (Intel Arc) | LAN-only | 0.0.0.0:11434 | Docker — ollama-intel, KEEP_ALIVE=5m |
| Qdrant | internal | 127.0.0.1:6333/:6334 | Docker — v1.18.0, on ai-net |
| LibreChat | chat.lab.example | 0.0.0.0:3080 | Docker — 5 containers |
| AnythingLLM | llm.lab.example | 0.0.0.0:3001 | Docker — vector store is Qdrant |
| SD.Next | LAN-only | 0.0.0.0:7860 | Native systemd — IPEX / Arc A770 |
| RAG MCP | LAN-only | 0.0.0.0:8765 | Native systemd — FastMCP SSE |
| RAG ingest | — | — | Native systemd timer — nightly |
LibreChat stack — /home/jeremy/docker/stacks/librechat/
| Container | Image | Port |
|---|---|---|
librechat | ghcr.io/danny-avila/librechat:latest | 0.0.0.0:3080 |
librechat-mongodb | mongo:7 | internal |
librechat-meilisearch | getmeili/meilisearch:v1.11.1 | internal |
librechat-pgvector | pgvector/pgvector:pg16 | internal |
librechat-rag-api | ghcr.io/danny-avila/librechat-rag-api-dev:latest | internal :8000 |
Configured through librechat.yaml v1.3.9 with agents enabled, MCP domain allowlisting, and SearXNG-backed web search. Two MCP tools are exposed: search_knowledge_base against Qdrant, and web_search against the local SearXNG instance. Image generation routes through a Stable Diffusion agent to SD.Next.
RAG knowledge base — /home/jeremy/docker/stacks/rag-ingest/
| Component | Detail |
|---|---|
| Collection | knowledge_base — 22,731 chunks |
| Embedding model | nomic-embed-text (768 dimensions) via Ollama |
| Sources | BookStack, Paperless-ngx, Gitea, Nextcloud |
| Schedule | Nightly via rag-ingest.timer |
| Interface | rag-mcp.service on :8765 |
SD.Next — native systemd, install at /home/jeremy/docker/stacks/sdnext/. IPEX backend on the Arc A770.
Security
| Service | Type |
|---|---|
| Wazuh agent | Native systemd — reports to sec-serv, agent ID 001 |
| MeshCentral agent | Native systemd — enrolled with the server on sec-serv |
| Velociraptor client | Native systemd — reports to the server on sec-serv |
Backup
| Service | Hostname | Host port | Type |
|---|---|---|---|
| BBS (Borg Backup Server) | backup.lab.example | 0.0.0.0:8081 web, 0.0.0.0:2222 SSH | Docker |
| Healthchecks | hc.lab.example | 0.0.0.0:8050 | Docker |
| ReaR | — | none | Native systemd timer |
BBS is the primary backup system, providing a web UI, agent management, and Discord notifications through Apprise. Three clients are enrolled: home-serv, sec-serv, and jeremy-pc. All state lives on the /mnt/backup/bbs/ bind mount.
ReaR produces weekly bare-metal recovery images — a bootable rescue ISO plus a system tarball — written to /mnt/backup/rear/home-serv/. It runs entirely independently of BBS.
Healthchecks tracks run status for the ReaR jobs on both servers.
Finance, utilities and dashboards
| Service | Hostname | Host port | Container |
|---|---|---|---|
| Actual Budget | actual.lab.example | 127.0.0.1:5006 | actual |
| Tamari | menu.lab.example | 0.0.0.0:4888 | tamari |
| Games | games.lab.example | 0.0.0.0:8108 | games |
| GameVault | gamevault.lab.example | 0.0.0.0:8080 | gamevault + gamevault-postgres |
| Questarr | questarr.lab.example | 0.0.0.0:5000 | questarr |
| Nexus Pulse | nexus.lab.example | 127.0.0.1:3007 | nexus-pulse |
| ViperDash | viperdash.lab.example | 0.0.0.0:3010 | viperdash — built locally |
| OmniTools | omnitools.lab.example | 0.0.0.0:8103 | omnitools |
| Changedetection.io | changes.lab.example | 127.0.0.1:8105 | changedetection |
| Dockge | dockge.lab.example | 127.0.0.1:5001 | dockge-dockge-1 |
| SearXNG | search.lab.example | 0.0.0.0:8888 | searxng |
| Homarr | home.lab.example | 0.0.0.0:7575 | homarr |
| Gitea | git.lab.example | 127.0.0.1:3020 | gitea |
| ntfy | ntfy.lab.example | 0.0.0.0:8181 | ntfy |
| Beszel agent | internal | host 45876 | beszel-agent — reports to the hub on sec-serv |
| Dozzle agent | internal | 0.0.0.0:7007 | dozzle-agent — reports to the hub on sec-serv |
Cloudflare tunnel routes
Catch-all rule: http_status:404.
| Hostname | Target |
|---|---|
actual.lab.example | localhost:5006 |
anki.lab.example | localhost:27701 |
backup.lab.example | localhost:8081 |
blinko.lab.example | 10.0.4.200:1111 |
bookmarks.lab.example | localhost:3008 |
changes.lab.example | localhost:8105 |
chat.lab.example | localhost:3080 |
convertx.lab.example | localhost:8897 |
dockge.lab.example | localhost:5001 |
gamevault.lab.example | localhost:8080 |
games.lab.example | localhost:8108 |
git.lab.example | localhost:3020 |
hc.lab.example | localhost:8050 |
home.lab.example | 10.0.4.200:7575 |
journal.lab.example | localhost:3009 |
llm.lab.example | 10.0.4.200:3001 |
menu.lab.example | 10.0.4.200:4888 |
nexus.lab.example | localhost:3007 |
nextcloud.lab.example | localhost:8082 |
ntfy.lab.example | localhost:8181 |
omnitools.lab.example | localhost:8103 |
openbooks.lab.example | localhost:5079 |
paperless.lab.example | localhost:8000 |
pdf.lab.example | localhost:8899 |
questarr.lab.example | localhost:5000 |
resume.lab.example | localhost:8106 |
rss.lab.example | localhost:8088 |
search.lab.example | localhost:8888 |
silverbullet.lab.example | localhost:3005 |
viperdash.lab.example | localhost:3010 |
wiki.lab.example | localhost:3004 |
ntfy.lab.example requires disableChunkedEncoding: true.
Host port map
| Port | Binding | Service |
|---|---|---|
22 | 0.0.0.0 | SSH |
631 | 127.0.0.1 | CUPS |
1111 | 0.0.0.0 | blinko-website |
2222 | 0.0.0.0 | BBS SSH — Borg agent connections |
3001 | 0.0.0.0 | anythingllm |
3004 | 0.0.0.0 | bookstack |
3005 | 0.0.0.0 | silverbullet |
3007 | 127.0.0.1 | nexus-pulse |
3008 | 0.0.0.0 | karakeep |
3009 | 127.0.0.1 | inkspire |
3010 | 0.0.0.0 | viperdash |
3020 | 127.0.0.1 | gitea |
3080 | 0.0.0.0 | librechat |
3306 | 127.0.0.1 | MariaDB (native) |
3478 | 0.0.0.0 TCP+UDP | nextcloud-aio-talk |
4888 | 0.0.0.0 | tamari |
5000 | 0.0.0.0 | questarr |
5001 | 127.0.0.1 | Dockge |
5006 | 127.0.0.1 | Actual Budget |
5079 | 0.0.0.0 | openbooks |
5435 | 0.0.0.0 | blinko-postgres |
6333 | 127.0.0.1 | Qdrant REST |
6334 | 127.0.0.1 | Qdrant gRPC |
7007 | 0.0.0.0 | dozzle-agent |
7575 | 0.0.0.0 | homarr |
7860 | 0.0.0.0 | SD.Next (native) |
8000 | 0.0.0.0 | paperless-ngx |
8050 | 0.0.0.0 | Healthchecks |
8080 | 0.0.0.0 | gamevault |
8081 | 0.0.0.0 | BBS web UI |
8082 | 0.0.0.0 | nextcloud-aio-apache |
8088 | 0.0.0.0 | freshrss |
8090 | 0.0.0.0 | nextcloud-aio-mastercontainer |
8103 | 0.0.0.0 | omnitools |
8105 | 127.0.0.1 | Changedetection.io |
8106 | 127.0.0.1 | Reactive Resume |
8108 | 0.0.0.0 | games |
8181 | 0.0.0.0 | ntfy |
8765 | 0.0.0.0 | rag-mcp (native) |
8888 | 0.0.0.0 | searxng |
8897 | 0.0.0.0 | convertx |
8899 | 0.0.0.0 | stirling-pdf |
11434 | 0.0.0.0 | ollama-intel |
20241 | 127.0.0.1 | cloudflared metrics |
27701 | 127.0.0.1 | anki-sync |
41641 | 0.0.0.0 UDP | tailscaled |
45876 | 0.0.0.0 | beszel-agent (host network) |
Compose projects
Stacks live under /home/jeremy/docker/stacks/, managed through Dockge.
actual/ anki/ anythingllm/
bbs/ beszel/ blinko/
bookstack/ changedetection/ convertx/
dockge/ freshrss/ games/
gitea/ healthchecks/ inkspire/
karakeep/ librechat/ nextcloud-aio/
nexus-pulse/ ntfy/ ollama-intel/
omnitools/ paperless-ngx/ qdrant/
rag-ingest/ reactive-resume/ sdnext/
searxng/ silverbullet/ tamari/
viperdash/ watchtower/
sdnext/ and rag-ingest/ hold installs and scripts for services that run as native systemd units rather than compose stacks.
Native systemd services
| Service | Role | Port |
|---|---|---|
cloudflared | Tunnel daemon | outbound + 127.0.0.1:20241 |
docker, containerd | Docker Engine | sockets |
mariadb | Native MariaDB | 127.0.0.1:3306 |
sdnext | SD.Next image generation (IPEX / Arc A770) | 0.0.0.0:7860 |
rag-mcp | Knowledge base MCP server | 0.0.0.0:8765 |
rag-ingest.timer | Nightly RAG ingestion | — |
rear-backup.timer | Weekly bare-metal system image | — |
bbs-agent | Borg Backup Server client | outbound only |
tailscaled | Tailscale mesh | UDP :41641 |
wazuh-agent | SIEM agent → sec-serv | outbound only |
meshagent | MeshCentral agent → sec-serv | outbound only |
velociraptor_client | Velociraptor DFIR client → sec-serv | outbound only |
smartmontools | SMART disk monitoring | — |
ssh | OpenSSH | 0.0.0.0:22 |
ufw | Firewall | — |
cups | Printing | 127.0.0.1:631 |
Scheduled tasks
| Job | Schedule | Defined in |
|---|---|---|
| SnapRAID sync | 02:00 nightly | jeremy crontab |
| SnapRAID scrub | 03:00 Sundays | jeremy crontab |
| RAG full ingest | 02:00 nightly | rag-ingest.timer |
| Obsidian → BookStack sync | 03:00 on the 1st and 15th | jeremy crontab |
| Scrutiny SMART collector → sec-serv | midnight nightly | root crontab |
| BBS backup — home-serv | 01:00 daily | BBS server-side scheduler |
| BBS backup — sec-serv | 01:00 daily | BBS server-side scheduler |
| BBS backup — jeremy-pc | 02:00 daily | BBS server-side scheduler |
| ReaR system image | 03:00 Sundays | rear-backup.timer |
rear-backup.timer sets Persistent=true, so a reboot inside the Sunday window triggers a catch-up run.