Skip to content

CIFS Apps Stack

Compose stack for VM 217 docker-apps-cifs — the apps that need the CIFS shares from the NAS, mounted under /mnt/. The VM’s own 32 GB disk holds little more than each app’s configuration.

GuestVM 217 on pve-dell, 192.168.1.180, 2 cores / 4 GB / 32 GB, onboot: 1
Checkout/home/unknown224/infrastructure-toolkit, tracking main
Shares/mnt/dockerappscifs, /mnt/rustfs, /mnt/qbittorentdownloads, /mnt/gamevault_archives

This is a VM, not an LXC, so pct exec does not reach it — see ../../.claude/host-access.md.

PathContents
docker-compose.ymlinclude: list — the only file compose is invoked on
host/arcane.yamlArcane agent, extends ../../arcane/arcane-agent.yaml
apps/<name>/<name>.yamlOne file per application

dozzle-agent comes from the shared ../dozzle/dozzle-agent.yaml.

Relative bind mounts resolve against the file that declares them, not the project directory: ./config in apps/sportarr/sportarr.yaml is apps/sportarr/config, inside the git checkout.

ServicePort(s)Routed asState
forgejo3000, 222forgejo.toolsera.lanapps/forgejo/data
forgejo-db——apps/forgejo/db
rustfs9000, 9001rustfs.toolsera.lan (:9001)/mnt/rustfs/data
sportarr1867sportarr.toolsera.lanapps/sportarr/config
cleanuparr11011cleanuparr.toolsera.lanapps/cleanuparr/data
arr-dashboard49153 → 3000arr-dashboard.toolsera.lanapps/arr-dashboard/config
neutarr9705neutarr.toolsera.lanapps/neutarr/config
boxarr8888boxarr.toolsera.lanapps/boxarr/config
comicarr8090comicarr.toolsera.lanapps/comicarr/config
opengist6157, 2222opengist.toolsera.lanapps/opengist/data
shelfarr5056shelfarr.toolsera.lanapps/shelfarr/data
shelfarr-libation——volume libation_config
crosswatch8787crosswatch.toolsera.lanvolume crosswatch_config
gamevault-backend8080gamevault.toolsera.lan/mnt/gamevault_archives
db (gamevault)——apps/gamevault/config
dockerproxy2375——
dozzle-agent7007——
arcane-agent3553—volume arcane-agent-data

All eighteen run. Check without logging in — the socket proxy is read-only:

Terminal window
curl -s http://192.168.1.180:2375/containers/json | jq -r '.[]|"\(.Names[0])\t\(.Status)"'

arr-dashboard is on 49153 because forgejo already owns 3000 on this host. rustfs publishes the S3 API (9000) and the console (9001); only the console is routed. forgejo and its database sit on their own bridge network rather than the project default. comicarr keeps its database and settings on the VM disk but reads both libraries off the share, at /mnt/dockerappscifs/comicarr/comics and .../manga, and imports from the same /mnt/qbittorentdownloads staging directory cleanuparr and sportarr use. opengist is a git-backed pastebin sitting alongside Forgejo: its whole state — SQLite database, search index and the gist repositories — is one directory, apps/opengist/data, on the VM disk. It publishes its own SSH server on 2222 for git clone/push, reached at opengist.toolsera.lan:2222 through a Traefik TCP passthrough (../traefik/dynamic/opengist-ssh.yml), the same arrangement Forgejo has on 222.

shelfarr does for audiobooks and ebooks what comicarr does for comics. Its libraries are /mnt/dockerappscifs/shelfarr/audiobooks and .../ebooks, and it imports from the same /mnt/qbittorentdownloads staging directory cleanuparr, sportarr and comicarr use, so the download client’s completed folder has to match. Its SQLite database and auto-generated Rails key stay on the VM disk in apps/shelfarr/data, for the reason Forgejo’s repositories do — the share is nounix with file_mode=0644. There is no setup token: the first account registered in the web UI becomes the admin. shelfarr-libation is the optional Audible-backup companion, internal-only on 8080 and idle until the integration is switched on; it holds the Audible tokens in its own libation_config volume and hands Shelfarr only a read-only token and the finished backups. Upstream’s example pins both images from one SHELFARR_VERSION, which does not work — the companion is tagged with Libation’s versions (0.39.7, 2026.08.31.1) and has no tag in common with shelfarr except latest, so both are pinned to latest here.

Routes are defined in Traefik’s file provider, in the traefik host’s git checkout:

FileCovers
../traefik/dynamic/docker-apps-cifs.ymlthe twelve routed services here
../traefik/dynamic/ugreen-nas.ymlsix apps on the NAS (192.168.1.29)

