Swainstation Migration Guide

i5-7500 backend → Lenovo Legion S7 (Proxmox + Ubuntu LXC)

Proxmox VE 8.x Ubuntu 24.04 LXC RTX 30 NVENC DUMB stack
0
Target Architecture

i5 "swainstation" — stays

  • JellyfinMediaPlayer (TV frontend)
  • Crafty Controller (Minecraft)
  • Pi-hole (DNS for both machines)
  • Acestream engine + web UI
  • JMP restart webhook (9191)
  • IP: 10.0.0.227

Legion S7 — new backend

  • Proxmox VE (bare metal)
  • Ubuntu LXC → Docker
  • DUMB (Jellyfin, Sonarr, Radarr…)
  • Jellyseerr, Caddy, Homepage
  • NVENC hardware transcoding
  • IP: 10.0.0.228
1
Laptop BIOS + Proxmox Install
physical laptop

BIOS (F2 on POST):

Enable SVM Mode (AMD virtualization / IOMMU)

Set GPU Mode → Discrete (disables Optimus)

Flash + install Proxmox VE 8.x ISO to NVMe:

Hostname: swainstation-backend  |  IP: 10.0.0.228  |  Gateway: 10.0.0.1

Wipes Windows 11 entirely. Set DHCP reservation for laptop MAC → 10.0.0.228 in xFi app afterward.

SSH in:

ssh root@10.0.0.228
2
NVIDIA Drivers on Proxmox Host
Proxmox bare metal
apt update && apt install -y pve-headers
apt install -y nvidia-driver nvidia-smi
reboot

After reboot — verify and note device major numbers (needed for LXC config):

