torrentvpn stack — 36 containers from one compose file, plus one that is not¶
~/torrentvpn/docker-compose.yml (34 services) + .env. Every service's config lives in ~/torrentvpn/<service>/. The directory is a private Git repo (EmbeddedEra/torrentvpn) with a deny-all .gitignore: only Compose plus grafana/dashboards and grafana/provisioning are tracked. Secrets are ${...} references backed by the mode-0600 .env, which is ignored. samba-nas is not from compose — it was docker run and the compose samba service has drifted from it.
Why this shape¶
One stack dir so one backup script (docker-backup.sh) and one rescue archive cover everything. VPN isolation is by network namespace: qBittorrent runs inside gluetun's, so torrent traffic cannot leave except through the tunnel.
Shape¶
Services by group (compose line numbers, 09-08):
- VPN + downloads: gluetun:2 (NordVPN; qbittorrent:40 uses network_mode: service:gluetun), unpackerr:58, rar-extractor:636, prowlarr:184, flaresolverr:265
- Media: plex:93 (/mnt/storage/{movies,tv,anime,kids-*}, all :ro + no-new-privileges since 2026-10-18 incident), jellyfin:280 (/mnt/storage:/data:ro), tautulli:557, tdarr:572 + tdarr-node:592 (no libraries configured — item 16 note), bazarr:212, overseerr:125
- *arr: radarr:139, sonarr:154, recyclarr:198 (sonarr-anime retired 2026-10-20, merged into sonarr; config kept in sonarr-anime/) (image pinned :8 since 2026-10-20, v8 trash_id configs in recyclarr/configs/*.yml, old recyclarr.yml renamed .disabled)
- Photos: immich-server:456, immich-machine-learning:481, immich-postgres:438, immich-redis:451 → immich
- Network: pihole:357 (dns.lan, resolves *.lan), npm:527 (npm.lan:81, proxies *.lan), wg-easy:327 (wg.lan; wg-docker-watchdog.timer restarts it every minute if stopped), duckdns:542
- Secrets: vaultwarden (end of file; vault.embeddedera.com via NPM, HTTPS only) → vaultwarden. On ee-vault, alone with npm (2026-09-24) — see Networks below.
- MRP/inventory: inventree-db/cache/server/worker/proxy (end of file; erp.lan via NPM, LAN-only for now) → inventree. On ee-erp, not the default network (2026-09-24) — see Networks below.
- Marketing: postiz + postiz-postgres/redis + postiz-temporal/temporal-postgres/temporal-es (end of file; social.embeddedera.com via tunnel, loopback 4007) → postiz
- Homepage (dashboard, replaced Homer 2026-10-18; not in this compose): own project ~/homepage (compose + config/*.yaml + neon custom.css, .env 0600), http://192.168.5.15:8092, also homepage.lan and homer.lan (NPM proxy host 1 → 192.168.5.15:8092; HOMEPAGE_ALLOWED_HOSTS lists the names). Start: cd ~/homepage && docker compose up -d. AI usage + "AI resets" tiles and the VPN tile read the hub (/api/ai-usage, /api/vpn-status, /api/vpn-ok). Homer container removed; its compose block is a comment; homer/assets/ stays because vpn-check writes there and filebrowser mounts it. Backups: docker-compose.yml.bak-20261018-homer-retire, NPM database.sqlite.bak.20261018-homer-retire. 2026-10-25: image updated to v2.7.0 (:latest); ./icons mounted at /app/public/icons (custom tiles use icon: /icons/<name>.svg, e.g. home-assistant.svg, same neon style as icon.svg); /mnt/storage mounted ro,rslave so the top resources widget shows NAS pool space; tiles added for Home Assistant (widget homeassistant: Indoor temp, HVAC mode, TVs on, Angel home; long-lived token homepage as HOMEPAGE_VAR_HA_KEY, copy in ~/.config/homeassistant/env) and Samba NAS (smb:// link). Also a Bambu H2S tile after Home Assistant (widget homeassistant: status, progress, time left, nozzle/bed), and the HA 'TVs on' count now only counts media_player with device_class tv in on/playing/paused (read 13 with Echos/Spotify/Cast, now 2). Backups *.bak-20261025-*.
- Home automation (2026-10-24): homeassistant (end of services, network_mode: host, http://192.168.5.15:8123, config ./homeassistant, image :stable, 2026.10.4) → ../../../projects/kitchen-hub. No Bluetooth caps (NET_ADMIN/NET_RAW) by choice. Custom components (HACS): alexa_media, tuya_local, bambu_lab (ha-bambulab 2.2.26, H2S LAN-mode entry, 2026-10-25), u_tec (2026-10-29, below).
- Scent diffuser (2026-11-08): Aroma-Link Smart.A5.WIFI (MAC 24:19:72:46:CC:0B, LAN 192.168.4.90, cloud device 389973). HACS mr-sparks/scent-assistant 1.2.2, scent_assistant in cloud mode (no Bluetooth adapter on this server; polls every 60 s; no fan control over cloud). Login in ~/.config/aroma-link/env 0600. Entities *.diffuser_389973_*, e.g. switch.diffuser_389973_power. Verified 2026-11-08: power off and on from HA, cloud state followed. oil_remaining reads unavailable. Fan from HA would need an ESP32 Bluetooth proxy near the diffuser.
- Smart scent (2026-11-08): diffuser doses follow the AC blower. Toggle input_boolean.smart_scent; automations smart_scent_dose (fan attribute of climate.home to on, or every 5 min with 20 min cooldown on long runs; 08:00-21:00; person.angel home or binary_sensor.home_occupancy; power on up to 3 min or until fan off) and smart_scent_safety_off (power on 5 min or HA start: off, so manual app runs get cut off while the toggle is on). Diffuser set 30 s work / 30 s pause (was 60/60). Spray starts within ~10 s of power-on, so a dose is ~90 s. No audible beep on power changes (owner). ecobee reports about every 3 min. Verified 2026-11-08 on 3 real fan cycles. Backup automations.yaml.bak-20261108-scent.
- Back door lock (2026-10-29): Ultraloq U-Bolt Pro WiFi (U-tec, app now "Xthings Home"). No local API/Matter on this model; Alexa Media Player has no lock platform. Added via HACS Uhome-HA u_tec 0.6.3 (cloud OpenAPI). Entities: lock.back_door_back_door, binary_sensor.back_door_door, sensor.back_door_battery. Client ID/secret from the app (My Account > OpenAPI, RedirectUri https://my.home-assistant.io/redirect/oauth); Angel holds them. Options: poll 20 s, optimistic locks off, Adaptive Aggressive all locks. Push never arrives (webhook is LAN-only, no Nabu Casa) so state is poll-only. HA internal_url set to http://192.168.5.15:8123 (was unset). Gotcha: my.home-assistant.io remembered homeassistant.local:8123, which doesn't resolve here; the OAuth callback fails unless the instance URL there is edited to 192.168.5.15 (or replay the /auth/external/callback?code&state URL against 192.168.5.15 within minutes). Verified 2026-10-29 with Angel at the door: lock from HA -> locked in 10 s, unlock -> unlocked in 18 s; Angel confirmed the bolt physically moved both times (first try failed while the door was open). binary_sensor.back_door_door is NOT trustworthy: read closed with the door open (API door_state null).
- Voice for HA Assist (2026-10-25): whisper (rhasspy/wyoming-whisper, --model small-int8 --language en since 2026-10-25 (base misheard names), ./whisper) and piper (rhasspy/wyoming-piper, --voice en_US-lessac-medium, ./piper), right after homeassistant; published on 127.0.0.1 only (10300 / 10200) since HA is host-network. HA Wyoming integrations point at 127.0.0.1. Backup docker-compose.yml.bak-20261025-wyoming. Used by the kitchen panel.
- Home Assistant (host network, :8123): homeassistant.lan and ha.lan via NPM proxy host 29 (→ 192.168.5.15:8123, websockets on). Verified 2026-10-25: both names 200, /api/websocket 101; trusted proxy 172.16.0.0/12 set in the UI and promoted (NPM connects from 172.29.0.8; without it HA answers 400). HA 2026.10 keeps HTTP settings in .storage/http (yaml_migration_done: true) and ignores a YAML http: block — set reverse proxy in UI Settings > System > Network, then confirm (pending config reverts in ~5 min unless promoted). The YAML block added 2026-10-25 is dead; restore configuration.yaml.bak-20261025-ha-proxy.
- Ops: portainer:503, uptime-kuma:514, dozzle:427, glances:411, scrutiny:690, filebrowser:393 (files.lan, /mnt/storage as /srv/media), mosquitto:674
- samba:615 — not compose-managed. Verified 2026-09-24: the live samba-nas binds exactly what the compose service declares — /mnt/storage/EmbeddedEra, /mnt/storage/Storage-snapshots, /home/angel/erp-config — so the bind-path drift previously recorded here is gone. What remains is the name collision: samba-nas was docker run, so compose does not own it and a bare docker compose up -d still fights for the container name. The stopped abd5e1804968_samba-nas (Created 2026-09-17, never started) is leftover debris from a run that tried.
- samba-nas -p strips exec bits (found 2026-10-22). dperson/samba's -p runs a background chown -R + find -exec chmod 0664/0775 over every share on each container start (~80 min across all three). It reset all of ~/erp-config at 13:39:49 on 10-21 (start 12:18) and on 10-19, breaking tester-report-sync with 203/EXEC. smbuser is uid 1000, so the chown is a no-op and -p has no use. Removed from compose; the live container still has it until ~/torrentvpn/samba-nas-drop-p.sh runs (recreates with the same volumes, keeps samba-nas-old-p for rollback; config backup samba-nas.inspect.bak-20261022.json, 0600, holds share passwords).
- samba-nas users (2026-11-11): angel, adiaz, yara. yara added live (Alpine adduser -D -H, smbpasswd -a, valid users on [EmbeddedEra] only, smbcontrol all reload-config; backup /etc/samba/smb.conf.bak-20261111 in the container) and in compose (-u yara;…, EmbeddedEra users field; uncommitted, file backup docker-compose.yml.bak-20261111) so a recreate keeps it. Read/write to all of EmbeddedEra; writes land as smbuser via force user; deletes go to .deleted (recycle). Password: ~/.config/samba-nas/yara.env (0600); Vaultwarden entry still to add (bw CLI broken: no node).
Networks (changed 2026-09-24)¶
The stack is no longer one flat network. torrentvpn_default still carries the media and ops
containers, but the business-critical ones were moved off it so that compromising a container
that parses untrusted internet content (flaresolverr, rar-extractor, the *arr stack) no
longer reaches the ERP database or the password vault.
| Network | Members | Notes |
|---|---|---|
ee-erp |
inventree-db/cache/server/worker/proxy, npm, + metabase from its own project | ERP_DOCKER_NETWORK=ee-erp in ~/.config/metabase/env |
ee-vault |
vaultwarden, npm | nothing else reaches it |
torrentvpn_kanboard-net |
kanboard, kanboard-proxy | pre-existing |
torrentvpn_default |
everything else, including npm | npm is the deliberate bridge |
Both are plain bridges — never internal: true, which disables port publishing and would
kill inventree-proxy's 127.0.0.1:8100; the same mistake took Kanboard down on 2026-09-14.
Isolation is by membership only.
ee-network-check asserts this and exits non-zero if anything drifts back. ee-network-apply
re-applies it (it is the only safe way — metabase needs --env-file ~/.config/metabase/env or
every variable resolves blank). Design: ../../../decisions/embeddedera-infrastructure-split.
Rules:
- glances binds docker0 only (2026-10-19): GLANCES_OPT=-w -B 172.17.0.1 (host network, so the LAN IP refuses). Homepage widget reaches it via host.docker.internal (= 172.17.0.1); NPM proxy host 20 (glances.lan) must forward to 172.17.0.1:61208, not 192.168.5.15. portainer, dozzle, glances, uptime-kuma images refreshed 2026-10-19 (were 8-11 months old); uptime-kuma stays on the :1 tag.
- Never a bare docker compose up -d. Always name the service: docker compose up -d scrutiny. Stop/start samba-nas by name with docker stop/start.
- Recreate gluetun and qbittorrent together or qbittorrent loses its network.
- gluetun's ICMP health probe fails forever on NordVPN; gf fixes it (HEALTH_TARGET is the wrong variable name on this build).
- A new public hostname needs docker exec pihole reloaddns. Pi-hole caches the NXDOMAIN from before the DNS record existed, so LAN clients get "no such host" while public DNS is fine (hit 2026-09-19 with tester.embeddedera.com). Nothing to configure: just clear the cache after cf-host add.
- Pi-hole blocklists: no Block List Project ads/tracking/malware/abuse/redirect lists; they block email click-links (Hilton, Schwab) and at times bank logins (Chase, BofA in Aug 2026). Ads and threats come from HaGeZi Multi PRO + TIF, which leave email links alone. BLP content-category lists (porn, gambling, social…) stay. One broken link → docker exec pihole pihole -q <domain> names the list, then pihole allow <domain>.
- Radarr/Sonarr/Sonarr-anime: removeCompletedDownloads=false and removeFailedDownloads=false — this is the safety interlock that prevents the 09-03 incident. DBs backed up as *.db.bak.incident-20260903.
- .env is 0600; the only off-box copy is env.gpg on the rescue USB.
Leftovers in the dir: prowlarr-broken-2025* ×4, immich-backup-20251205-*, snapraid-webui/, h5ai/, jellystat/, openvpn/ — config dirs with no running container. The 17 manual docker-compose.yml.bak* files were moved to the recoverable desktop trash on 2026-09-18 after Git history was established.
Media automation state (2026-10-20)¶
- Prowlarr (v1 API) pushes to Radarr/Sonarr/Sonarr-anime by tag:
anime-> Sonarr,movies-> Radarr,tvandanime-> Sonarr; tagflaresolverr(id 1) is the Cloudflare proxy. Add a new indexer with the right routing tag or it syncs everywhere/nowhere. - Recyclarr profiles: Radarr "HD Bluray + WEB", Sonarr "WEB-1080p", Sonarr also "[Anime] Remux-1080p" (one
web-1080p.yml; never two Recyclarr files for one Sonarr, it skips both). Older profiles are untouched; Overseerr defaults point at the new ones, so only new requests use them. Radarr, Sonarr and Prowlarr have Allowed Hosts set (<name>,<name>.lan,localhost,127.0.0.1, since 2026-10-20; a Host outside that list gets 400, so a new caller must use a container name or.lanname, never the container IP). Sonarr renames new episodes (renameEpisodestrue, existing files not bulk-renamed). Safety interlockremoveCompletedDownloads=false/removeFailedDownloads=falseon all three download clients is still in force. - Bazarr talks to Sonarr + Radarr; no OpenSubtitles.com key yet.
- Overseerr has one Sonarr server: TV -> "WEB-1080p"
/data/tv; anime (auto-detected) -> "[Anime] Remux-1080p"/data/anime, series type anime. Kids routing is manual (/data/kids-movies,/data/kids-shows). - Recycle bin:
/data/.recycle-bin(host/mnt/storage/.recycle-bin, 14 days) on all three arrs. - Kuma monitors for Prowlarr, Bazarr, Overseerr, Tautulli, FlareSolverr (ids 14-19) use the shared "Ntfy IPhone" notification; gluetun is covered by the QBittorrent monitor.
- Downloads:
/data/downloads/*is plain torrent data, not a library; no arr root folder points into it (the 8 empty wrong-match Sonarr series were removed 2026-10-20).
Connected to¶
- owns: every container's config dir; the
.env - owned-by: host/machine
- joins: storage/mergerfs-pool (all media binds), storage/rescue-usb (nightly archive of this dir), automation/alerting
If you change this¶
- Hits:
docker-backup.shsnapshot contents (skipsVideos media immich wireguard-wg_confs docker-backups+ caches) · the rescue archive · Pi-hole/NPM entries if a hostname changes · homerconfig.ymllinks · the *arr interlock if you touch a download client. - Does not hit: SnapRAID, mergerfs, rsnapshot, precious.
Surfaces¶
| Surface | Role |
|---|---|
dps alias |
docker ps table |
Portainer (portainer.lan), Dozzle (logs) |
UI |
docker-backup.sh 01:00 |
rsync of config dirs → /mnt/storage/docker-backups/snapshots/ (7 kept); log /var/log/docker-backup.log (832 MB, unrotated) |
See¶
- Source:
/home/angel/torrentvpn/docker-compose.yml·/usr/local/bin/docker-backup.sh·~/.local/bin/gf
qBittorrent seed limit: OFF (2026-10-20)¶
Global limits stay -1 (ratio, seeding minutes, inactive minutes) and arr "Remove Completed"/"Remove Failed" stay false: see the interlock above. 2026-10-20 a limit (1 / 14 d / 7 d) plus Remove Completed deleted ~77 torrents' data again (library intact via hardlinks); reverted the same day. Change via the WebUI API (app/setPreferences) or the conf with the container stopped. WebUI login is saved in ~/.config/qbittorrent/env (0600); Homepage tile shows a qBittorrent widget (HOMEPAGE_VAR_QBT_* in ~/homepage/.env). Reconsider only with per-tracker rules and a dry-run proving a stopped torrent cannot trigger removal.
Compose parse trap (2026-10-24)¶
A # comment inside a folded command: > block becomes part of the command, and compose refuses the whole file (services[samba].command invalid command line string). The 10-22 samba comment did this; moved above command: in commit 2b2db91. Run docker compose config --quiet after any edit. Also: samba's adiaz password is plaintext in the tracked compose — should be a ${...} from .env.