Operational runbook for the Nextcloud “Drive” stack (Nextcloud + Postgres + Redis + OnlyOffice + tsauth-proxy + drive-mcp) and the code-server browser IDE, both on telep-mainframe. This is the closing deliverable of 2026-08-31-nextcloud-drive-tailscale-plan (design of record: 2026-08-31-nextcloud-drive-tailscale-spec). Everything below is verified state as of 2026-08-31 — a future operator or agent should be able to act from this page without re-deriving anything.

The host this all runs on has a known CPU hardware fault

telep-mainframe panics under sustained all-core load — see 2026-08-31-telep-mainframe-mce-hardware-fault. Any heavy job driven from this stack (a large files:scan, a big OnlyOffice conversion batch, a code-server build) can take the box down. Crash-capture instrumentation is listed under Crash capture and thermal instrumentation.

Standing rule — NO FUNNEL. EVER.

Every service here is tailscale serve tailnet-only. Do not add tailscale funnel to any of these nodes. This is absolute for code-server, which runs with --auth none: tailnet membership IS the credential. A Funnel on code would publish an unauthenticated remote shell + file browser to the public internet. Verify at any time with serve status — every mount must print (tailnet only).


Architecture at a glance

The per-service Tailscale sidecar pattern (the house convention — see tailnet-service-exposure-convention): each service gets its own userspace tailscaled, its own tailnet hostname, and a tailscale serve mount that fronts a loopback-only port.

tailnet client (identified by Tailscale)
   │  HTTPS, tailnet-only, LetsEncrypt cert for <svc>.taild4189d.ts.net
   ▼
tailscaled-<svc>.service      --tun=userspace-networking
   socket   /run/tailscale-<svc>/tailscaled.sock
   statedir /var/lib/tailscale-<svc>
   --port=0
   │  tailscale serve --bg --https=<port> http://127.0.0.1:<loopback>
   ▼
127.0.0.1:<loopback>          the app. NEVER bound to LAN or tailnet IP.

Each sidecar is a separate systemd unit and a separate tailnet device, so ACLs, sharing and node lifecycle are per-service.

Nodes in scope

NodeUnitSocketStatedirFronts
drivetailscaled-drive.service/run/tailscale-drive/tailscaled.sock/var/lib/tailscale-driveNextcloud (via tsauth-proxy) + OnlyOffice + drive-mcp
cadtailscaled-cad.service/run/tailscale-cad/tailscaled.sock/var/lib/tailscale-cadFreeCAD noVNC, exports index, Online3DViewer, FreeCAD MCP
knowledgebasetailscaled-kb.service/run/tailscale-kb/tailscaled.sock/var/lib/tailscale-kbKB site + telep-kb MCP
codetailscaled-code.service/run/tailscale-code/tailscaled.sock/var/lib/tailscale-codecode-server

Socket dir ≠ hostname for the knowledge base

The KB node’s hostname is knowledgebase but its unit/socket/statedir are named kb (/run/tailscale-kb/…). Every other node here matches. If --socket=/run/tailscale-knowledgebase/… errors with “no such file or directory”, that’s why.

For Agents

telep-mainframe cannot reach its own tailnet-only HTTPS URLs — the same-host userspace-tailnet hairpin times out. That is expected and is NOT a fault. Prove things over loopback (127.0.0.1:<port>) instead; the tailnet hop can only be confirmed from a separate in-ACL device. See Verification checklist.


Port map

Everything binds loopback only; the tailnet ACL is the perimeter. There is no LAN listener for any of these.

These backends now bind 127.0.0.2, NOT 127.0.0.1 — do not "correct" them back

A systemic finding this session: tailscale serve does not close the userspace-networking raw forwarder, which forwards the node’s tailnet IP to 127.0.0.1:<port> as plaintext, bypassing the TLS front door. On drive/code this had exposed raw SSO-bypassing Nextcloud (:11000) and an unauthenticated code-server shell (:8888) in cleartext to the whole tailnet. The fix moved these backends to 127.0.0.2 (the forwarder only targets 127.0.0.1, so the plaintext door goes dead while serve, re-pointed at 127.0.0.2, keeps working): Nextcloud 127.0.0.2:11000, tsauth-proxy 127.0.0.2:11001, OnlyOffice 127.0.0.2:11002, drive-mcp 127.0.0.2:9100, code-server 127.0.0.2:8888. Any NEW sidecar must bind 127.0.0.2 from the start. Test closure by IPv4 (100.x), never by hostname (MagicDNS resolves IPv6 first, which the forwarder ignores → false “closed”). Full detail, the box-wide sweep, and the still-open cad:9875 XML-RPC surface: 2026-08-31-tailnet-plaintext-port-hardening. The loopback ports in the table below are otherwise unchanged; wherever it reads 127.0.0.1, the SSO/no-auth backends listed here are now 127.0.0.2.

LoopbackOwnerFronted byNotes
127.0.0.1:11000Nextcloud apache (docker-proxy → container :80)(none directly)Deliberately not served. Reachable only via tsauth-proxy.
127.0.0.1:11001tsauth-proxy (Go, systemd, runs as root)https://drive.taild4189d.ts.netIdentity injector. See .
127.0.0.1:11002OnlyOffice DocumentServer containerhttps://drive.taild4189d.ts.net:8443Browser-facing editor origin.
127.0.0.1:9100drive-mcp — WebDAV-backed MCP serverhttps://drive.taild4189d.ts.net:8444/mcpServed from the existing drive sidecar — no new tailnet node. See .
127.0.0.1:8080Stirling-PDF containerhttps://pdf.taild4189d.ts.netOwn sidecar tailscaled-stirling.service.
127.0.0.1:8087o3dv container — landing + /o3dv/ viewer + /exports/, one originhttps://cad.taild4189d.ts.net:8090The “O3DV landing on 8090” is the tailnet port; the loopback is 8087.
127.0.0.1:8092Knowledge base web apphttps://knowledgebase.taild4189d.ts.net2026-07-24-knowledgebase
127.0.0.1:9099telep-kb MCP serverhttps://knowledgebase.taild4189d.ts.net:8443kb-agent-api
127.0.0.1:3010homepage dashboard (next-server, host networking)https://home.taild4189d.ts.netHost-networked → its siteMonitors use loopback.
127.0.0.1:8888code-server containerhttps://code.taild4189d.ts.netNo auth. Node is tagged tag:telep.

The drive node now serves THREE ports

443 (Nextcloud via tsauth-proxy) · 8443 (OnlyOffice DocumentServer) · 8444 (drive-mcp). All three are (tailnet only). Check them together:

sudo -n tailscale --socket=/run/tailscale-drive/tailscaled.sock serve status

Adjacent mounts on the same cad node, from the FreeCAD workstation build (listed so nobody reuses their ports):