They used to be labels in VM 217’s Proxmox description, read by the traefik-proxmox-provider plugin. That field is capped at 8192 characters and had reached 7808, so comicarr — needing 397 more than the 384 left — could not be added at all; qm set rejects an over-length value whole and writes nothing. Both groups moved to files, and the description now holds a pointer instead.

Adding a service is therefore two steps, and neither touches Proxmox:

  1. Add apps/<name>/<name>.yaml and an include: entry.
  2. Add a router and service to ../traefik/dynamic/docker-apps-cifs.yml, pointing at http://192.168.1.180:<published port>/.

A service that also needs raw TCP — a git-SSH port — takes a third step, and that one does need a Traefik recreate: a tcp router in its own dynamic file, plus an --entryPoints.<name>.address flag and a published port in the traefik compose file. forgejo-ssh.yml (222) and opengist-ssh.yml (2222) are the two examples. Each such port carries exactly one backend: SSH sends no SNI, so the rule can only be a wildcard HostSNI.

Do not re-add labels to the description. A label block and a file entry for the same host produce two competing routers.

The com.getarcaneapp.arcane.updater labels in these files are read by Arcane; Traefik labels here would still do nothing — the Docker provider is not what routes this guest.

The zone wildcards to Traefik, so a new .lan name needs no DNS work — and an unrouted name resolves and 404s rather than failing. 404 means no route; 502 means routed with no backend.

Nothing here carries a real value; every ${VAR} comes from Bitwarden Secrets Manager at launch.

VariableUsed byDefault if unset
FORGEJO_DB_PASSWORDforgejo, its dbnone — required
FORGEJO_DB_SECRET_KEYforgejonone — required
FORGEJO_DOMAINforgejoforgejo.toolsera.lan
RUSTFS_SECRET_KEYrustfsnone — required
RUSTFS_ACCESS_KEYrustfsrustfsadmin
ARCANE_DOCKER_APPS_CIFS_TOKENarcane-agentnone — required
POSTGRES_PASSWORDboth gamevaultnone — required
METADATA_IGDB_CLIENT_ID / METADATA_IGDB_CLIENT_SECRETgamevaultnone — required
OPENGIST_SECRET_KEYopengistself-generated on disk
OPENGIST_DOMAINopengistopengist.toolsera.lan
TZseveralEurope/Paris

OPENGIST_SECRET_KEY is the one entry here that is genuinely optional. Left unset, Opengist generates a key into apps/opengist/data/opengist-secret.key (0600) and re-reads it on every later start, so sessions survive restarts on their own. Setting it keeps the key in BWS rather than only on the VM disk, so a rebuilt guest or a restored data/ does not invalidate every session. Generate one with openssl rand -hex 32 — hex, because the value is used verbatim as raw HMAC bytes.

shelfarr has an equivalent optional key, RAILS_MASTER_KEY, but it is not wired to a ${VAR} here. Rails treats an empty value differently from an unset one — it fails to decrypt rather than falling back — and setting the key after the first start invalidates everything already encrypted. To keep it in BWS, uncomment the line in apps/shelfarr/shelfarr.yaml with a value from openssl rand -hex 32 before the first run.

arr-dashboard, neutarr, comicarr and shelfarr need none: their keys and setup tokens are generated on first start into their own config/ (data/ for shelfarr).

Terminal window
cd /home/unknown224/infrastructure-toolkit/docker-compose-apps/docker-apps-cifs
sudo -u unknown224 git pull --ff-only
bws run --access-token "$BWS_ACCESS_TOKEN" -- docker compose up -d

/root/infrastructure-toolkit also exists here and is stale — it stopped being the live tree when the stacks moved to unknown224, but it still pulls cleanly, so a git pull there reports success and changes nothing. Verified 2026-09-09 against com.docker.compose.project.working_dir. Pull as unknown224 so the tree does not end up root-owned.

bws run is not optional — a bare up -d starts Forgejo and its Postgres with an empty password and re-registers the Arcane agent with an empty token. Changes flow commit → push → git pull on the host → up -d; editing files on the guest makes the next pull conflict.

  • apps/forgejo/ has no .gitignore, so its state directories — the repos and the database — show up untracked in the host checkout. A git clean -fdx there would delete them. apps/opengist/data is gitignored but holds the gist repositories, which exist nowhere else.
  • GameVault’s Postgres data is at apps/gamevault/config, on the VM’s root disk rather than a share.
  • db is the one compose service key not named for its app, so a second stack file introducing db would collide.