🐹 Home

Home Lab Command School

Everything you need to know to run your R720xd stack.
Pick a system below to start learning.

🖥️

Proxmox

VM management, storage, networking, snapshots, and the shell commands you'll live in.

35+ commands
💾

TrueNAS

ZFS pools, datasets, snapshots, scrubs, and how to manage your 24 drive bays.

25+ commands
🐋

Docker

Containers, images, compose, volumes, networks. The full lifecycle from pull to destroy.

40+ commands
🐳

Portainer

CLI tricks that work alongside the Portainer UI. Deploy stacks, manage agents, reset access.

15+ commands
📦

Container-Specific

Jellyfin, Nextcloud, Pi-hole, Nginx PM, Frigate, Open WebUI, Navidrome, Calibre Web.

8 services covered
🐧

Linux Core

The fundamental shell skills that underpin everything else. Files, permissions, networking, processes.

30+ commands

Proxmox Commands

Run these in the Proxmox Shell (node shell, not inside a VM). Most use the qm and pct CLI tools.

VM Management
qm list
List all VMs and their current state
safe
What you'll see
VMID, Name, Status (running/stopped), Memory, Bootdisk, PID. This is your first command every time you SSH into Proxmox.
Example output
VMID NAME STATUS MEM(MB) BOOTDISK(GB) PID
100 TrueNAS running 8192 32.00 12445
101 docker-host running 4096 32.00 12891
qm start 100
Start VM with ID 100
safe
Note
Replace 100 with your VMID. Get the ID from qm list. Can also do this from the web UI — CLI is faster once you know your IDs.
qm stop 100
Hard stop VM (like pulling power)
warn
⚠️ This is a hard kill. Use qm shutdown 100 for a clean OS shutdown. Hard stop risks filesystem corruption on non-journaled drives.
Clean shutdown instead
qm shutdown 100
qm reboot 100
Gracefully reboot a VM
safe
Sends a clean reboot signal to the guest OS. The VM will shut down and restart. Good for applying OS updates inside a VM.
qm config 100
Show full config for VM 100
safe
Shows CPU, memory, disk, network config, boot order, passthrough devices. Super useful for debugging or backing up config.
cat /etc/pve/qemu-server/100.conf # same thing, direct file read
qm destroy 100
Delete VM and all its disks
danger
🔴 Irreversible. Deletes the VM config AND all disk images. Make a snapshot or backup first if there's anything you care about.
qm destroy 100 --purge # also removes from backups
Snapshots & Backups
qm snapshot 100 snap-name
Create a snapshot of VM 100
safe
Snapshots are your safety net. Take one before making big changes. Not a backup — if the disk dies, snapshot dies with it.
qm snapshot 100 before-update --description "pre OS update"
qm listsnapshot 100 # list all snapshots
qm rollback 100 before-update # revert to snapshot
qm delsnapshot 100 before-update # delete snapshot
vzdump 100 --storage local
Backup VM 100 to local storage
safe
Real backup — survives disk failures. The --storage flag specifies where to save. Check your storage names with pvesm status.
vzdump 100 --storage local --compress zstd --mode snapshot
vzdump 100 101 102 --storage local # backup multiple VMs
Restore a backup
qmrestore /var/lib/vz/dump/vzdump-qemu-100-*.vma.zst 100
Storage & Disks
pvesm status
Show all configured storage pools and usage
safe
Shows every storage location Proxmox knows about — local, NFS mounts, ZFS pools, etc. Check this if a VM says it can't find its disk.
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT
List all block devices (disks)
safe
Shows every drive the Proxmox host can see, including all 24 bays on the R720xd. Use this to figure out which /dev/sdX is which disk before passing through to TrueNAS.
lsblk -o NAME,SIZE,SERIAL,MODEL # includes serial numbers
qm set 100 --scsi1 /dev/disk/by-id/...
Pass a physical disk through to a VM
warn
⚠️ Always use /dev/disk/by-id/ paths, NOT /dev/sda etc. The sdX names can change on reboot. by-id is permanent.
Find the by-id path first
ls -la /dev/disk/by-id/ | grep -v part # list drives by serial
Full passthrough command
qm set 100 --scsi1 /dev/disk/by-id/ata-WDC_WD40_ABCD1234,cache=none
Node Health & Info
pvesh get /nodes/pve/status
JSON dump of node health — CPU, RAM, uptime
info
Replace pve with your node name. Check the web UI top-right for your node name if unsure.
pvesh get /nodes/pve/status --output-format yaml # human readable
pveversion -v
Show Proxmox version and all component versions
info
Good to check before and after updates. Also useful when reporting issues — people will ask what version you're on.
apt update && apt dist-upgrade
Update Proxmox (run on the host)
warn
⚠️ Stop or snapshot your VMs first. Proxmox uses Debian under the hood. A kernel update requires a reboot of the entire host — taking all VMs down with it.
No-subscription repo fix (do this once)
# removes the "no subscription" nag on updates
sed -i 's/^deb/#deb/' /etc/apt/sources.list.d/pve-enterprise.list
echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" > /etc/apt/sources.list.d/pve-no-sub.list
journalctl -xe -u pveproxy
Check Proxmox web UI service logs
info
If the web UI won't load, this tells you why. Common fix after seeing errors:
systemctl restart pveproxy # restart the web UI service
systemctl restart pvedaemon # restart the main Proxmox daemon
LXC Containers (lightweight alternative to VMs)
pct list
List all LXC containers
safe
LXC containers are lighter than full VMs — no full OS kernel overhead. Great for Pi-hole, small services. Same commands as qm but use pct.
pct start 200 # start container 200
pct stop 200 # stop container
pct enter 200 # shell INTO the container
pct exec 200 -- bash # alternative shell access