LoopbackOwnerFronted by
127.0.0.1:3080FreeCAD noVNChttps://cad.taild4189d.ts.net
127.0.0.1:8085exports-http nginx autoindexhttps://cad.taild4189d.ts.net:8080
127.0.0.1:9876FreeCAD MCPhttps://cad.taild4189d.ts.net:8443

127.0.0.1:8090 is NOT O3DV

Loopback 8090 is occupied by an unrelated local python3 listener. O3DV’s 8090 is a tailscale serve HTTPS port on the cad node that proxies to loopback 8087. Don’t pick 8090 for a new service and don’t curl 127.0.0.1:8090 expecting the viewer.


Identity

Nextcloud has no password prompt for tailnet users. Identity comes from Tailscale itself:

tailnet client
  → drive node, tailscale serve :443 (TLS)          sets X-Forwarded-For = client tailnet IP
  → 127.0.0.1:11001  tsauth-proxy
        · reads X-Forwarded-For
        · tailscale --socket=/run/tailscale-drive/tailscaled.sock whois --json <ip>
        · resolves UserProfile.LoginName (e.g. ledererandras2004@gmail.com)
        · OR, for a TAGGED device, maps a configured tag → a Nextcloud user (see below)
        · STRIPS any inbound Remote-User / Tailscale-User-Login, then sets  Remote-User: <login>
  → 127.0.0.1:11000  Nextcloud apache        sees $_SERVER['HTTP_REMOTE_USER']
  → user_saml in "environment-variable" mode → auto-provisions + logs the user in
  • App: user_saml 8.3.0, type = environment-variable.
  • Attribute mapping lives in the provider DB record, provider id 1not in appconfig: occ saml:config:set 1 --general-uid_mapping=HTTP_REMOTE_USER --saml-attribute-mapping-email_mapping=HTTP_REMOTE_USER --saml-attribute-mapping-displayName_mapping=HTTP_REMOTE_USER
  • Break-glass local admin is retained: https://drive.taild4189d.ts.net/index.php/login?direct=1, password in /home/levander/nextcloud/.env.
  • trusted_proxies = 127.0.0.1, 172.23.0.1, 172.23.0.0/16 (docker bridge gw + subnet, the tsauth-proxy source as seen by the container).

Why this is loopback-safe

Remote-User is trusted because only tailscale serve can reach the proxy: tsauth-proxy listens on 127.0.0.1:11001 and nothing else on the box fronts it, and the proxy unconditionally strips any client-supplied Remote-User before setting its own. The trust boundary is “can you open a socket on this host’s loopback” — i.e. a local root/shell compromise, which already owns the box.

This is why there is NO LAN vhost

A Caddy *.telep.lan entry point for Drive was deliberately skipped. A LAN vhost would arrive without a tailnet source IP, so whois yields nothing and the identity layer is bypassed — a LAN client would either get no identity or (worse) be able to hand-craft Remote-User. Drive is reachable only over the tailnet, on purpose. Do not “helpfully” add a LAN vhost later.

Tag identity — mapping a tagged device to a Nextcloud user

Tagged Tailscale devices have no user for whois to resolve. tsauth-proxy can map a configured tag to a Nextcloud username via TS_TAG_USERS in /etc/tsauth-proxy.env (an env file, chmod 600 — read it on the box, never here).

Security properties worth recording, because they are what makes this acceptable:

PropertyBehaviour
Explicit allowlist onlyOnly tags named in the config map to a user. An unlisted tag resolves to nothing.
tag:telep maps to NOTHINGThe house-wide tag deliberately grants no Nextcloud identity. Tagging a device tag:telep does not silently give it Drive access.
Header stripping is unconditionalClient-supplied Remote-User / Tailscale-User-Login are stripped after identity resolution, so a tagged client cannot smuggle a different user.
Fails closedNo resolvable identity → no Remote-User header → Nextcloud does not auto-login. It does not fall back to a default user.

Tagging a device REPLACES its personal Tailscale identity

This is a Tailscale property, not ours. Once a device is tagged, whois returns the tag and not the human who owns it — so any other tailnet service that identifies users by LoginName will stop recognising that device. Do not tag a device you use interactively as yourself unless you have accounted for every service that whois-identifies it.

tsidp was the plan's primary and was NOT used

tsidp builds fine (/home/levander/gobin/tsidp) but needs either its own owner-authed tailnet node or -use-local-tailscaled (which collides with the host node’s already-bound :443). The whois→header fallback was taken per the plan’s “don’t thrash” rule. If a standards-based OIDC IdP is wanted later, tsidp needs its own node.


External storage

Two external storages are mounted into Drive. They are managed completely differently — one is a read/write working area, the other is a strictly read-only window onto the Windows install.

Storage idDrive pathNextcloud compose bind (host → container)datadirMode
1/CAD/home/levander/freecad/exports/mnt/cad/mnt/cadrw — the FreeCAD/CAD-designer export pipeline writes here
2/windows/mnt/win/mnt/windows :ro/mnt/windows/Usersreadonly: "1" — read-only, enforced at four independent layers

Host says win, container says windows — they are not typos of each other

The NTFS volume is mounted on the host at /mnt/win; the Nextcloud container sees it at /mnt/windows. occ output and docker compose exec paths use the container form; findmnt / fstab use the host form. Confirm with occ files_external:list.

Nextcloud binds the WHOLE C: drive and narrows to Users in config — code-server narrows in the bind

This asymmetry is deliberate and is the single most useful thing to know about this storage.

BindWhere the narrowing to Users happens
Nextcloud/mnt/win:/mnt/windows:ro — the entire Windows C: driveat the Nextcloud layer, via storage id 2’s datadir: /mnt/windows/Users
code-server/mnt/win/Users:/home/coder/windows:ro — only Usersin the bind itself; the container never sees the rest of C:

Consequence — widening Drive to the full C: drive is ONE command, no compose edit, no restart:

docker compose exec -T --user www-data nextcloud php occ files_external:config 2 datadir /mnt/windows

The bind already covers it. Do not conclude from the datadir alone that compose needs touching. (Narrowing to a different subtree is the same command with a different path.) code-server is the opposite case: changing its view of the Windows disk does require a compose edit, because its bind is the narrowing.

Verify the live bind rather than trusting either table:

docker inspect -f '{{range .Mounts}}{{.Source}} -> {{.Destination}} (rw={{.RW}}){{"\n"}}{{end}}' \

$(docker compose -f /home/levander/nextcloud/docker-compose.yml ps -q nextcloud)

/windows — read-only at four layers

The Windows NTFS volume is exposed so files can be found and read from Drive, code-server and MCP clients. It must never be writable: a stray write into a live Windows install’s Users tree is unrecoverable, and NTFS-under-ntfs3 write support is not something to bet a Windows install on.

