Deploy a new app / stack
Adding or starting containers is state-changing: propose the compose, confirm, then bring it up.
First, learn THIS homelab's layout - do not assume
Read infra/docker-stacks.md for the host alias, which compose model is in use,
the pool name, and the config/media paths. There are a few common structures and
this homelab may use any of them:
- Single compose file - one
/mnt/<pool>/docker/docker-compose.ymlholding every service; per-app config at/mnt/<pool>/configs/<app>; a single/mnt/<pool>/mediamount. - Per-stack (Dockge) -
/opt/stacks/<stack>/compose.yaml, with the app's volumes kept inside the stack directory. - Portainer - stacks pasted/edited in the Portainer UI.
If infra/docker-stacks.md does not say, ask which one before writing paths.
A common convention worth knowing (the "servers@home" / TrueNAS *arr layout)
This is one widely used structure, not the only correct one - match what the box actually uses:
- One
mediadataset with subdirectoriesmovies,tv,downloads(a single mount so the *arr apps can hardlink instead of copying); oneconfigsdataset with a sub-dataset per app. - Volumes:
/mnt/<pool>/configs/<app>:/configand/mnt/<pool>/media:/media. PUID/PGID: 568/568 on TrueNAS (theappsgroup); commonly 1000/1000 on Ubuntu/Proxmox hosts. Always setTZ. Dataset perms770, no spaces or capitals in names.- Images:
lscr.io/linuxserver/<app>orghcr.io/hotio/<app>;restart: unless-stopped; ports asHOST:CONTAINER.
Service template:
services: radarr: image: lscr.io/linuxserver/radarr:latest container_name: radarr environment: - PUID=568 - PGID=568 - TZ=America/New_York volumes: - /mnt/tank/configs/radarr:/config - /mnt/tank/media:/media ports: - 7878:7878 restart: unless-stopped
Download clients behind a VPN (killswitch)
Route a download client through a VPN container so it cannot leak the home IP.
The client defines no ports of its own - the ports go on the VPN container,
and the client joins its network. No depends_on is needed.
services: gluetun: # or a linuxserver/wireguard container image: qmcgaw/gluetun cap_add: [NET_ADMIN] environment: - VPN_SERVICE_PROVIDER=... - VPN_TYPE=wireguard - WIREGUARD_PRIVATE_KEY=... - FIREWALL_VPN_INPUT_PORTS=6881 ports: - 8080:8080 # the qBittorrent WebUI port lives HERE - 6881:6881/tcp qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest network_mode: "service:gluetun" # shares the VPN's network stack # NO ports: block here
After it's up, confirm the egress IP is the VPN's, not home (see the gluetun rule in docker-stack-ops).
Workflow
-
Confirm app, image, ports, and the layout/paths from
infra/docker-stacks.md. -
Create the config dir/dataset with the right owner and
770(on TrueNAS, create the dataset in the UI ormkdirunder the configs dataset). -
Add the service block (single file) or create the stack dir (Dockge/Portainer). Show the user the compose and confirm before applying.
-
Bring it up (state-changing):
hh run <dockerhost> "cd <composedir> && docker compose up -d <service>" -
Verify:
docker ps,docker logs --tail 80 <name>, and the WebUI on its port. -
External access wanted? Add it to the Cloudflare Tunnel (Zero Trust -> the tunnel -> add public hostname) or the reverse proxy - see network-diag and
infra/. Do not publish anything externally without an explicit go-ahead.
Record the new app in infra/docker-stacks.md so the picture stays current.