nvidia-smi
ls -la /dev/nvidia*
cat /proc/devices | grep nvidia
Note the major number for nvidia-uvm — it varies by kernel. You'll need it in Phase 3.
3
Create Ubuntu LXC
Proxmox CLI / web UI (https://10.0.0.228:8006)
pveam update
pveam download local ubuntu-24.04-standard_*.tar.zst

pct create 100 local:vztmpl/ubuntu-24.04-standard_*.tar.zst \
  --hostname dumb-stack \
  --memory 8192 \
  --cores 6 \
  --rootfs local-lvm:60 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --unprivileged 0 \
  --features nesting=1,fuse=1
Must be privileged (unprivileged 0) — /dev/fuse required for rclone FUSE mounts inside DUMB.

Add to /etc/pve/lxc/100.conf — replace 507 with actual nvidia-uvm major from Phase 2:

lxc.cgroup2.devices.allow: c 195:* rwm
lxc.cgroup2.devices.allow: c 507:* rwm
lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file
lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir
pct start 100
4
Set Up LXC — Docker + NVIDIA
inside LXC: pct enter 100
pct enter 100

# Docker
apt update && apt install -y curl ca-certificates rsync fuse3
curl -fsSL https://get.docker.com | sh

# nvidia-container-toolkit
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | 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' \
  | tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt update && apt install -y nvidia-container-toolkit
nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

# Verify
docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi

# Create user matching i5 PUID
useradd -u 1000 -m jasper
5
Copy Stack from i5
run from i5 terminal
rsync -av /home/jasper/docker/DUMB/       jasper@10.0.0.228:/home/jasper/docker/DUMB/
rsync -av /home/jasper/docker/jellyseerr/ jasper@10.0.0.228:/home/jasper/docker/jellyseerr/
rsync -av /home/jasper/docker/caddy/      jasper@10.0.0.228:/home/jasper/docker/caddy/
rsync -av /home/jasper/docker/homepage/   jasper@10.0.0.228:/home/jasper/docker/homepage/
rsync -av /home/jasper/docker/docker-compose.yml jasper@10.0.0.228:/home/jasper/docker/
Do NOT copy crafty/ or pihole/ — they stay on the i5.
6
Update Configs on Laptop
inside LXC, on laptop files

docker-compose.yml

ChangeOldNew
Homepage allowed hosts10.0.0.227:300110.0.0.228:3001
group_add- "109" (Intel render group)remove entirely
devices/dev/dri:/dev/driremove (add back if ffmpeg needs it)
LIBVA env varsLIBVA_DRIVERS_PATH, LIBVA_DRIVER_NAMEremove both
NVIDIA runtime(not present)add runtime: nvidia + NVIDIA_VISIBLE_DEVICES=all

dumb_config.json — jellyfin env block

Remove: LIBVA_DRIVERS_PATH and LIBVA_DRIVER_NAME: iHD

Keep: JELLYFIN_FFMPEG: /usr/lib/jellyfin-ffmpeg/ffmpeg

homepage/config/services.yaml — update these hrefs to 10.0.0.228:

ServiceOld hrefNew href
Sonarr10.0.0.227:898910.0.0.228:8989
Radarr10.0.0.227:787810.0.0.228:7878
Prowlarr10.0.0.227:969610.0.0.228:9696
Decypharr10.0.0.227:828210.0.0.228:8282
DUMB UI10.0.0.227:300510.0.0.228:3005
Restart JMP (9191), Acestream (9292), Stop Stream — keep at 10.0.0.227, they stay on i5.

Caddyfile — no changes needed (uses Docker service names, not IPs).

7
Host-Level Setup on LXC
inside LXC

CPU governor:

cat > /etc/systemd/system/cpu-performance.service << 'EOF'
[Unit]
Description=Set CPU governor to performance
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now cpu-performance.service

DNS (point to Pi-hole on i5):

echo "DNSStubListener=no" >> /etc/systemd/resolved.conf
systemctl restart systemd-resolved

printf "nameserver 10.0.0.227\nnameserver 75.75.75.75\n" > /etc/resolv.conf
chattr +i /etc/resolv.conf
8
Start + Test on Laptop
inside LXC
cd /home/jasper/docker && docker compose up -d

Verify before cutover:

  • http://10.0.0.228:8096 — Jellyfin loads
  • http://10.0.0.228:8989 — Sonarr loads
  • http://10.0.0.228:3001 — Homepage loads
  • Jellyfin Admin → Playback → Hardware Acceleration → set to NVENC
  • Play HEVC file — check DUMB/data/jellyfin/log/FFmpeg.*.log for h264_nvenc
9
Cutover
router + i5

Router port forwarding (xFi app):

PortWas → NowNotes
80, 44310.0.0.227 → 10.0.0.228Caddy / HTTPS
22stays → 10.0.0.227SSH to i5
2222 (optional)new → 10.0.0.228:22SSH to laptop
19132, 25500-25600stays → 10.0.0.227Crafty / Minecraft

Update jmp-autoconnect.sh on i5 — change Jellyfin backend URL:

# /home/jasper/jmp-autoconnect.sh
# change:  http://10.0.0.227:8096/web/index.html
# to:      http://10.0.0.228:8096/web/index.html

Stop migrated services on i5:

cd /home/jasper/docker
docker compose stop DUMB jellyseerr caddy homepage

DuckDNS — leave script on i5. It updates to the house's public IP which hasn't changed.

10
i5 Cleanup
i5
  • Remove DUMB, jellyseerr, caddy, homepage from i5 docker-compose.yml (or just leave stopped)
  • Pi-hole — keep running, no changes
  • Crafty — keep running, no changes
  • JMP, xorg, acestream — keep running, no changes
  • jmp-restart-server — keep running, no changes
Final Verification Checklist
  • External: https://jellyfin.comfums.duckdns.org loads and plays video
  • External: https://requests.comfums.duckdns.org loads Jellyseerr
  • TV (i5): JMP auto-navigates to Jellyfin on laptop at boot
  • NVENC: HEVC FFmpeg log shows h264_nvenc encoder
  • Homepage: all service widgets show data
  • Homepage: Restart JMP button (10.0.0.227:9191) still works
  • Homepage: Acestream buttons (10.0.0.227:9292) still work
  • Sonarr/Radarr: grab a test release, confirm it appears in Jellyfin
  • Pi-hole: DNS resolving correctly from laptop (dig google.com @10.0.0.227)
  • Crafty: Minecraft server still reachable