#LayerMechanism
1Kernel mount/etc/fstab mounts /mnt/win with ntfs3 and the ro option
2Docker bindboth consumers bind :ro — Nextcloud /mnt/win:/mnt/windows:ro, code-server /mnt/win/Users:/home/coder/windows:ro
3Nextcloudthe storage is configured readonly=1, applied via a PermissionsMask storage wrapper — Nextcloud masks the permission bits itself, so the UI hides write/rename/delete rather than offering them and failing
4code-serverfiles.readonlyInclude marks the path read-only in the editor, so VS Code refuses the save before it reaches the (read-only) mount

Defence in depth is deliberate: layer 3 is the only one the user sees, layers 1–2 are the ones that actually hold if 3 is misconfigured, and layer 4 stops the most likely accidental write (someone editing a config file in the IDE).

/etc/fstab mounts /mnt/win by UUID — never by device node

The two NVMe device nodes swap across boots: what is nvme0n1p2 on one boot can be nvme1n1p2 on the next. A device-node mount would, sooner or later, mount the wrong disk at /mnt/win — which on this box means mounting (part of) the LUKS/LVM OS drive’s neighbour instead of the Windows volume, and exposing the wrong tree to Drive, code-server and MCP. If you ever edit this fstab line, keep it UUID=. Verify with lsblk -o NAME,UUID,MOUNTPOINT and findmnt /mnt/win after any disk change.

filebrowser is retired — /windows replaced it

filebrowser was the only other door onto the same /mnt/win data. Leaving it running would have meant two independently-configured read-only enforcements on one dataset. The container is stopped, its volumes are preserved (so it can be brought back), and its tailscale serve :8445 mount has been removed. If something still points at :8445, that is the reason it 404s.

Checking both storages

docker compose exec -T --user www-data nextcloud php occ files_external:list
docker compose exec -T --user www-data nextcloud php occ files_external:verify 1     # /CAD      datadir /mnt/cad
docker compose exec -T --user www-data nextcloud php occ files_external:verify 2     # /windows  datadir /mnt/windows/Users, readonly "1"
findmnt /mnt/win                                                                     # HOST path — must show ro and a UUID= source
docker inspect -f '{{range .Mounts}}{{.Source}} -> {{.Destination}} (rw={{.RW}}){{"\n"}}{{end}}' \
  $(docker compose -f /home/levander/nextcloud/docker-compose.yml ps -q nextcloud)
#   expect:  /mnt/win -> /mnt/windows (rw=false)
docker compose exec -T nextcloud sh -c 'touch /mnt/windows/Users/.probe'; echo "exit=$?"   # CONTAINER path — must FAIL

Scheduled maintenance — nextcloud-scan.timer

Fires every 3 minutes (OnBootSec=2min, OnUnitActiveSec=3min, Persistent=true). nextcloud-scan.service runs as root (so the normalization steps have privilege) and only the scan drops to www-data:

[Service]
Type=oneshot
ExecStartPre=/usr/bin/find /home/levander/nextcloud/data ! -uid 33 -exec chown 33:33 {} +
ExecStartPre=/usr/bin/find /home/levander/freecad/exports ! -group www-data -exec chgrp www-data {} +
ExecStartPre=/usr/bin/find /home/levander/freecad/exports -type f ! -perm -020 -exec chmod g+rw {} +
ExecStart=/usr/bin/docker compose -f /home/levander/nextcloud/docker-compose.yml exec -T --user www-data nextcloud php occ files:scan --all --home-only --quiet
ExecStart=/usr/bin/docker compose -f /home/levander/nextcloud/docker-compose.yml exec -T --user www-data nextcloud php occ files:scan --path="<user>/files/CAD" --quiet

Order matters: normalize data dir → normalize exports group → exports g+rw → scan home dirs → scan /CAD. find … -exec … {} + exits 0 when nothing matches, so an ExecStartPre never spuriously fails the unit.

The scan MUST NOT be allowed to walk /windows

A naive files:scan --all now includes the /windows external storage — a 123 000-entry NTFS tree. Measured: it exceeded 10 minutes and never completed, on a 3-minute timer. That means overlapping scans, a permanently-busy box, and — given the host’s CPU fault — sustained load that can panic the machine.

The fix is the two-ExecStart form above: --home-only restricts the sweep to users’ own home storages (excluding all external mounts), and a targeted --path= scan picks up /CAD, the only external storage that actually changes. Measured cost of the current form: 0.83 s. /windows is read-only and effectively static, so it is scanned on demand only.

If you ever “simplify” this back to files:scan --all, you will reintroduce a >10-minute job on a 3-minute timer.

Why the two shares are normalized DIFFERENTLY

ShareHost pathActionWhy
Nextcloud data/home/levander/nextcloud/datachown 33:33owner AND group takenNothing but Nextcloud writes here. www-data (uid 33) must fully own the tree to rename/edit/delete from the web UI.
CAD exports/home/levander/freecad/exportschgrp www-data + chmod g+rwowner PRESERVEDThe FreeCAD/CAD-designer export pipeline writes here as levander and must keep its owner-write bit. Taking the owner would break that pipeline. Management from the web UI is granted via the group instead: dir is setgid 2775 group www-data, files g+rwwww-data can rewrite files and unlink them through the directory’s group-write, regardless of who owns each file. other r-x/r-- is preserved so O3DV’s read-only consumer is untouched.

Up to a 3-minute lag before a Taildrive write is editable from the web

A file written over Taildrive lands root:root immediately. Until the next timer fire (≤3 min) Nextcloud can index and read it but cannot rewrite/delete it. After the fire it is normalized and fully manageable. This is by design — an inotify/.path unit would close the gap but was explicitly out of scope.

Per-cycle cost

Both find sweeps plus the two scoped scans run every 3 min, at 0.83 s measured. If the data dir grows large, scope the finds to hot subpaths too rather than lengthening the interval.

Crash capture and thermal instrumentation

Not part of the Drive stack, but they run on the same host and they are what you will need when it dies mid-operation. Full story: 2026-08-31-telep-mainframe-mce-hardware-fault.

Unit / fileWhat it does
netconsole-target.service (host, enabled) → netconsole-rx on telep-routerships kernel messages off-box over UDP; the only thing that captures the MCE panic. Log: telep-router:/tmp/netconsole/kmsg.logRAM-backed, copy it off first thing after a crash
thermalwatch.service (host, enabled) → /var/log/thermalwatch/samples.log10 s samples, fsync’d per line, ~10 days; tags the suspect core as SUSPECT_core16_cpu8_9=
/etc/sysctl.d/99-crash-capture.confkernel.printk = 5 4 1 7 — the box boots quiet (console_loglevel 4), which was silently dropping every KERN_WARNING including lockup and hung-task warnings
# all three of the box's own added units should report "enabled"
/usr/bin/ssh levander@telep-mainframe \
  'systemctl is-enabled drive-mcp.service netconsole-target.service thermalwatch.service'