TrueNAS Commands

Run in TrueNAS Shell (web UI → System → Shell) or SSH into the TrueNAS VM. ZFS is the filesystem underneath everything.

Pool Status & Health
zpool status
Overall health of all ZFS pools
safe
Your #1 health check. Look for ONLINE on everything. Any DEGRADED or FAULTED means a drive is dying or dead.
zpool status tank # specific pool named "tank"
zpool status -v # verbose — shows errors per drive
zpool list
List pools with size, used, available space
safe
Quick storage usage overview. ALLOC = used, FREE = available, FRAG = fragmentation (want this low). High fragmentation (>50%) can slow things down.
zpool scrub tank
Start a data integrity scrub on pool "tank"
safe
A scrub reads every block and verifies checksums. Catches silent corruption. TrueNAS does this automatically monthly but you can kick one off manually. Takes hours on large pools.
zpool scrub tank # start scrub
zpool status tank # check progress (look for "scrub in progress")
zpool scrub -s tank # stop a running scrub
Datasets & Snapshots
zfs list
List all datasets and their sizes
safe
Datasets are like folders with superpowers. Each can have its own compression, quotas, snapshots. You'll create one per service (media, nextcloud, backups, etc).
zfs list -t snapshot # list only snapshots
zfs list -r tank # recursive — shows children
zfs snapshot tank/media@snap1
Take an instant snapshot of the media dataset
safe
Snapshots are instant and nearly free on ZFS. They only use space for what changes after the snapshot. Use them before big moves or deletions.
zfs rollback tank/media@snap1 # revert dataset to snapshot
zfs destroy tank/media@snap1 # delete a snapshot
zfs diff tank/media@snap1 # see what changed since snapshot
zfs get compressratio tank/media
Check compression ratio on a dataset
info
ZFS can compress data transparently. Enable it on new datasets (TrueNAS does this by default with lz4). Video files don't compress much. Text/logs compress a lot.
zfs set compression=lz4 tank/documents # enable compression
zfs get all tank/media | grep compress # check compression settings
Drive Management
smartctl -a /dev/sda
Full SMART health report on a drive
safe
SMART data is the drive's internal health log. Key things to look for: Reallocated Sectors (should be 0), Pending Sectors (should be 0), Temperature (under 50°C).
smartctl -t short /dev/sda # run a short self-test (2min)
smartctl -t long /dev/sda # run a long self-test (hours)
smartctl -H /dev/sda # just show PASSED/FAILED
zpool replace tank sda sdb
Replace a failed drive (sda) with a new one (sdb)
warn
This is how you swap a dead drive. ZFS will resilver (rebuild) automatically after the replace. Check progress with zpool status.
⚠️ Use by-id paths, not sdX. The sda/sdb will change after a reboot.
zpool replace tank /dev/disk/by-id/old-drive /dev/disk/by-id/new-drive

Docker Commands

Run these in any Linux shell where Docker is installed — your Docker VM, or the Proxmox host if you install Docker directly.

