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.
| Guest | VM 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.
Layout
Section titled “Layout”| Path | Contents |
|---|---|
docker-compose.yml | include: list — the only file compose is invoked on |
host/arcane.yaml | Arcane agent, extends ../../arcane/arcane-agent.yaml |
apps/<name>/<name>.yaml | One 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.
Services
Section titled “Services”| Service | Port(s) | Routed as | State |
|---|---|---|---|
forgejo | 3000, 222 | forgejo.toolsera.lan | apps/forgejo/data |
forgejo-db | — | — | apps/forgejo/db |
rustfs | 9000, 9001 | rustfs.toolsera.lan (:9001) | /mnt/rustfs/data |
sportarr | 1867 | sportarr.toolsera.lan | apps/sportarr/config |
cleanuparr | 11011 | cleanuparr.toolsera.lan | apps/cleanuparr/data |
arr-dashboard | 49153 → 3000 | arr-dashboard.toolsera.lan | apps/arr-dashboard/config |
neutarr | 9705 | neutarr.toolsera.lan | apps/neutarr/config |
boxarr | 8888 | boxarr.toolsera.lan | apps/boxarr/config |
comicarr | 8090 | comicarr.toolsera.lan | apps/comicarr/config |
opengist | 6157, 2222 | opengist.toolsera.lan | apps/opengist/data |
shelfarr | 5056 | shelfarr.toolsera.lan | apps/shelfarr/data |
shelfarr-libation | — | — | volume libation_config |
crosswatch | 8787 | crosswatch.toolsera.lan | volume crosswatch_config |
gamevault-backend | 8080 | gamevault.toolsera.lan | /mnt/gamevault_archives |
db (gamevault) | — | — | apps/gamevault/config |
dockerproxy | 2375 | — | — |
dozzle-agent | 7007 | — | — |
arcane-agent | 3553 | — | volume arcane-agent-data |
All eighteen run. Check without logging in — the socket proxy is read-only:
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.
Routing
Section titled “Routing”Routes are defined in Traefik’s file provider, in the traefik host’s git checkout:
| File | Covers |
|---|---|
../traefik/dynamic/docker-apps-cifs.yml | the twelve routed services here |
../traefik/dynamic/ugreen-nas.yml | six 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:
- Add
apps/<name>/<name>.yamland aninclude:entry. - Add a router and service to
../traefik/dynamic/docker-apps-cifs.yml, pointing athttp://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.
Secrets
Section titled “Secrets”Nothing here carries a real value; every ${VAR} comes from Bitwarden Secrets Manager at
launch.
| Variable | Used by | Default if unset |
|---|---|---|
FORGEJO_DB_PASSWORD | forgejo, its db | none — required |
FORGEJO_DB_SECRET_KEY | forgejo | none — required |
FORGEJO_DOMAIN | forgejo | forgejo.toolsera.lan |
RUSTFS_SECRET_KEY | rustfs | none — required |
RUSTFS_ACCESS_KEY | rustfs | rustfsadmin |
ARCANE_DOCKER_APPS_CIFS_TOKEN | arcane-agent | none — required |
POSTGRES_PASSWORD | both gamevault | none — required |
METADATA_IGDB_CLIENT_ID / METADATA_IGDB_CLIENT_SECRET | gamevault | none — required |
OPENGIST_SECRET_KEY | opengist | self-generated on disk |
OPENGIST_DOMAIN | opengist | opengist.toolsera.lan |
TZ | several | Europe/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).
Running
Section titled “Running”cd /home/unknown224/infrastructure-toolkit/docker-compose-apps/docker-apps-cifssudo -u unknown224 git pull --ff-onlybws 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.
Housekeeping
Section titled “Housekeeping”apps/forgejo/has no.gitignore, so its state directories — the repos and the database — show up untracked in the host checkout. Agit clean -fdxthere would delete them.apps/opengist/datais 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. dbis the one compose service key not named for its app, so a second stack file introducingdbwould collide.