/sys/fs/pstore will be EMPTY after a panic — that is expected here

efi_pstore_write runs in <#MC> context where the FPU is unavailable, so the EFI variable write always fails for this failure mode. Don’t chase it.

Retrieval commands are in What to do after the next crash.


File actions — what opens what

Clicking a file in Drive routes to a purpose-built viewer/editor rather than downloading. The click-default routing table (by lowest-order enabled action carrying a default):

MatchesClick-default opens in
.stl .3mf .obj .gltf .glb .ply .step .stpOnline3DViewer on the cad node
.FCStd under /CADFreeCAD GUI (via the freecad-open shim — see below)
.csv .docx (+ the office set)OnlyOffice (native defFormats, not a custom action)
source/code files (.py .js .sh .html .c .json .yaml … — anything the text viewer claims alone, minus prose)VS Code (code-server)
.md .txt (prose)Nextcloud Text
.pdf, imagesViewer (Nextcloud’s built-in)
everything else (unknown ext, files under code-server binds)VS Code — the fallback
shared-in / unmappable filesDownload (the container can’t reach them)
always present, always lastDownload (order: 1000)

"Open with" is now UNIVERSAL — offered on every file, across every type

Owner feedback drove two changes to the cadviewer-open-with submenu: (1) it appears on every file (presence rule: any non-folder node), not only files with ≥2 handlers; (2) the type-preference gating was removed, keeping only hard reachability gates. So OnlyOffice, Online3DViewer and Download are offered on every file regardless of type (each still delegates to the live action, so it can never offer to open something the underlying handler would refuse). VS Code and FreeCAD stay reachability-gated — not by taste but because their containers literally cannot ingest the file otherwise: VS Code needs the file under a code-server bind (/CAD, /windows, or a Drive-home mount); FreeCAD needs it under /CAD and an importable format (.FCStd/.step/.stp/.iges/.igs/.brep). Download always sorts last. Folders get no submenu.

The whole routing lives in cadviewer-main.js and is proven per-change by a Node isolation harness (harness3.js) that transcribes NC 34’s real FileEntryMixin selection logic and asserts click-defaults are byte-stable across edits — because there is no in-ACL browser on this box to click from. Each ship bumps occ config:app:set theming cachebuster (now at 9), so clients need one hard refresh to pick up the new ?v=.

OnlyOffice for .csv/.docx is native config, NOT a custom action

The office formats are claimed via the OnlyOffice connector’s own defFormats configuration. No cadviewer action was written for them. Do not add one — you would end up with two actions competing for the same extension and a non-deterministic default.

The NC 34 file-action registry has a silent-failure mode — read the dedicated note

Registration on NC 34 fails silently if you use the pre-34 global (_nc_fileactions) or positional callbacks; the built-in download wins and the file downloads instead of opening. The real registry, the context-object callback contract, the ordering rules and the cachebuster requirement are all in 2026-08-31-nextcloud-34-custom-file-action-registration — not repeated here.

cadviewer is now a MISNOMER — it handles ALL file actions, not just CAD

The app started as the .stl → O3DV shim and kept the name while growing into the whole routing table above. It was deliberately not renamed: a Nextcloud app id is baked into the install path (custom_apps/cadviewer), the enabled-apps config, and every served asset URL (/custom_apps/cadviewer/js/…). Renaming means a reinstall plus a cachebuster bump for zero functional gain. Expect to find non-CAD logic in an app called cadviewer — that is correct, not a mistake.

FreeCAD open-from-Drive — .FCStd opens in the FreeCAD GUI

Clicking a .FCStd under /CAD (or choosing FreeCAD from any importable file’s Open-with) opens it in the shared FreeCAD GUI on the cad node. The path:

Drive click -> cadviewer-open-freecad -> https://cad.taild4189d.ts.net:8091/?f=<path-under-/CAD>
   -> freecad-open shim (127.0.0.1:8094, freecad-open.service, User=levander, ProtectSystem=strict)
   -> validates path (traversal rejected, only /CAD reachable)
   -> FreeCAD XML-RPC execute_code on 127.0.0.1:9875 (the FreeCADMCP addon)
       .FCStd -> FreeCAD.openDocument()   .step/.stp/.iges/.igs/.brep -> Part.insert()
   -> 302 to https://cad.taild4189d.ts.net/  (the noVNC desktop)

FreeCAD open gotchas — three ways it silently breaks

  • execute_code_async SEGFAULTS FreeCAD when it touches documents (it runs on a plain background thread). The shim uses execute_code, which runs on the GUI thread. Never use the async form to open a document.
  • A modal dialog blocks ALL opens. FreeCAD is a single shared session; process_gui_tasks defers while any modal/popup/held-mouse exists. A stuck “Document Recovery” modal made every open return GUI dispatch timed out after 90s while get_rpc_status still said healthy. If open-in-FreeCAD stops working, check for a modal dialog first (dismiss via docker exec -u abc -e DISPLAY=:1 freecad xdotool …).
  • FreeCAD is single-user / single-session — every click opens the document in the one shared desktop everyone sees. No per-user isolation. Only /CAD files are reachable (it is the only bind into the FreeCAD container). Served on the existing cad node via serve --https=8091 127.0.0.1:8094; the shim’s :8094 bind was moved to 127.0.0.2 by the hardening sweep. cad:9875 (FreeCAD XML-RPC) is a remote-code surface still open plaintext on the tailnet — flagged there for follow-up.

Online3DViewer is now Drive-wide (not just /CAD)

O3DV can open 3D files anywhere in the admin Drive, not only /CAD. Two nginx locations on the o3dv container serve read-only from /home/levander/nextcloud/data/admin/files:/drive:ro:

  • location ~* ^/drive/.+\.(stl|step|stp|3mf|obj|gltf|glb|ply|fbx|dae|3ds|off|wrl|brep|igs|iges|fcstd|amf|3dm|ifc)$ serves the file (disable_symlinks on).
  • location /drive/ 403 (every non-3D file and all directory listing blocked; autoindex off).

URL scheme: a /CAD file stays #model=/exports/<basename> (unchanged); any other admin-Drive file at /Foo/bar.stl #model=/drive/Foo/bar.stl (each path segment URL-encoded). Traversal is rejected (nginx normalizes ../ out before matching; encoded %2e%2e 400, 0 bytes leaked).

Those 3D files are readable on the no-auth cad node

