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>'(thesshalias is broken; always/usr/bin/ssh). Router work goes through a second hop:... "ssh -o StrictHostKeyChecking=no root@192.168.1.1 '<cmd>'". Usesudo -non 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>/withdocker-compose.yml. - Data dir:
/home/levander/nextcloud/dataon the root LVM (2.8 TB free). Runtime user inside the apache image iswww-data(uid 33). - Tailnet:
taild4189d.ts.net. New node name:drive→drive.taild4189d.ts.net. Follow the existing per-servicetailscaled-<svc>.servicesidecar 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 —
tsidpunit/container (primary path)
Interfaces:
-
Produces: a reachable
https://drive.taild4189d.ts.netserving Nextcloud; an OIDC issuer URL from tsidp; a workinguser_oidcprovider entry in Nextcloud. -
Step 1: Create the
driveTailscale sidecar node. Model/etc/systemd/system/tailscaled-drive.serviceon the existingtailscaled-bambuddy.service(copy its ExecStart, change statedir→/var/lib/tailscale-drive, socket→/run/tailscale-drive/tailscaled.sock).systemctl enable --now tailscaled-drive, thentailscale --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: nodedriveshows100.xaddress, online. -
Step 3: Minimal Nextcloud stack. Write
docker-compose.ymlwithnextcloud:apache,postgres:16,redis:7; bind Nextcloud to127.0.0.1:8971(or next free port — checkss -tlnp). Set.envwith generatedPOSTGRES_PASSWORD,NEXTCLOUD_ADMIN_USER=admin, generatedNEXTCLOUD_ADMIN_PASSWORD,NEXTCLOUD_TRUSTED_DOMAINS="drive.taild4189d.ts.net",OVERWRITEPROTOCOL=https,OVERWRITECLIHOST+TRUSTED_PROXIESfor 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 loadshttps://drive.taild4189d.ts.netand 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.netor 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 whoison the source IP and injectsRemote-User; enable Nextclouduser_samlin “environment variable” mode mapping that header. Use only if Step 5 fails. -
Step 6: Wire Nextcloud
user_oidcto 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). Setuser_oidcto auto-redirect + auto-create users. -
Step 7: GATE — prove login. Owner opens
https://drive.taild4189d.ts.netfrom a tailnet device → is logged in as their Tailscale identity with no password, and a matching Nextcloud user was auto-created. Verify:occ user:listshows 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 viaconfig.php/env. Addoverwrite.cli.url=https://drive.taild4189d.ts.net. - Step 2: Background jobs = cron. Add a
nextcloud-cronsidecar (same image, entrypoint/cron.sh) or a hostsystemdtimer runningocccron every 5 min. Setocc 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/documentserverto compose bound to127.0.0.1:<port2>, with a generatedJWT_SECRET. Front it on the drive node OR reach it container-to-container; setONLYOFFICE_JWTon 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/healthcheckreturnstrue. - Step 3: Install + configure the connector.
occ app:install onlyoffice; set the DS URL + JWT secret viaocc config:app:set onlyoffice DocumentServerUrl ...and the secret. Confirm internal + storage URLs resolve container-side. - Step 4: GATE. Upload a
.docxin the web UI → it opens in OnlyOffice, type a change, close → reopen shows the change persisted. Same for a.xlsx.
Task 4: CAD external storage + 3D deep-link
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/exportsas Local external storage namedCAD, 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:listshows 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 tohttps://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
.stepin CAD opens the O3DV viewer showing the model; a new file written to/home/levander/freecad/exportson 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. (ACLsdrive:share/drive:accessalready provisioned.) - Step 2: Share the CAD dir.
tailscale drive share cad /home/levander/freecad/exports. Verify:tailscale drive listshows both. - Step 3: Scan-on-change. Add a lightweight
occ files:scantrigger so Taildrive/out-of-band writes appear in the web UI — a systemd.pathunit watching the data dir, or a 5-min timer runningocc 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/drivein Finder → sees Nextcloud user files; mounts.../cad→ sees CAD files. Drop a file into the mountedcadshare → 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), adddrive.telep.lanto Nextcloudtrusted_domains, reload Caddy. Verifycurl -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 hrefhttps://drive.taild4189d.ts.net, siteMonitorhttp://127.0.0.1:<port>/status.php), reload homepage. - Step 3: Document. Spawn
obsidian-documenterto 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.