Nextcloud Drive with Tailscale identity — Implementation Plan

For agentic workers: Implement task-by-task via subagent-driven development. Each task ends with a concrete verification gate. No git commits (owner rule); “checkpoint” = confirm the verification output, then proceed. This is remote infra on telep-mainframe — every command runs over /usr/bin/ssh -o ConnectTimeout=20 levander@telep-mainframe '<cmd>' (the ssh alias is broken; always /usr/bin/ssh). Router work goes through a second hop: ... "ssh -o StrictHostKeyChecking=no root@192.168.1.1 '<cmd>'". Use sudo -n on the mainframe.

Goal: Stand up a self-hosted Nextcloud “Drive” on telep-mainframe with Tailscale-provisioned identity, OnlyOffice editing, the CAD dir surfaced as a folder, and the whole Nextcloud storage exposed via Taildrive.

Architecture: Docker Compose stack (nextcloud + postgres + redis + onlyoffice) at /home/levander/nextcloud/, data on the root LVM. A dedicated drive Tailscale node fronts it; identity comes from Tailscale via tsidp (Tailscale’s OIDC IdP) + Nextcloud user_oidc (primary) or a whois→header proxy + user_saml env mode (fallback). CAD dir is external storage + its own Taildrive share.

Tech Stack: Docker/Compose, Nextcloud (apache image), PostgreSQL 16, Redis 7, OnlyOffice DocumentServer, Tailscale 1.102 (tailscale serve, tailscale drive, tsidp).

Spec: projects/homelab/2026-08-31-nextcloud-drive-tailscale-spec.md

Global Constraints

  • Host: telep-mainframe (192.168.1.123 / tailnet 100.115.209.87). Docker convention: /home/levander/<service>/ with docker-compose.yml.
  • Data dir: /home/levander/nextcloud/data on the root LVM (2.8 TB free). Runtime user inside the apache image is www-data (uid 33).
  • Tailnet: taild4189d.ts.net. New node name: drivedrive.taild4189d.ts.net. Follow the existing per-service tailscaled-<svc>.service sidecar pattern (userspace, --tun=userspace-networking, own --statedir + --socket, --port=0).
  • No secrets in the vault. Generate DB/admin passwords on the box; record them only in /home/levander/nextcloud/.env (chmod 600).
  • No git commits anywhere. No comments in any scripts/config authored (owner style).
  • Access is tailnet-only. Do NOT expose via Funnel.

Task 1: Identity spike — prove Tailscale login into a throwaway Nextcloud

Deliverable: a real tailnet user logs into a minimal Nextcloud with no password, auto-provisioned from Tailscale identity. Nothing else is built until this passes.

Files:

  • Create: /home/levander/nextcloud/docker-compose.yml (minimal: nextcloud + postgres + redis)
  • Create: /home/levander/nextcloud/.env (chmod 600)
  • Create: /etc/systemd/system/tailscaled-drive.service (sidecar tailscaled)
  • Create: identity component — tsidp unit/container (primary path)