cad’s O3DV origin is tailnet-only but has no auth (any tailnet peer that reaches :8090, including a tag:telep-user collaborator, can read it). The Drive-wide change makes any admin-owned 3D-extension file readable there, read-only, path-addressable only (the peer must know the path). NOT exposed: /windows (deliberately not mounted), non-admin/shared/external-storage files, and anything non-3D ( 403).


Language and defaults

SettingValue
default_languagehu
default_localehu_HU
defaultappfiles
force_languagedeliberately unset

force_language is left unset on purpose: it would lock every user to Hungarian and remove the per-user language switcher. New users default to Hungarian; anyone can still switch. Set via occ config:system:set <key> --value <v>.


drive-mcp — files over MCP

A WebDAV-backed MCP server exposing Drive’s files to agent clients.

Endpointhttps://drive.taild4189d.ts.net:8444/mcp
Loopback127.0.0.1:9100
Sidecarthe existing drive node — deliberately not a new tailnet node
BackendNextcloud WebDAV
Identitya single shared identity — every MCP client acts as the same Nextcloud user

Reusing the drive sidecar keeps the file service and its MCP face on one tailnet name and one ACL entry, at the cost of a third serve port (see the port map). A separate node would have meant a second device to approve, ACL, and keep alive for no isolation benefit — the MCP server already has full Drive access by design.

FastMCP behind tailscale serve returns 421 Invalid Host header unless allowed_hosts is set

tailscale serve forwards the original Host header, and mcp ≥ 1.29 turns on DNS-rebinding protection with an empty allowed_hosts — so only 127.0.0.1 passes. The symptom is brutal to diagnose: loopback works perfectly, every tailnet request gets a bare 421 that names no layer, so it reads as a proxy or auth failure rather than an application setting. Fix: pass TransportSecuritySettings(allowed_hosts=[...]) to FastMCP(...) including the tailnet hostname. Identical trap and identical fix as 2026-08-31-telep-kb-mcp-server — if a new MCP server on this box 421s, this is why.

# loopback proof (should answer; tailnet hop needs an in-ACL device)
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9100/mcp
sudo -n tailscale --socket=/run/tailscale-drive/tailscaled.sock serve status | grep 8444

Common operations

All commands run from a workstation over SSH. Always use /usr/bin/ssh (the ssh shell alias is broken on the Mac). Use sudo -n on the box.

The occ invocation form

/usr/bin/ssh levander@telep-mainframe \
  'cd /home/levander/nextcloud && docker compose exec -T --user www-data nextcloud php occ <subcommand>'

-T (no TTY) is required for non-interactive/systemd contexts. --user www-data is required — running occ as root refuses to start.

Frequently used:

Taskocc subcommand
Health / versionstatus
Full config auditsetupchecks (exit code is 1 whenever any ℹ item exists — read the output, not the code)
List usersuser:list
Rescan by handfiles:scan --all (or files:scan --path="<user>/files/CAD")
External storage healthfiles_external:verify 1
OnlyOffice connectivityonlyoffice:documentserver --check
Identity provider settingssaml:config:get --providerId 1
Appsapp:list, app:enable <id>, app:disable <id>

Restarting things

# Nextcloud stack (nextcloud, cron, db, redis, onlyoffice)
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/nextcloud && docker compose up -d'
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/nextcloud && docker compose restart nextcloud'
 
# identity proxy
/usr/bin/ssh levander@telep-mainframe 'sudo -n systemctl restart tsauth-proxy.service'
 
# drive-mcp (WebDAV MCP server on :9100)
/usr/bin/ssh levander@telep-mainframe 'sudo -n systemctl restart drive-mcp.service'
 
# code-server
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/code-server && docker compose restart'
 
# dashboard
/usr/bin/ssh levander@telep-mainframe 'docker compose -f /home/levander/homepage/docker-compose.yml restart'
 
# a tailscale sidecar (serve config is persisted in the node state and reappears ~10s after restart)
/usr/bin/ssh levander@telep-mainframe 'sudo -n systemctl restart tailscaled-drive.service'

Checking a sidecar

SVC=drive   # or cad | code | kb (node hostname is "knowledgebase")
/usr/bin/ssh levander@telep-mainframe "sudo -n systemctl status tailscaled-$SVC.service --no-pager"
/usr/bin/ssh levander@telep-mainframe "sudo -n tailscale --socket=/run/tailscale-$SVC/tailscaled.sock status"
/usr/bin/ssh levander@telep-mainframe "sudo -n tailscale --socket=/run/tailscale-$SVC/tailscaled.sock serve status"

Re-adding a serve mount if one is ever lost (example — Drive):

sudo -n tailscale --socket=/run/tailscale-drive/tailscaled.sock serve --bg --https=443  http://127.0.0.1:11001
sudo -n tailscale --socket=/run/tailscale-drive/tailscaled.sock serve --bg --https=8443 http://127.0.0.1:11002

Re-running the scan by hand

/usr/bin/ssh levander@telep-mainframe 'sudo -n systemctl start nextcloud-scan.service'
/usr/bin/ssh levander@telep-mainframe 'sudo -n systemctl show -p Result -p ExecMainStatus nextcloud-scan.service'
# expect: Result=success  ExecMainStatus=0
/usr/bin/ssh levander@telep-mainframe 'sudo -n systemctl list-timers nextcloud-scan.timer --no-pager'

Taildrive shares

Registered on the main node (default socket), not a sidecar:

/usr/bin/ssh levander@telep-mainframe 'sudo -n tailscale drive list'
# drive → /home/levander/nextcloud/data      as root
# cad   → /home/levander/freecad/exports     as root

Client mount (Finder / file manager, from an in-ACL device): http://100.100.100.100:8080/taild4189d.ts.net/telep-mainframe/drive and .../cad.

Where the secrets live

Never copy a secret into this vault

Read them on the box only. All are chmod 600.

FileHolds
/home/levander/nextcloud/.envPostgres password, Nextcloud admin password, ONLYOFFICE_JWT_SECRET, PHP limits, OVERWRITE*, trusted domains
/home/levander/code-server/.envTailscale auth key for the code node
/etc/tsauth-proxy.envTS_TAG_USERS tag→user map (see )
/etc/drive-mcp.envdrive-mcp’s WebDAV credentials for its shared Nextcloud identity. 0600, owner levander:levander — note this is in /etc, not alongside a compose dir like the other two

Compose files reference these only as ${VAR} — no literal secret is in any tracked config.

Key paths