Container Lifecycle
docker ps
List running containers
safe
Your most-used command. Shows Container ID, image, status, ports, name. Use the name or ID in other commands.
docker ps -a # all containers including stopped ones
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" # cleaner output
docker start jellyfin
Start a stopped container by name
safe
docker stop jellyfin # graceful stop (SIGTERM, 10s timeout)
docker kill jellyfin # immediate kill (SIGKILL)
docker restart jellyfin # stop + start in one command
docker logs -f jellyfin
Follow live logs for a container
safe
The -f flag means "follow" — live stream. Hit Ctrl+C to stop following. This is how you debug a container that's not working right.
docker logs jellyfin # dump all logs
docker logs -f --tail 100 jellyfin # follow last 100 lines
docker logs jellyfin 2>&1 | grep ERROR # filter for errors
docker exec -it jellyfin bash
Open a shell INSIDE a running container
warn
Like SSHing into the container. -it = interactive + tty. bash can be replaced with sh if the container doesn't have bash installed.
docker exec -it jellyfin bash # shell inside
docker exec -it jellyfin sh # if no bash
docker exec jellyfin ls /config # run one command without interactive
⚠️ Changes made inside a container shell are lost when the container restarts (unless in a volume). Config files should be in volumes.
docker rm jellyfin
Delete a stopped container
warn
Only deletes the container, not its volumes (your data is safe). Container must be stopped first.
docker rm -f jellyfin # force — stops and removes in one go
docker container prune # remove ALL stopped containers
Images
docker images
List all downloaded images
safe
Images are the blueprints. Containers are instances of images. One image, multiple containers possible.
docker pull jellyfin/jellyfin # download latest image
docker rmi jellyfin/jellyfin # remove an image
docker image prune -a # remove ALL unused images (frees space)
docker pull lscr.io/linuxserver/jellyfin:latest
Update an image to latest version
safe
This downloads the new image but doesn't update the running container. After pulling, recreate the container with docker compose up -d.
Full update workflow
docker compose pull # pull new images for all services in compose file
docker compose up -d # recreate containers with new images
docker image prune # clean up old image layers
Docker Compose (this is how you'll run everything)
docker compose up -d
Start all services in compose file (detached)
safe
Run from the directory containing docker-compose.yml. -d = detached (runs in background). This is the main command you'll use to bring up your whole stack.
docker compose up -d # start everything
docker compose up -d jellyfin # start only one service
docker compose up -d --force-recreate # force recreate even if nothing changed
docker compose down
Stop and remove all containers in the stack
warn
Stops containers and removes them. Volumes (your data) are NOT deleted. Safe to do before making config changes.
docker compose down -v # ⚠️ ALSO deletes volumes — use carefully
docker compose stop # stops but doesn't remove containers
docker compose logs -f
Follow logs for all services in the stack
safe
docker compose logs -f jellyfin # only one service
docker compose logs --tail 50 # last 50 lines from all services
docker compose ps
Status of services in the current compose stack
safe
Unlike docker ps which shows everything, this only shows what's in the current compose file. More focused view.
Volumes & Networks
docker volume ls
List all Docker volumes (persistent data)
safe
Volumes are where container data actually lives. A container can be deleted and recreated and all data is still there in the volume.
docker volume inspect jellyfin_config # see where data is stored on disk
docker volume rm jellyfin_config # ⚠️ delete volume — data gone
docker volume prune # remove all unused volumes
docker network ls
List all Docker networks
safe
Containers on the same network can talk to each other by name. You'll have a custom network for all your services so they can reach each other without exposing ports externally.
docker network create homelab # create a network
docker network inspect homelab # see connected containers + IPs
docker network connect homelab jellyfin # add container to network
System & Cleanup
docker system df
Show disk usage by Docker (images, containers, volumes)
safe
Run this when you're surprised your disk is filling up. Images accumulate fast especially after updates.
docker system prune -a
Nuclear cleanup — removes everything not in use
danger
🔴 Removes all stopped containers, all unused images, all build cache. If a container is currently stopped (not running), its image gets deleted. Won't delete volumes by default.
docker system prune # safer — only removes "dangling" images
docker system prune -a # ⚠️ removes ALL unused images
docker system prune -a -f # no confirmation prompt
docker stats
Live resource usage per container (CPU, RAM, network)
safe
Like htop but for containers. See which one is eating all your RAM. Hit Ctrl+C to exit.
docker stats jellyfin # stats for one container only
docker stats --no-stream # one snapshot, then exit

Portainer Commands

Portainer is mostly a web UI, but these CLI commands handle install, reset, and the things the UI can't do.

Installation & Setup
docker volume create portainer_data
Create the Portainer data volume (do once)
safe
Full install (copy-paste this)
docker volume create portainer_data

docker run -d \
-p 8000:8000 \
-p 9443:9443 \
--name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
Then go to: https://YOUR-SERVER-IP:9443
docker restart portainer
Restart the Portainer container
safe
If the Portainer UI is acting up, usually a restart fixes it. Your stacks and configs are preserved in the volume.
docker stop portainer && docker rm portainer
Update Portainer to latest version
warn
Since Portainer manages Docker, you can't update it via Portainer itself. Do it manually in the CLI.
Full update procedure
docker stop portainer
docker rm portainer
docker pull portainer/portainer-ce:latest
# then re-run the install command above
Admin Reset
docker exec -it portainer portainer --admin-password-file /tmp/pass
Reset admin password if locked out
danger
Proper password reset procedure
# Stop Portainer
docker stop portainer

# Reset password to "newpassword"
docker run --rm \
-v portainer_data:/data \
portainer/portainer-ce:latest \
--admin-password='$2y$05$yourbcrypthash'

# Easier: just nuke the data volume and start fresh
docker rm portainer
docker volume rm portainer_data
docker volume create portainer_data
# then re-run install command
Stacks via CLI
docker compose -f /opt/stacks/jellyfin/compose.yml up -d
Deploy a Portainer stack manually from CLI
safe
Portainer stores stacks in /opt/stacks/ (or similar). You can manage them from CLI too — useful if Portainer is down. The UI and CLI are interchangeable.
Recommended stack file layout
/opt/stacks/
jellyfin/compose.yml
nextcloud/compose.yml
pihole/compose.yml
nginx-proxy-manager/compose.yml

Service Commands

Common commands, configs, and fixes for each service in your stack.

🎬 Jellyfin
docker logs -f jellyfin | grep -i error
Watch for Jellyfin errors in real time
info
Fix: permissions error on media files
# Your user (PUID) needs to match who owns the media files
chown -R 1000:1000 /mnt/media
chmod -R 755 /mnt/media
Fix: force library rescan
docker exec jellyfin curl -s "http://localhost:8096/Library/Refresh?api_key=YOUR_API_KEY"
Compose snippet (save as reference)
services:
jellyfin:
image: lscr.io/linuxserver/jellyfin:latest
environment:
- PUID=1000
- PGID=1000
volumes:
- /opt/appdata/jellyfin:/config
- /mnt/media:/media
ports:
- 8096:8096
restart: unless-stopped
☁️ Nextcloud
docker exec -it --user www-data nextcloud php occ
Run Nextcloud's admin CLI tool (occ)
info
occ is Nextcloud's built-in admin tool. You use it for everything that can't be done in the web UI.
# Full format
docker exec -it --user www-data nextcloud php occ [command]

# Common occ commands
... php occ status # health check
... php occ maintenance:mode --on # enable maintenance mode
... php occ maintenance:mode --off # disable it
... php occ files:scan --all # scan for new files added directly to disk
... php occ user:list # list all users
... php occ upgrade # run after updating the image
🔒 Pi-hole
docker exec -it pihole pihole status
Check Pi-hole service status
safe
docker exec -it pihole pihole status # is it blocking?
docker exec -it pihole pihole restartdns # restart DNS resolver
docker exec -it pihole pihole updateGravity # update block lists
docker exec -it pihole pihole -q google.com # test if domain is blocked
docker exec -it pihole pihole whitelist add google.com # unblock a site
docker exec -it pihole pihole blacklist add annoying.com # block a site
Fix: DNS stops working on whole network
docker restart pihole # most common fix
🌐 Nginx Proxy Manager
docker logs -f nginx-proxy-manager
Watch NPM logs — catch cert / routing issues
info
Default login (change immediately)
Email: admin@example.com
Password: changeme
Fix: Let's Encrypt cert won't issue
# Port 80 must be open from internet for HTTP challenge
# Or use DNS challenge (works behind NAT)
docker exec -it nginx-proxy-manager certbot renew --dry-run
🏠 Open WebUI / Ollama
docker exec -it ollama ollama list
List downloaded AI models in Ollama
safe
docker exec -it ollama ollama list # list models
docker exec -it ollama ollama pull llama3 # download a model
docker exec -it ollama ollama rm llama3 # delete a model
docker exec -it ollama ollama run llama3 "say hello" # test a model
Fix: Open WebUI can't connect to Ollama
# Both must be on same Docker network
# Open WebUI env var should be:
OLLAMA_BASE_URL=http://ollama:11434 # uses container name, not IP
📷 Frigate NVR
docker logs -f frigate | grep -i "camera\|error\|detect"
Monitor Frigate camera and detection logs
info
Check camera stream is working
docker exec -it frigate ffprobe -v quiet -print_format json \
-show_streams "rtsp://192.168.x.x:554/stream1"
Fix: detection not working
# Check if CPU/GPU detector is being seen
docker exec -it frigate frigate --help
docker logs frigate | grep -i "detector"
📚 Calibre Web
docker logs calibre-web | grep -i "database\|error"
Check for Calibre Web database issues
info
Default login
Username: admin
Password: admin123
Compose volume setup (critical)
volumes:
- /opt/appdata/calibre-web:/config
- /mnt/books:/books # point to your books folder
Calibre Web needs a pre-existing Calibre library (metadata.db file). You can create one with the Calibre desktop app and copy the folder to your server.
🎵 Navidrome
docker exec -it navidrome /app/navidrome --configfile /data/navidrome.toml
Run Navidrome with explicit config
info
Minimal compose setup
services:
navidrome:
image: deluan/navidrome:latest
volumes:
- /opt/appdata/navidrome:/data
- /mnt/music:/music:ro # ro = read-only, safe for music
ports:
- 4533:4533
environment:
ND_MUSICFOLDER: /music
ND_DATAFOLDER: /data
Force rescan music library
docker restart navidrome # easiest way to trigger rescan

Linux Shell Commands

The foundation. Everything in your stack runs on Linux. Know these and everything else makes more sense.

Files & Navigation
ls -lah
List directory contents (long, human-readable, hidden)
safe
ls -lah /opt/stacks/ # list stacks folder
ls -lt # sort by modification time (newest first)
cd /opt/stacks/jellyfin
Change directory
safe
cd ~ # go home (/root or /home/user)
cd .. # go up one level
cd - # go back to where you just were
pwd # print where you currently are
cat /opt/stacks/jellyfin/compose.yml
Print file contents to terminal
safe
cat file.txt # print whole file
head -20 file.txt # print first 20 lines
tail -20 file.txt # print last 20 lines
tail -f /var/log/syslog # follow a log file live
nano /opt/stacks/jellyfin/compose.yml
Edit a file (nano is beginner-friendly)
warn
Nano is the easiest text editor in Linux. Controls shown at bottom of screen.
Ctrl+O then Enter # Save
Ctrl+X # Exit
Ctrl+K # Cut a line
Ctrl+U # Paste
Ctrl+W # Search
⚠️ If you open vim by accident: press Escape, then type :q! and press Enter to exit without saving.
mkdir -p /opt/stacks/jellyfin
Create directory (and parents if needed)
safe
mkdir -p /opt/stacks/{jellyfin,nextcloud,pihole,npm} # create multiple at once
rm -rf /opt/old-folder/ # ⚠️ delete folder and everything in it
cp -r /source /dest # copy folder recursively
mv /old/path /new/path # move or rename
Permissions & Ownership
chown -R 1000:1000 /mnt/media
Change owner of a folder (used constantly with Docker)
warn
Docker containers run as a user. If the container can't read your files, the user ID (1000) needs to own them. Most LinuxServer.io images use PUID/PGID 1000.
id # show your current user ID
chown -R 1000:1000 /mnt/media # set owner recursively
chmod -R 755 /mnt/media # rwxr-xr-x permissions
ls -la /mnt/media # check who owns files
Processes & System
htop
Interactive CPU/RAM/process monitor
safe
Better than top. Colorful, sortable, you can kill processes from it. F10 or Q to quit.
apt install htop # install if not present
free -h # quick RAM usage check
df -h # disk space on all mounts
uptime # how long system has been running
systemctl status docker
Check if a system service is running
safe
systemctl status docker # is Docker running?
systemctl start docker # start it
systemctl stop docker # stop it
systemctl restart docker # restart it
systemctl enable docker # start on boot
systemctl disable docker # don't start on boot
Networking
ip addr
Show IP addresses for all network interfaces
safe
ip addr # all interfaces and IPs
ip route # show routing table (gateway)
ping -c 4 8.8.8.8 # test internet connectivity
ping -c 4 192.168.1.1 # test gateway reachability
nmap -sP 192.168.1.0/24 # scan for all devices on LAN
ss -tulpn # show open ports and what's listening
curl -I http://localhost:8096 # test if a web service is responding
ssh user@192.168.1.100
SSH into a remote machine
safe
ssh root@192.168.1.100 # as root
ssh -p 2222 user@192.168.1.100 # custom port
ssh-keygen -t ed25519 # generate SSH key pair
ssh-copy-id user@192.168.1.100 # copy public key (no more passwords)
Grep, Pipe, & Text Tools
grep -r "error" /var/log/
Search for text in files recursively
safe
grep is your search tool. The pipe ( | ) sends output from one command into another.
grep -i "error" file.log # case insensitive search
grep -v "debug" file.log # exclude lines with "debug"
docker logs jellyfin | grep -i error # filter Docker logs
cat file.txt | grep -c "warning" # count matches