Buildout plan · 27 Sep 2026
A new always-on machine to carry local AI and Jellyfin — retiring the P2000 transcoding problem, the Docker Desktop session trap, and the last workload stranded on the Beelink.
Three answers change what gets built. Everything else is decided.
192.168.4.220. Verified free by ping and by UniFi client records, and it sits in the server band beside the NAS at .218. Protect it by shrinking the VLAN 4 DHCP pool end from 192.168.4.254 to 192.168.4.200, which turns .201–.254 into static-only space. Safe to shrink: the only occupants are .215 and .218 (fixed reservations, honored outside the pool) and .230. Once the rig boots, add a UniFi fixed reservation for its MAC as a third lock.
The dead-drop rule is one outbox per agent. The new box gets its own for-<name>.md and must never write for-nuc.md or for-beelink.md — two agents editing one file over SMB clobber each other silently.
A migration is the natural moment to jump. 12.0 fixes unauthorized file access and XSS, and your server is internet-facing. It also rewrites the database on first boot, so the snapshot is the only way back. Both pre-flight hazards are already verified clear: no third-party plugins, and no usernames differing only by capitalization across your 26 users.
Two downloads on your Windows machine. Nothing else goes on the stick.
| Item | Version | Where |
|---|---|---|
| Ubuntu Server ISO Not Desktop. Not the "live" desktop image. | 24.04 LTS | ubuntu.com/download/server |
| Rufus Writes the USB. Balena Etcher works too. | latest | rufus.ie |
24.04 LTS is the recommendation over the newer 26.04 — it has two more years of NVIDIA and CUDA packaging shaken out, and every guide you'll hit for Ollama, Jellyfin and the Container Toolkit assumes it. Support runs to 2029 either way.
In Rufus: select the ISO, leave GPT / UEFI, write in ISO mode if prompted. Verify the download's SHA256 against the SHA256SUMS file on the Ubuntu releases page first — a corrupt ISO fails deep into the install, not at the start.
In BIOS: enable EXPO/DOCP for your RAM, and disable Secure Boot. Secure Boot forces the NVIDIA driver through MOK enrollment — a blue key-management screen at first reboot that's easy to misread and get wrong. Turning it off avoids the whole detour.
Runs from the NUC before the machine exists. Additive and reversible.
Your media has no snapshot protection at all — no periodic tasks, and the only snapshots on Kloud/Media are two taken by hand this month. Kloud/Immich, holding 257 GB of irreplaceable photos, has none either. A delete from the Jellyfin UI would be permanent.
| Dataset | Frequency | Retain | Covers |
|---|---|---|---|
| Kloud/Media | daily | 14 days | Jellyfin UI deletes, *arr accidents |
| Kloud/Immich | daily | 30 days | Photos — loss noticed late |
| Kloud/AppData | daily | 14 days | App configs and databases |
Snapshots store only changed blocks, so on a library that barely changes this costs single-digit GB against 10.9 TiB.
Then the NFS export, scoped deliberately narrow:
path /mnt/Kloud/Media/Jellyfin # NOT all of /mnt/Kloud/Media
networks 192.168.4.0/24
access read-write, mapall off
Exporting only the Jellyfin library means the new box can write what it legitimately manages and cannot reach usenet, AppData, or your personal backup folder at all.
Text-menu installer — arrow keys, space to toggle, Enter. No commands.
| Screen | Choose |
|---|---|
| Installer update | Continue without updating |
| Install type | Ubuntu Server — not "minimized" |
| Network | Edit IPv4 → Manual. Subnet 192.168.4.0/24, 192.168.4.220, gateway 192.168.4.1, nameservers 192.168.4.1 |
| Proxy | Leave blank |
| Mirror | Default |
| Storage | Use entire disk, LVM on — then see the trap below |
| Profile | Your name, a hostname, username, password |
| Ubuntu Pro | Skip |
| SSH | Install OpenSSH server — tick this. Everything afterwards depends on it |
| Featured snaps | Select none |
Ubuntu's guided LVM setup allocates only part of the disk to the root volume and leaves the rest unused in the volume group. You'll install on a 2 TB drive and find 100 GB. On the partition summary screen, select ubuntu-lv → Edit → set size to max. If you miss it, fix it after boot:
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv sudo resize2fs /dev/ubuntu-vg/ubuntu-lv df -h /
No desktop environment, and don't install VS Code on it. A display stack drags in hundreds of packages and can tangle with the NVIDIA driver, which here has exactly one job: CUDA and NVENC.
Two hard gates. Nothing proceeds past a failed one.
sudo apt update && sudo apt upgrade -y
sudo ubuntu-drivers install
sudo reboot
# after reboot
nvidia-smi
nvidia-smi must list the RTX 4070 Ti. If it doesn't, stop — every later step depends on it, and debugging here is far easier than after four more layers are stacked on top.
curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker $USER # log out and back in after this # NVIDIA Container Toolkit — lets containers see the GPU curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \ | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \ | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \ | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt update && sudo apt install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker # Gate 2 — a container must see the card docker run --rm --gpus all ubuntu nvidia-smi
The same listing, but from inside a container. This is the one that matters — Jellyfin, Ollama and Immich ML all reach the card this way.
Then Portainer, so container management is a browser tab rather than a terminal:
docker volume create portainer_data docker run -d -p 9443:9443 --name portainer --restart=always \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer_data:/data portainer/portainer-ce:latest
Reach it at https://192.168.4.220:9443 and set the admin password within a few minutes of first start, or it locks itself and needs a restart.
The last thing you do by hand. After this the agent takes over.
# Node 20 LTS — Ubuntu's apt node is too old curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs git node -v # Claude Code npm install -g @anthropic-ai/claude-code claude --version # CIFS client, so the agent can reach the cross-agent dead-drop sudo apt install -y cifs-utils
Run claude once in a terminal on the box to complete login, then add the machine as an SSH host in the Claude desktop app — that gives you the UI you prefer while the session runs on the server. No GUI is ever installed on the box itself.
An agent installed here gets real file tools — Read, Edit, Grep — against this machine's actual filesystem, plus its own CLAUDE.md and memory. Me shelling in from the NUC would give a command pipe and nothing else; every file operation would degrade into cat and sed.
First message to the fresh Claude Code session on the box. It carries the context that isn't discoverable from the machine itself.
You are the Claude Code agent on a new Ubuntu Server box in Kirov's home estate.
Read this fully before acting.
MACHINE
Ryzen 9 7900X + RTX 4070 Ti (12 GB), Ubuntu Server 24.04 LTS, always on.
IP 192.168.4.220 . Agent name ____________ .
Role: local AI (Ollama) + Jellyfin. Later: Palworld, Immich ML.
NO Blender on this box — that lives on the gaming rig.
ALREADY DONE — do not redo
NVIDIA driver installed, `nvidia-smi` passes on host AND inside a container.
Docker Engine + NVIDIA Container Toolkit installed. Portainer on :9443.
THE ESTATE
TrueNAS R730xd 192.168.4.218 storage, Immich, *arr stack. Pool "Kloud".
NUC 192.168.4.58 websites, Nginx Proxy Manager, DDNS. Agent "nuc".
Beelink 192.168.4.59 empty, being repurposed for monitoring.
Gaming rig 192.168.1.114 never a server; powers on and off.
CROSS-AGENT DEAD-DROP
\\192.168.4.218\Media\AppData\claude-cross (mount via cifs)
ONE outbox per agent. Create your own for-<name>.md. NEVER write
for-nuc.md or for-beelink.md — concurrent SMB writes clobber silently.
No secrets in that folder; it is broadly readable on the LAN.
YOUR TASKS, IN ORDER
1. Mount the NAS media over NFS, read-write:
192.168.4.218:/mnt/Kloud/Media/Jellyfin -> /mnt/jellyfin
Add to /etc/fstab with _netdev. Verify you can write a test file
and then delete it.
2. Deploy Jellyfin (linuxserver image) with runtime nvidia and
PUID=3000 PGID=3000. NFS maps identity by NUMERIC uid/gid — the
NAS files are owned by 3000. Any other uid gives permission errors
that look like a broken export.
3. Migrate the Jellyfin config: copy the WHOLE directory from
192.168.4.218:/mnt/Kloud/Media/jellyfin-migration/config
Do not rebuild from scratch — that folder carries watch history and
26 user accounts. Then set transcoding to NVENC in the dashboard.
4. Deploy Ollama with --gpus all, plus Open WebUI.
5. Report back before any cutover. Do NOT repoint DNS or the proxy
yourself — the NUC agent owns Nginx Proxy Manager.
RULES
- Ask before anything destructive. Snapshots exist on the NAS but
restoring is manual.
- There is a folder on the Media share named "Peronal Backup Claude
dont go here". Never read, list, or touch it. Exclude it from every
scan.
- Report outcomes honestly, including partial failures.
sudo apt install -y nfs-common
sudo mkdir -p /mnt/jellyfin
# /etc/fstab
192.168.4.218:/mnt/Kloud/Media/Jellyfin /mnt/jellyfin nfs rw,_netdev,noatime 0 0
sudo mount -a && touch /mnt/jellyfin/.wtest && rm /mnt/jellyfin/.wtest
NFS maps identity by numeric UID/GID, not username. Your files are owned by uid 3000, so every container touching them runs PUID=3000 / PGID=3000.
services:
jellyfin:
image: lscr.io/linuxserver/jellyfin:10.11.11
container_name: jellyfin
runtime: nvidia
environment:
- PUID=3000
- PGID=3000
- TZ=America/Phoenix
- NVIDIA_VISIBLE_DEVICES=all
volumes:
- /opt/jellyfin/config:/config
- /mnt/jellyfin/movies:/data/movies
- /mnt/jellyfin/tv:/data/tv
- /mnt/jellyfin/music:/data/music
ports: ["8096:8096"]
restart: unless-stopped
Ada NVENC needs no sysext, no legacy driver, and no reapplying after a TrueNAS update. It also takes Jellyfin off the TrueNAS app platform before 26.04 lands — the release that removes the REST API this estate is administered through.
docker run -d --gpus all -p 11434:11434 -v ollama:/root/.ollama \ --name ollama --restart always ollama/ollama docker exec -it ollama ollama pull llama3.1:8b docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway \ -v open-webui:/app/backend/data --name open-webui --restart always \ ghcr.io/open-webui/open-webui:main
12 GB holds an 8B model at Q4 (~6 GB), or 14B at Q4 if you're careful. With no Blender here the model can stay resident — NVENC runs on a separate encoder block and costs a few hundred MB, so inference and transcoding coexist without contention.
Immich itself stays on the NAS with its database and storage. Only the ML service moves — run the immich-machine-learning image with the CUDA tag here, then point the NAS deployment's IMMICH_MACHINE_LEARNING_URL at http://192.168.4.220:3003. Face recognition and CLIP search currently run on 2014 Xeons against a 257 GB library; this is the largest visible improvement in the plan.
Image ich777/steamcmd:palworld with USERNAME=anonymous — a personal Steam account fails, Palworld downloads anonymously. Keep the perf flags -useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS, restore the world from the NAS backup, and rebuild the 6-hourly backup as a systemd timer rather than the interactive-login scheduled task the Beelink needed.
Each line is one field, and each reverses on its own.
| What | From | To |
|---|---|---|
| NPM proxy host 8 · jellyfin | 192.168.4.218:8096 | 192.168.4.220:8096 |
| UniFi forward · WAN UDP 38211 | 192.168.4.59 | 192.168.4.220 |
| Immich ML endpoint | local (CPU) | http://192.168.4.220:3003 |
| TrueNAS Jellyfin app | RUNNING | stop — keep, don't delete |
Websockets and forced SSL are already set on proxy host 8, so SyncPlay and HTTPS carry over untouched — only the forward target changes.
Revert proxy host 8 to 192.168.4.218:8096 and start the TrueNAS app again. That's the whole undo, which is why the old app gets stopped rather than deleted. For Jellyfin's config, restore from /mnt/Kloud/Media/.zfs/snapshot/<snap>/jellyfin-migration/config — copy that one folder, never roll back the whole dataset.
complete/tv (~50 Kai episodes), and two drives at 52 °C in raidz2-1 — same vdev, which puts that vdev's entire fault tolerance in one hot spot.| Host | Address | Role | Agent |
|---|---|---|---|
| New rig | 192.168.4.220 | AI + Jellyfin + Palworld, always on | planned |
| TrueNAS R730xd | 192.168.4.218 | Storage, Immich, *arr stack | none |
| NUC | 192.168.4.58 | Websites, NPM, DDNS | nuc |
| Beelink | 192.168.4.59 | Empty → monitoring | beelink |
| Gaming rig | 192.168.1.114 | Games + Blender. Never a server. | none |
| Port | Service | Exposure |
|---|---|---|
| 8096 | Jellyfin | via NPM, HTTPS forced |
| 11434 | Ollama API | LAN only |
| 3000 | Open WebUI | LAN only |
| 9443 | Portainer | LAN only |
| 38211/udp | Palworld game | WAN forward |
| 30443 | Palworld REST | LAN only |
| 3003 | Immich ML | LAN only — NAS calls it |
Phase 00 can run today. Address settled: 192.168.4.220. Outstanding: the agent name.