WhatPath
Nextcloud stack/home/levander/nextcloud/
Nextcloud data (also Taildrive drive share)/home/levander/nextcloud/data (www-data:www-data)
CAD exports (also Taildrive cad share, NC external storage /CAD, O3DV /exports)/home/levander/freecad/exports (levander:www-data 2775)
CAD bind inside the nextcloud container/mnt/cad (rw)
Windows volume (host mount, NC external storage /windows)/mnt/winntfs3, ro, mounted by UUID in /etc/fstab
Windows bind inside the nextcloud container/mnt/windows ← host /mnt/win (:ro, the whole C: drive) — ⚠ container path is windows, host path is win. NC narrows to Users via datadir
Windows bind inside the code-server container/home/coder/windows ← host /mnt/win/Users (:ro) — narrowed in the bind, not in config
drive-mcp env file/etc/drive-mcp.env (0600, levander:levander)
drive-mcp unitdrive-mcp.service (systemd, enabled)
Custom cadviewer app/var/www/html/custom_apps/cadviewer (inside the persistent app named volume — survives image upgrades)
tsauth-proxy source + binary/home/levander/nextcloud/tsauth-proxy/
code-server stack/home/levander/code-server/
code-server workspace/home/levander/code-workspace → container /home/coder/workspace
HSTS apache snippet/home/levander/nextcloud/hsts.conf
Dashboard config/home/levander/homepage/config/services.yaml

code-server — mounts and the code tailnet node

What the IDE can see

Verbatim from /home/levander/code-server/docker-compose.yml:

- /home/levander/freecad/exports:/home/coder/cad
- /mnt/win/Users:/home/coder/windows:ro
- /home/levander/nextcloud/data/admin/files:/home/coder/drive/admin:ro
- "/home/levander/nextcloud/data/ledererandras2004@gmail.com/files:/home/coder/drive/ledererandras2004@gmail.com:ro"
Host pathContainer pathModeWhy
/home/levander/freecad/exports/home/coder/cadrw — carries no :ro, deliberatelythe working area; editing exports and scripts from the IDE is the point
/mnt/win/Users/home/coder/windowsrofourth read-only layer; files.readonlyInclude also marks it read-only inside VS Code. Narrowing to Users happens here, unlike Nextcloud
…/nextcloud/data/<user>/files/home/coder/drive/<user>roone bind per account, not one tree — see below

Drive-home is bound PER USER — a new Nextcloud account is invisible in the IDE until you add its line

There is no single …/nextcloud/data:/home/coder/drive mount. Each account gets its own line. If someone logs into Drive for the first time and then can’t find their files in code-server, this is why: add a bind for …/nextcloud/data/<user>/files and docker compose up -d. Quote any path containing @ — the ledererandras2004@gmail.com line is quoted for exactly this reason. An unquoted @ in a compose bind is a parse hazard; copy the quoting style of the existing line.

Drive-home is read-only ON PURPOSE — and reversing it is one compose line

Read-only lets the mount reuse the existing 0644 bits on Nextcloud’s data dir. Making it writable from code-server would require loosening permissions on Nextcloud’s own data directory (or running the container as www-data), which trades a real security property for convenience. Since files are editable from the Drive web UI anyway, read-only in the IDE loses very little. If it ever needs to be writable: flip :ro:rw on that user’s bind in /home/levander/code-server/docker-compose.yml, remove the path from files.readonlyInclude, and docker compose up -d. Deliberate, reversible, one line per account.

The code node is tagged tag:telep

code now carries tag:telep, matching cad / knowledgebase / home. This makes the node an infrastructure device rather than a personal one, so its lifecycle and ACLs follow the same rules as the rest of the house services.

Re-authing a tagged node does NOT retrigger device approval

tailscale up on an already-registered node reuses the existing node key, so re-authing to apply or change tags does not create a new device and does not need re-approval. You can safely re-run it.

Pass --hostname=code explicitly or tailscale up resets it to the OS hostname

Without --hostname, the sidecar re-registers under the host’s name (telep-mainframe), the MagicDNS name code.taild4189d.ts.net stops resolving, and the serve mounts land on the wrong name. Always include it:

sudo -n tailscale --socket=/run/tailscale-code/tailscaled.sock up \
  --hostname=code --advertise-tags=tag:telep --auth-key=<from /home/levander/code-server/.env>

The same applies to every other sidecar on this box.

OpenSCAD baked into the image

OpenSCAD is available in code-server, made persistent via a Dockerfile layer on the codercom/code-server:latest base (a locally-built code-server-openscad:latest, build: in compose) — baked into the image, so it survives every docker compose up -d recreate. The headless CLI render works (openscad -o out.stl in.scad, this 2021.01 build exports CSG STL without needing xvfb); the GUI preview does not (the Antyos extension, from OpenVSX, launches the GUI app which has no display in the headless container — the CLI .scad.stl/.off into /CAD is the working path). The image build was pinned taskset -c 0-15,17-23 off the faulty core.

code-server now mounts the ENTIRE home dir — ~/.ssh and .env secrets are exposed to anyone reaching code

A - /home/levander:/home/coder/host rw bind was added to the code-server compose. Because code-server runs --auth none (tailnet membership IS the credential), the whole home directory — including ~/.ssh, every .env secret file, and every service’s compose/state under /home/levander — is now readable and writable by anyone who can reach the code node. This is a materially larger blast radius than the previous scoped /CAD + Drive-home + /windows mounts. The mitigation is at the ACL, not the mount: scope tag:telep-user’s (and any collaborator’s) tailnet ACL to exclude the code node. The mount stays as the owner requested; the ACL is what must keep others off it. Revert is one compose line (docker compose up -d) if the full-home mount is ever unwanted.


Gotchas

Taildrive writes land root:root — there is no per-share uid map

Both shares serve as root because tailscaled runs as root, and Tailscale 1.102.2 has no per-share uid mapping. Anything written into drive or cad over Taildrive is created root:root. The mitigation is nextcloud-scan.timer (see above): its three ExecStartPre steps normalize ownership before each scan. Consequence: a ≤3-minute window where the file is visible/readable in the web UI but not yet editable. Do not “fix” this by running the sidecar as www-data — that’s a much larger change and was rejected.

services.yaml — ALWAYS quote description: values