Interfaces:

  • Produces: a reachable https://drive.taild4189d.ts.net serving Nextcloud; an OIDC issuer URL from tsidp; a working user_oidc provider entry in Nextcloud.

  • Step 1: Create the drive Tailscale sidecar node. Model /etc/systemd/system/tailscaled-drive.service on the existing tailscaled-bambuddy.service (copy its ExecStart, change statedir→/var/lib/tailscale-drive, socket→/run/tailscale-drive/tailscaled.sock). systemctl enable --now tailscaled-drive, then tailscale --socket=/run/tailscale-drive/tailscaled.sock up --hostname=drive. Authenticate (auth key or interactive URL — surface the URL to the owner if needed).

  • Step 2: Verify the node. tailscale --socket=/run/tailscale-drive/tailscaled.sock status | head. Expected: node drive shows 100.x address, online.

  • Step 3: Minimal Nextcloud stack. Write docker-compose.yml with nextcloud:apache, postgres:16, redis:7; bind Nextcloud to 127.0.0.1:8971 (or next free port — check ss -tlnp). Set .env with generated POSTGRES_PASSWORD, NEXTCLOUD_ADMIN_USER=admin, generated NEXTCLOUD_ADMIN_PASSWORD, NEXTCLOUD_TRUSTED_DOMAINS="drive.taild4189d.ts.net", OVERWRITEPROTOCOL=https, OVERWRITECLIHOST + TRUSTED_PROXIES for the tsnet proxy IP. docker compose up -d.

  • Step 4: Serve it on the drive node. tailscale --socket=/run/tailscale-drive/tailscaled.sock serve --bg --https=443 http://127.0.0.1:<port>. Verify: curl -s -o /dev/null -w '%{http_code}' -H 'Host: drive.taild4189d.ts.net' http://127.0.0.1:<port>/status.php → 200; from a tailnet device the owner loads https://drive.taild4189d.ts.net and sees the Nextcloud login.

  • Step 5: Stand up tsidp (Tailscale OIDC IdP). Run tsidp (Tailscale’s OIDC provider) as its own tsnet service so it issues tokens for tailnet users. Capture its issuer URL (https://idp.taild4189d.ts.net or the tsidp-published URL). (Fallback if tsidp is unavailable/flaky: skip to Step 5b.)

  • Step 5b (fallback only): whois→header proxy. Front Nextcloud with a proxy that calls tailscale whois on the source IP and injects Remote-User; enable Nextcloud user_saml in “environment variable” mode mapping that header. Use only if Step 5 fails.

  • Step 6: Wire Nextcloud user_oidc to tsidp. docker compose exec --user www-data nextcloud php occ app:install user_oidc; register the tsidp provider (client id/secret, issuer, auto-provision on). Set user_oidc to auto-redirect + auto-create users.

  • Step 7: GATE — prove login. Owner opens https://drive.taild4189d.ts.net from a tailnet device → is logged in as their Tailscale identity with no password, and a matching Nextcloud user was auto-created. Verify: occ user:list shows the tailnet login. If this fails, stop and reassess identity approach before any further task.


Task 2: Harden the Nextcloud stack

Deliverable: production-shaped Nextcloud — background jobs, caching, memory, no admin warnings.

Files: Modify /home/levander/nextcloud/docker-compose.yml, /home/levander/nextcloud/.env

  • Step 1: Redis + APCu caching. Set memcache.local=\OC\Memcache\APCu, memcache.distributed+memcache.locking=\OC\Memcache\Redis, redis host/port via config.php/env. Add overwrite.cli.url=https://drive.taild4189d.ts.net.
  • Step 2: Background jobs = cron. Add a nextcloud-cron sidecar (same image, entrypoint /cron.sh) or a host systemd timer running occ cron every 5 min. Set occ background:cron.
  • Step 3: PHP memory + upload limits. Set PHP_MEMORY_LIMIT=1024M, upload limits (e.g. 16G) via env/.htaccess/apache config for large CAD/media.
  • Step 4: docker compose up -d, then verify. occ status → installed, no maintenance. occ setupchecks (or the admin overview) → no critical warnings (trusted proxy, caching, cron all green). Reachable check: curl /status.php = 200.
  • Step 5: Checkpoint. Confirm the admin → Overview page shows zero red warnings for a tailnet-loaded session.

Task 3: OnlyOffice Document Server + connector

Deliverable: a .docx opens, edits, and saves back inside Nextcloud.

Files: Modify docker-compose.yml (add onlyoffice/documentserver), Nextcloud onlyoffice app config.

  • Step 1: Add DocumentServer. Add onlyoffice/documentserver to compose bound to 127.0.0.1:<port2>, with a generated JWT_SECRET. Front it on the drive node OR reach it container-to-container; set ONLYOFFICE_JWT on both sides.
  • Step 2: Serve DS on the tailnet (OnlyOffice needs a browser-reachable URL): tailscale --socket=/run/tailscale-drive/... serve --bg --https=<p> http://127.0.0.1:<port2> → e.g. https://drive.taild4189d.ts.net:<p>. Verify /healthcheck returns true.
  • Step 3: Install + configure the connector. occ app:install onlyoffice; set the DS URL + JWT secret via occ config:app:set onlyoffice DocumentServerUrl ... and the secret. Confirm internal + storage URLs resolve container-side.
  • Step 4: GATE. Upload a .docx in the web UI → it opens in OnlyOffice, type a change, close → reopen shows the change persisted. Same for a .xlsx.

Deliverable: the CAD export dir appears as a live “CAD” folder in the Drive; a .step opens in Online3DViewer.

Files: Nextcloud files_external config; optional small “open in 3D viewer” action.

  • Step 1: Enable external storage. occ app:enable files_external. Mount /home/levander/freecad/exports as Local external storage named CAD, read-write, available to the tailnet user(s). Bind-mount that host path into the nextcloud container first (add to compose volumes, e.g. /home/levander/freecad/exports:/mnt/cad), then point the external mount at /mnt/cad.
  • Step 2: Scan + verify listing. occ files_external:list shows CAD mount OK; occ files:scan --all (or --path) then the web UI “CAD” folder lists the 26 existing files (.step, .3mf, .FCStd, .csv, .py).
  • Step 3: 3D deep-link. For .step/.stl/.3mf/.obj/.gltf, provide an “Open in 3D viewer” path to https://cad.taild4189d.ts.net:8090/o3dv/#model=/exports/<name> (a small custom file action, or documented right-click→copy-link convention if a custom action is over-scope). Keep it minimal.
  • Step 4: GATE. In the web UI, a .step in CAD opens the O3DV viewer showing the model; a new file written to /home/levander/freecad/exports on the host appears in the CAD folder after the next scan.

Task 5: Taildrive shares + scan-on-change

Deliverable: the whole Nextcloud storage and the CAD dir both mount on the Mac via Taildrive; out-of-band writes surface in Nextcloud.

Files: Taildrive shares (main tailscaled), a scan trigger unit.

  • Step 1: Share the whole Drive. On the mainframe main node: tailscale drive share drive /home/levander/nextcloud/data. (ACLs drive:share/drive:access already provisioned.)
  • Step 2: Share the CAD dir. tailscale drive share cad /home/levander/freecad/exports. Verify: tailscale drive list shows both.
  • Step 3: Scan-on-change. Add a lightweight occ files:scan trigger so Taildrive/out-of-band writes appear in the web UI — a systemd .path unit watching the data dir, or a 5-min timer running occ files:scan --all --quiet. Match ownership (chown www-data on new files if needed).
  • Step 4: GATE. Owner mounts http://100.100.100.100:8080/<tailnet>/telep-mainframe/drive in Finder → sees Nextcloud user files; mounts .../cad → sees CAD files. Drop a file into the mounted cad share → it appears in the Nextcloud CAD folder within the scan interval.

Task 6: LAN vhost + documentation

Deliverable: optional drive.telep.lan access + an Obsidian runbook.

Files: /etc/caddy/Caddyfile (add vhost), new Obsidian note.

  • Step 1 (optional): LAN vhost. Add http://drive.telep.lan { reverse_proxy 127.0.0.1:<port> } to the Caddyfile (match existing pattern), add drive.telep.lan to Nextcloud trusted_domains, reload Caddy. Verify curl -H 'Host: drive.telep.lan' http://127.0.0.1:80/status.php = 200. (Note: identity/OIDC redirect is tuned for the tailnet host; confirm login still works on the LAN host or scope LAN to view-only.)
  • Step 2: Dashboard tile. Add a Nextcloud tile to /home/levander/homepage/config/services.yaml (tailnet href https://drive.taild4189d.ts.net, siteMonitor http://127.0.0.1:<port>/status.php), reload homepage.
  • Step 3: Document. Spawn obsidian-documenter to write a runbook: stack layout, ports, the identity mechanism actually used, Taildrive shares, the scan-on-change job, and restore/backup notes. Cross-link the spec + this plan.
  • Step 4: Checkpoint. Owner confirms end-to-end: login, upload, share, CAD folder, OnlyOffice edit, Taildrive mount all work.

Self-Review

Spec coverage: Nextcloud stack (T1–2) ✓; Tailscale identity (T1) ✓; OnlyOffice (T3) ✓; CAD external storage + 3D link (T4) ✓; whole-Drive + CAD Taildrive shares (T5) ✓; scan-on-change consistency (T5) ✓; access + LAN vhost + dashboard (T6) ✓; backups explicitly out-of-scope v1 (noted in T6 docs). Covered.

Risk-first ordering: identity proven in T1 before anything else — matches spec.

Open decision carried into execution: identity = tsidp+user_oidc (primary) vs whois-header+user_saml (fallback), resolved at T1 Step 5/7 by whichever proves login.