An unquoted colon-space inside a description is parsed as a YAML mapping and blanks the entire dashboard, not just that tile. Quote every description. Two more house rules for this file:

  • Tiles use a loopback siteMonitor (http://127.0.0.1:<port>/...) because homepage runs host-networked — the container shares the host netns, so 127.0.0.1 resolves to the host’s loopback listeners. A tailnet URL there would fail.
  • No tile uses an icon: field. Adding one would try a dashboard-icons CDN fetch that may not resolve on this tailnet-restricted host; a broken icon looks worse than the consistent default. Match the neighbours.

The live Nextcloud tile (group 💾 Tárolás & Tudás):

    - Nextcloud:
        href: https://drive.taild4189d.ts.net
        siteMonitor: http://127.0.0.1:11000/status.php
        description: "Nextcloud - files, CAD, Office - tailnet"

code-server has an equivalent tile in 🛠️ Eszközök & Média. After any edit: docker compose -f /home/levander/homepage/docker-compose.yml restart, then confirm with curl -s http://127.0.0.1:3010/api/services.

OnlyOffice: matching JWT secret + jwt_header=Authorization + internal URLs

All four must line up or the editor silently refuses to load:

  • DocumentServer env: JWT_ENABLED=true, JWT_SECRET=${ONLYOFFICE_JWT_SECRET}, JWT_HEADER=Authorization
  • Connector: jwt_secret = the same 64-hex value, jwt_header = Authorization
  • DocumentServerUrl = https://drive.taild4189d.ts.net:8443/browser-facing, must be the tailnet URL because the user’s browser loads the editor from it
  • DocumentServerInternalUrl = http://onlyoffice/ and StorageUrl = http://nextcloud/container-to-container on the nextcloud_default network
  • occ config:system:set allow_local_remote_servers --value true is required for those internal http:// URLs to be accepted

Check with occ onlyoffice:documentserver --check“…is successfully connected”.

NC 34 file-action registry — see File actions — what opens what and the dedicated note

Registering a custom file action on NC 34 fails silently if you use the pre-34 global or positional callbacks; the built-in download action wins and the file downloads instead of opening. Full root cause, the live registry, the callback contract and the cachebuster requirement: 2026-08-31-nextcloud-34-custom-file-action-registration. Not repeated here.

code-server has NO authentication

It runs --auth none. Tailnet membership IS the credential. There is no password, no login page, no second factor. The code node’s ACL is the only thing between an attacker and a shell on the box. Consequences:

  • Never enable Funnel on code.
  • Never share the code node out to anyone you would not give SSH to.
  • Keep the container bound to 127.0.0.1:8888; verify after any compose change (see the loopback-only proof below).

Two smaller ones

  • The code-server named volumes code-server_code-config / code-server_code-local had to be chowned 1000:1000 once (the image lacks ~/.config / ~/.local, so fresh volumes mount root-owned and code-server hits EACCES). A future docker compose down -v deletes them and the chown must be repeated. The coder user is uid/gid 1000 = levander, so container-written workspace files land owned by levander with no remap.
  • The cadviewer app declares max-version="35" in info.xml. A Nextcloud upgrade past major 35 will disable it until the bound is bumped.

Verification checklist

Provable from the host (SSH only)

# --- Nextcloud core ---
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: drive.taild4189d.ts.net' http://127.0.0.1:11000/status.php   # 200
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:11001/status.php                                       # 200  (proxy hop alive)
cd /home/levander/nextcloud && docker compose ps                                                                  # nextcloud, cron, db, redis, onlyoffice all Up
docker compose exec -T --user www-data nextcloud php occ status                                                   # installed=true, maintenance=false
docker compose exec -T --user www-data nextcloud php occ setupchecks                                              # target: zero ⚠, zero errors (ℹ items are fine)
 
# --- Identity ---
sudo -n systemctl is-active tsauth-proxy.service                                                                  # active
sudo -n tailscale --socket=/run/tailscale-drive/tailscaled.sock whois --json <a-real-tailnet-device-ip>           # resolves LoginName
docker compose exec -T --user www-data nextcloud php occ saml:config:get --providerId 1                           # uid/email/displayName all HTTP_REMOTE_USER
docker compose exec -T --user www-data nextcloud php occ user:list                                                # admin + the tailnet login
 
# --- OnlyOffice ---
curl -sf http://127.0.0.1:11002/healthcheck                                                                       # true
docker compose exec -T --user www-data nextcloud php occ onlyoffice:documentserver --check                        # "successfully connected"
 
# --- External storages ---
docker compose exec -T --user www-data nextcloud php occ files_external:list                                      # /CAD rw, /windows readonly=1
docker compose exec -T --user www-data nextcloud php occ files_external:verify 1                                  # /CAD      status: ok, code: 0
docker compose exec -T --user www-data nextcloud php occ files_external:verify 2                                  # /windows  status: ok, code: 0
docker compose exec -T nextcloud ls /mnt/cad | wc -l                                                              # matches host exports count
findmnt -no SOURCE,FSTYPE,OPTIONS /mnt/win                                                                        # HOST: ntfs3, ro — and the fstab entry must be UUID=
docker compose exec -T nextcloud sh -c 'touch /mnt/windows/Users/.probe'; echo "exit=$?"                          # CONTAINER path — MUST fail (read-only)
 
# --- drive-mcp ---
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9100/mcp                                                # server alive on loopback
sudo -n tailscale --socket=/run/tailscale-drive/tailscaled.sock serve status                                      # 443 + 8443 + 8444, all "(tailnet only)"
 
# --- Scan / normalization timer ---
sudo -n systemctl list-timers nextcloud-scan.timer --no-pager                                                     # enabled + next fire within 3 min
sudo -n systemctl start nextcloud-scan.service
sudo -n systemctl show -p Result -p ExecMainStatus nextcloud-scan.service                                         # Result=success ExecMainStatus=0
# ownership round-trip: drop a root:root probe into either share, run the service, re-stat
sudo -n tailscale drive list                                                                                      # drive + cad shares present
 
# --- Sidecars / serve (the NO-FUNNEL check) ---
for s in drive cad code kb; do
  sudo -n tailscale --socket=/run/tailscale-$s/tailscaled.sock serve status
done
# EVERY mount must read "(tailnet only)". Any line mentioning Funnel is an incident.
 
# --- code-server ---
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8888/                                                   # 302 -> ./?folder=/home/coder/workspace
curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 http://192.168.1.123:8888/                                  # 000 (exit 7) = NOT on the LAN
curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 http://100.115.209.87:8888/                                 # 000 (exit 7) = NOT on the host tailnet IP
sudo -n systemctl is-enabled tailscaled-code.service                                                              # enabled
docker compose -f /home/levander/code-server/docker-compose.yml ps                                                # running
sudo -n tailscale --socket=/run/tailscale-code/tailscaled.sock status --json | grep -i 'tag:telep'                # node is tagged
docker inspect -f '{{range .Mounts}}{{.Source}} -> {{.Destination}} (rw={{.RW}}){{"\n"}}{{end}}' \
  $(docker compose -f /home/levander/code-server/docker-compose.yml ps -q)
#   /home/levander/freecad/exports -> /home/coder/cad          (rw=true)
#   /mnt/win/Users                 -> /home/coder/windows      (rw=false)
#   …/nextcloud/data/<user>/files  -> /home/coder/drive/<user> (rw=false)   one line PER account
 
# --- filebrowser is RETIRED (should all be negative) ---
docker ps -a --format '{{.Names}} {{.Status}}' | grep -i filebrowser                                              # present but Exited (volumes preserved)
curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 http://127.0.0.1:8445/                                      # 000 = gone
 
# --- cadviewer file action (server side) ---
docker compose exec -T --user www-data nextcloud php occ app:list | grep cadviewer                                # enabled
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:11000/custom_apps/cadviewer/js/cadviewer-main.js        # 200
 
# --- Dashboard ---
curl -s http://127.0.0.1:3010/api/services | grep -o 'Nextcloud\|Code Server'                                     # both tiles present

Only confirmable from a tailnet device inside the ACL

The host cannot prove these — do not try

telep-mainframe cannot hairpin to its own tailnet-only HTTPS (times out), and neither fk-dev nor telep-router is admitted by the drive/code node ACLs (connect timeout / “Operation not permitted” — identically on ports already proven working over loopback). These require an in-ACL device with a browser.

#CheckExpected
1Open https://drive.taild4189d.ts.netAuto-logged-in as the Tailscale identity, no password prompt
2Upload/create a .docx and .xlsx, open, edit, close, reopenOnlyOffice iframe loads from :8443; the change persisted
3Browse Drive → CAD, hard-refresh once (⌘/Ctrl-Shift-R), click a .stlNew tab → https://cad.taild4189d.ts.net:8090/o3dv/#model=/exports/<name>, model paints. The hard-refresh is required once — the action registers client-side and the Files bundle is cached.
4Open https://cad.taild4189d.ts.net:8090/O3DV landing card grid; clicking a card renders the model
5Open https://code.taild4189d.ts.netVS Code loads straight into /home/coder/workspace, no auth prompt (expected — see the danger callout)
6Mount both Taildrive shares in Finderdrive and cad mount; reads work. Writes land root:root until the next timer fire.
7Load https://home.taild4189d.ts.netNextcloud + Code Server tiles render with a green status dot
8Browse Drive → windowsTree lists; no New/Upload button, no rename/delete in the row menu (the PermissionsMask wrapper hides them). Reads work.
9Click a .csv and a .docxOnlyOffice opens both (native defFormats, not a custom action)
10Click a .md, then a random unknown-type fileNextcloud Text for the .md; VS Code for the unknown one (fallback). Every file has an “Open with” submenu, with Download last.
11In code-server, open a file under /windows and try to saveVS Code refuses — the path is in files.readonlyInclude
12UI language on a fresh loginHungarian (hu / hu_HU), landing on Files; the per-user language switcher is still available (force_language unset)
13Point an MCP client at https://drive.taild4189d.ts.net:8444/mcpTools list; no 421. A 421 means allowed_hosts is unset — see .

If a browser check fails but every host check above is green

It is almost certainly a client → node reachability / ACL / cert-trust issue on that device, not a stack misconfiguration. Check that the device is in the node’s ACL and can resolve MagicDNS before touching any config on the box.


Backups / rollback

Every file the build modified was backed up in place. Reverting = copy the .bak back and restart the relevant thing.

BackupReverting it undoes
/home/levander/nextcloud/docker-compose.yml.bakThe hardening pass: removes the x-shared-env anchor, the cron sidecar, the hsts.conf mount, OVERWRITECLIURL and the raised PHP limits → back to the bare Task-1 nextcloud/db/redis stack.
/home/levander/nextcloud/.env.bakThe hardening .env additions: OVERWRITECLIURL, PHP_MEMORY_LIMIT=1024M, PHP_UPLOAD_LIMIT=16G. Also predates ONLYOFFICE_JWT_SECRET — restoring it breaks OnlyOffice.
/home/levander/nextcloud/docker-compose.yml.task3.bakThe OnlyOffice service, its onlyoffice_data / onlyoffice_log volumes and its healthcheck.
/home/levander/nextcloud/.env.task3.bakThe ONLYOFFICE_JWT_SECRET entry (pre-OnlyOffice .env).
/home/levander/nextcloud/docker-compose.yml.task4.bakThe CAD bind mount /home/levander/freecad/exports:/mnt/cad → the /CAD external storage loses its backing dir.
/etc/systemd/system/nextcloud-scan.service.bak-prechownThe three ownership-normalization ExecStartPre steps → back to scan-only. Taildrive-written files stay root:root and become read-only from the web UI. Requires systemctl daemon-reload.
/home/levander/homepage/config/services.yaml.bak-nextcloudThe Nextcloud dashboard tile.
/home/levander/homepage/config/services.yaml.bak-codeserverThe Code Server dashboard tile (this snapshot already contains the Nextcloud tile).
/home/levander/homepage/config/services.yaml.bak-3dviewer, .bak-cadgroup, .bak-tailnetEarlier same-session services.yaml snapshots from the CAD/3D-viewer and tailnet dashboard edits. Older than the two above — restoring one of these also drops the Nextcloud and Code Server tiles.
/var/www/html/custom_apps/cadviewer/js/cadviewer-main.js.stl.bak (inside the container, in the persistent app volume)The NC-34 file-action fix → back to the broken registration where .stl clicks download. Restore with cp … cadviewer-main.js && chown www-data:www-data cadviewer-main.js, and occ config:app:delete theming cachebuster.

Things with no .bak — they were purely additive; revert by removing:

# scan timer (Task 5) — the units themselves were new
sudo -n systemctl disable --now nextcloud-scan.timer
sudo -n rm /etc/systemd/system/nextcloud-scan.{service,timer} && sudo -n systemctl daemon-reload
 
# Taildrive shares
sudo -n tailscale drive unshare drive
sudo -n tailscale drive unshare cad
 
# the cadviewer app (lives entirely in the persistent app volume; no compose edit was made)
docker compose exec -T --user www-data nextcloud php occ app:disable cadviewer
 
# code-server (all new: compose dir, .env, workspace dir, sidecar unit)
cd /home/levander/code-server && docker compose down
sudo -n systemctl disable --now tailscaled-code.service
 
# the drive sidecar + identity proxy (all new units)
sudo -n systemctl disable --now tsauth-proxy.service tailscaled-drive.service
 
# the /windows external storage (id 2) — removing it does NOT unmount /mnt/win
docker compose exec -T --user www-data nextcloud php occ files_external:delete 2
# …then drop the ":ro" bind from docker-compose.yml, and the /etc/fstab UUID line if the
# host mount is no longer wanted. Leave the fstab line UUID-based if you keep it.
 
# drive-mcp (new unit + new serve mount on the existing drive node)
sudo -n systemctl disable --now drive-mcp.service
sudo -n tailscale --socket=/run/tailscale-drive/tailscaled.sock serve --https=8444 off
 
# bring filebrowser back (container stopped, volumes preserved, serve mount removed)
docker start filebrowser
sudo -n tailscale --socket=/run/tailscale-<svc>/tailscaled.sock serve --bg --https=8445 http://127.0.0.1:8445