NAS08 is a **multi‑service application server** providing DNS, storage, media hosting, and self‑hosted applications for the KingDezigns network.
It is the **Pi‑hole DNS provider** for all VLANs and also hosts several Docker‑based services including Nextcloud, Vaultwarden, and nginx.
This device is a **critical infrastructure component**, supporting both internal services and Home Assistant integrations.
> Note: Plex Media Server was migrated off NAS08 to **PLEX32** (dedicated Dell Wyse 5070) on 2026-06-18 to support advanced Plex Pass features (Sonic Radio) requiring more CPU capability. The Plex container remains present on NAS08 but is permanently disabled (`restart: no`). Media files remain on NAS08 — PLEX32 accesses them via NFS. See `server_plex32.md`.
---
## 🖥️ Hardware
- Raspberry Pi 5 (8GB RAM)
- SSD storage (see drive inventory below)
- Gigabit Ethernet
### **Drive Inventory**
| Device | Model | Capacity | Role |
|--------|-------|----------|------|
| /dev/sda | PNY CS900 | 500GB | OS / system drive |
NAS08 runs `systemd-resolved`. The host's `eth0` interface correctly uses Pi‑hole (192.168.150.35) as its only DNS server via DHCP.
> ⚠️ **Known issue, fixed 2026-07-18:** `/etc/systemd/resolved.conf` previously had a manually-set global override:
> ```
> [Resolve]
> DNS=1.1.1.1 8.8.8.8
> FallbackDNS=9.9.9.9
> ```
> Because `resolv.conf mode` is `uplink`, this **global** `DNS=` line merged with the correct per-interface Pi‑hole entry, causing `/etc/resolv.conf` to list all three nameservers. This produced inconsistent/duplicate DNS answers (e.g. a hostname resolving to both its correct internal IP *and* the public WAN IP), which broke local-DNS-override-dependent services such as ONLYOFFICE (see below).
>
> **Fix applied:** removed the global `DNS=` line from `/etc/systemd/resolved.conf`, leaving `FallbackDNS=9.9.9.9` in place as a last-resort fallback only. Verify with `resolvectl status` — the `Global` block should show no `DNS Servers:` line, and `Link 2 (eth0)` should show only `192.168.150.35`.
>
> This fix required a container restart (or host reboot) for already-running Docker containers to pick up the corrected host resolver, since Docker's embedded DNS (127.0.0.11) inherits the host's `resolv.conf` at container start.
---
## 📦 Primary Functions
### **Core Services**
- **Pi‑hole DNS** (Docker)
- **Nextcloud** (Docker) — with ONLYOFFICE document editing (see dedicated section below)
- **Vaultwarden** (Docker)
- **nginx** (Docker, disabled except for backup use)
### **Additional Roles**
- Home Assistant backup storage
- General NAS storage
- Internal web services
- **PLEX32 config backup destination** — receives nightly tar archives from PLEX32 via NFS
Nextcloud on NAS08 uses **ONLYOFFICE Document Server (Community Edition, free)** for in-browser editing of Word/Excel/PowerPoint-compatible files (.docx/.xlsx/.pptx/.odt etc.), providing a Google Docs/MS Office–style collaborative editing experience.
Only **one** Office integration app is active. Nextcloud supports multiple competing Office apps simultaneously, which caused a multi-day outage (see Troubleshooting History below) — **do not enable more than one Office/document-editing app at a time.**
### **Current Configuration**
| Component | Value |
|---|---|
| Nextcloud app in use | `onlyoffice` (official Ascensio app) |
| Document Server container | `onlyoffice` (compose service name) — image `onlyoffice/documentserver:latest` |
| Container host | NAS08, part of the `nextcloud` docker-compose project |
| Internal port | 9980 → container port 80 |
| Public URL | `https://office.kingdezigns.com/` (proxied via NPM on HAS) |
| JWT authentication | **Enabled** — shared secret configured on both the Nextcloud `onlyoffice` app settings and the container's `JWT_SECRET` env var |
| Edition | Community Edition — free, capped at 20 simultaneous editing connections (more than sufficient for home/small network use) |
### **Disabled Office Apps (do not re-enable simultaneously with `onlyoffice`)**
| App | Reason disabled |
|---|---|
| `eurooffice` | Third-party wrapper around the same ONLYOFFICE engine — redundant, was misconfigured (`StorageUrl` DNS issue) |
| `officeonline` | Legacy Microsoft WOPI/Office Online connector — largely deprecated for self-hosting, historically required MS volume licensing |
| `office` | Nextcloud's newer bundled Office app — less mature, no backend configured |
-`ALLOW_PRIVATE_IP_ADDRESS=true` — **required**. Without this, the Document Server's built-in SSRF protection rejects callback requests to Nextcloud's private LAN IP/hostname, causing "Error while downloading the document file to be converted."
-`JWT_ENABLED=true` + `JWT_SECRET=...` — requires every request between Nextcloud and the Document Server to be signed. Without a matching secret on both sides, requests fail with a silent `{"message":"Access denied"}` from Nextcloud's `onlyoffice` app — this does **not** appear in the standard `nextcloud.log` PHP-level log; it only shows in the Apache/container stdout log (`docker logs nextcloud`) and in `nextcloud.log` if you grep specifically for `"app":"onlyoffice"`.
### **Pi-hole Local DNS Requirement**
The ONLYOFFICE Document Server container must reach Nextcloud at its **public hostname** (`cloud.kingdezigns.com`) to download/upload files, because that's the `StorageUrl` Nextcloud is configured with (`overwritehost` = `cloud.kingdezigns.com`). Without a local DNS override, this hostname resolves to the WAN IP, causing a NAT hairpin request that typically fails.
**Required Pi-hole Local DNS Record:**
| Domain | IP |
|---|---|
| `cloud.kingdezigns.com` | `192.168.150.30` |
Configured under Pi-hole Admin → Local DNS → DNS Records. This also improves performance for all other internal clients accessing Nextcloud by its public domain name.
### **Verification Commands**
```bash
# Confirm only onlyoffice app is enabled (not eurooffice/officeonline/office)
# Watch live Document Server logs during a test document open
sudo docker logs -f onlyoffice --tail 0
```
### **Troubleshooting History (2026-07-17/18)**
Office editing broke silently after previously working. Root causes, in the order they were found and fixed:
1.**Four Office apps were installed simultaneously** (`onlyoffice`, `eurooffice`, `officeonline`, `office`), causing Nextcloud to route file-open actions unpredictably. Resolved by disabling all but `onlyoffice`.
2.**Host DNS split-brain** — a stray global `DNS=1.1.1.1 8.8.8.8` in `/etc/systemd/resolved.conf` caused `cloud.kingdezigns.com` to resolve to both the correct internal IP and the public WAN IP, producing a NAT hairpin failure. Fixed by removing the global override (see DNS section above) and adding a Pi-hole local DNS record.
3.**SSRF private-IP block** — the Document Server refused to call back to Nextcloud's private IP by default. Fixed with `ALLOW_PRIVATE_IP_ADDRESS=true`.
4.**JWT mismatch** — Nextcloud had a JWT secret configured while the container had `JWT_ENABLED=false`, causing every Document Server callback to be silently rejected with `{"message":"Access denied"}` — invisible in normal logs, only visible via Apache-level container stdout log or a targeted grep of `nextcloud.log`. Fixed by aligning JWT config on both sides (currently: enabled, with a shared secret).
**Diagnostic lesson learned:** Nextcloud's PHP-level `nextcloud.log` does **not** capture ONLYOFFICE app-level "Access denied" rejections by default in a way that's easy to spot — always check `docker logs nextcloud` (Apache access log) directly for the actual HTTP status code of the failing request, then grep `nextcloud.log` for the specific `app":"onlyoffice"` entries at that timestamp.
---
## 📈 STOCKPROXY — Self-Hosted Stock/Fund Price API
### **Overview**
As of 2026-07-21, NAS08 runs **STOCKPROXY**, a small self-hosted Flask API that supplies
current and historical stock/mutual-fund prices to ONLYOFFICE spreadsheets. It was built
because ONLYOFFICE has no equivalent to Excel's `STOCKHISTORY()`/linked stock data types,
and neither of the obvious alternatives worked on their own:
- **Finnhub** (free tier) has no mutual fund NAV coverage at all — confirmed via direct
testing, returns `{"error":"no data"}` for every mutual fund ticker tried (MKDVX, FIFRX,
etc.). Finnhub is still used directly (not through this proxy) for regular equities via
separate ONLYOFFICE custom functions on the "Dashboard" tab.
- **Yahoo Finance** has good mutual fund coverage (current + historical) but explicitly
blocks direct browser-based (CORS) requests — confirmed by Yahoo Finance library
maintainers themselves ("we will not help you bypass" the block).
STOCKPROXY solves this by calling Yahoo Finance **server-to-server** (no CORS restriction
applies between servers), then re-serving the result to the browser with permissive CORS
headers so an ONLYOFFICE custom function running client-side can call it directly.
Used by the "HSA Investiments Choices" tab of `Rufus Retirement Account Tracking 2026.xlsx`
to track ~50 mutual funds. See that workbook's own documentation for the spreadsheet-side
custom functions (`FUNDPRICE`, `FUNDPRICE_HIST`) and cell layout.
### **Current Configuration**
| Component | Value |
|---|---|
| Container name | `stockproxy` (compose service name) |
| Internal port | 5005 (host and container match) |
| Public URL | `https://stocks.kingdezigns.com/` (proxied via NPM on HAS — see `server_homeassistant.md` Proxy Hosts table) |
| Data source | Yahoo Finance unofficial chart API (`query1.finance.yahoo.com/v8/finance/chart/{ticker}`), scraped server-side with a browser-like `User-Agent` header — no API key required |
| Auth | Shared-secret `key` query parameter on every price endpoint — confirmed rejects requests without it (`{"error":"unauthorized"}`). Secret stored in Vaultwarden, generated via `openssl rand -hex 16` |
| CORS | `Access-Control-Allow-Origin: *` set explicitly — required because ONLYOFFICE custom functions call `fetch()` from the browser tab, not from a server |
### **Endpoints**
| Endpoint | Params | Behavior |
|---|---|---|
| `/current` | `ticker`, `key` | Today's price. In-memory cache, 15-minute TTL (mutual funds price once/day after close; short cache lets same-day NAV postings show up without waiting a full day). |
| `/historical` | `ticker`, `date` (YYYY-MM-DD), `key` | Closing price on/before the given date — resolves to the nearest prior trading day to survive weekends/holidays. Cached **permanently** in SQLite once resolved, since a past date's close never changes. If `date` is today or in the future, transparently falls back to `/current` logic. |
The `synaplan_integration` Nextcloud app is installed and enabled, providing a "Summarize with Synaplan" action on files. **This is currently non-functional** — the app is only a thin client/connector; it requires a separate, full Synaplan server stack to be deployed and reachable.
### **What's Missing**
Synaplan's full self-hosted stack (per synaplan.com) consists of:
- PHP backend
- Vue.js frontend
- MariaDB (separate database instance from Nextcloud's)
- Qdrant (vector database, for RAG/semantic search)
- Optionally Ollama (for local AI inference, avoiding external API calls)
None of these components exist anywhere on the KingDezigns network as of 2026-07-18. The Nextcloud app currently points at a default `http://localhost:8000/api/v1/files/upload`, which fails with connection refused since nothing is listening there.
### **Status: Deferred**
Deploying the full Synaplan stack is a separate project (additional containers, database, storage planning, and a decision on local vs. cloud AI model routing) — not a quick config fix. Revisit when ready to scope it properly.
| `Host "localhost" violates local access rules` | Nextcloud's SSRF protection (`allow_local_remote_servers`) blocked the request before `allow_local_remote_servers` was set to `true` |
| `cURL error 7: Failed to connect to localhost port 8000` | Confirmed no Synaplan backend server exists/listens on the expected port — the connector app was installed without its required backend service |
`allow_local_remote_servers` was set to `true` via:
-`2770` (setgid) ensures new items inherit the `ncshare` group
- Default ACLs (`-d`) ensure new files/folders created by either Nextcloud or NFS inherit `rwx` for `ncshare`
### **NFS Export Configuration (OMV)**
The `kingdezigns-all` and `kingdezigns-public` NFS shares are configured in **OMV → Services → NFS → Shares** with the following extra options on all client entries:
-`all_squash` — maps all NFS client users to the anonymous UID/GID
-`anonuid=33` — maps to `www-data`
-`anongid=1001` — maps to `ncshare`
This ensures files uploaded via NFS land as `www-data:ncshare` regardless of the client user, making them immediately writable by Nextcloud.
> ⚠️ `/etc/exports` is auto-generated by OMV — never edit it directly. All changes must be made through the OMV UI and applied there.
> ⚠️ **Important for PLEX32 backup:** The `all_squash` setting means even `sudo` from PLEX32 cannot create new files on NFS mounts unless the destination directory on NAS08 is owned by `www-data:ncshare` with `2770` permissions. If the PLEX32 backup script fails to copy archives, recheck permissions on the backup directories below.
> ⚠️ **Important for NAS08→NAS16 sync:** The same `all_squash` behavior applies on NAS16's destination. The sync script (`nas08_to_nas16_sync.sh`) self-corrects destination ownership automatically before every run — no manual intervention required.
### **Nextcloud File Scan**
After uploading files via NFS, Nextcloud must be told to index them:
### **Verify backup directories exist and have correct ownership**
```bash
ls -la /export/kingdezigns-all/Docker/plex/config/
ls -la /export/kingdezigns-all/Docker/plex/config/backups/
```
Expected output: `www-data:ncshare` ownership, `drwxrws---` permissions on all directories.
### **SSH Access from PLEX32**
PLEX32 has an SSH key installed on NAS08 (`rufusking` account) — used by the backup script to fetch accurate disk stats via `df`. This is read-only and harmless.
---
## 🛠️ SSD Hardening & Maintenance
### **TRIM**
- TRIM support confirmed on all drives (`DISC-GRAN: 4K`, `DISC-MAX: 4G`)
- systemd `fstrim.timer`**disabled** — replaced by OMV Scheduled Job
- Weekly TRIM script: `/usr/scripts/omv/fstrim-report.sh`
- Scheduled: **Wednesday at 1:00 AM** via OMV Scheduled Jobs
- Script runs `fstrim -av`, gathers SMART health data, sends HTML email report, saves dated log to `/var/log/fstrim/`
- Log retention: 90 days
### **noatime**
- All filesystems mounted with `noatime` — confirmed via `findmnt`
- Managed automatically by the `openmediavault-flashmemory` plugin
- No manual configuration required
### **openmediavault-flashmemory Plugin**
- Installed and active
- Automatically handles: `noatime`, tmpfs for `/var/log` and `/tmp`, swap disabled
**Never use the Nextcloud browser UI to trigger app updates.** Apache workers handle browser-initiated updates and can segfault mid-update, leaving Nextcloud stuck in maintenance mode with cron also blocked — a self-reinforcing deadlock that requires manual `occ` intervention to recover.
All Nextcloud updates must be performed via `occ` on the command line.
- **Shared permissions:** `ncshare` group (GID 1001) shared by `www-data` and `rufusking` — allows both Nextcloud and NFS to read/write the same files.
- NFS exports use `all_squash,anonuid=33,anongid=1001` — all NFS uploads land as `www-data:ncshare` automatically.
- Default ACLs on `/export/kingdezigns-public` ensure all new files/folders inherit `ncshare` group permissions.
- After NFS uploads, run `occ files:scan` to index new files in Nextcloud.
- **Never update Nextcloud apps via the browser UI** — Apache segfaults mid-update leave Nextcloud stuck in maintenance mode. Always use `occ app:update --all` from the command line.
- **Nextcloud update check script:** `/usr/scripts/omv/nextcloud_update_check.sh` — runs daily at 7AM, emails HTML report with ready-to-paste update commands. SMTP password at `/etc/nextcloud-smtp-pass`.
- **Custom `nextcloud-ffmpeg:latest` image (built 2026-07-28):** The `nextcloud`/`cron` services in `nextcloud.yml` run a locally-built image (`FROM nextcloud:latest` + ffmpeg installed via apt), not the stock Nextcloud image. Built by a separate, standalone Compose project at `/kingdezignsnas/Docker/Compose/nextcloud-ffmpeg-build/` (`nextcloud-ffmpeg-build.yml` + `.env`) that the main `nextcloud`/`compose.override.yml` files do not reference. **This must be rebuilt with `docker compose build --no-cache --pull` BEFORE any `docker compose pull` is run in the main `nextcloud` project** — otherwise `pull` fails on `nextcloud-ffmpeg:latest` ("pull access denied", not a real registry image) and aborts a chained `pull && up -d` command before `up -d` ever runs, silently leaving the old container in place while looking like the update succeeded (confirmed 2026-07-29). `--pull` (not just `--no-cache`) is required on the rebuild to actually fetch a fresh `nextcloud:latest` base — `--no-cache` alone can still reuse an already-cached base layer. See dedicated Update Procedure section above for the full corrected 3-step sequence.
- **Nextcloud update check script — core-update false negative fixed (2026-07-25):** the script previously only checked `occ app:update` (app updates), never `occ update:check` (core/server updates), so a core release like 34.0.2 was invisible and the email falsely reported "up to date." Fixed by adding a core-update check, fixing a `\n`-in-double-quotes bug that made emailed commands unreadable, and correcting the emailed update commands to use the real Compose path (`/kingdezignsnas/Docker/Compose/nextcloud/`, root-only, requires `sudo bash -c` + explicit `-f nextcloud.yml -f compose.override.yml` flags — not the default `docker-compose.yml`/`~/docker/nextcloud` assumed originally). Also documented: the official Nextcloud image auto-upgrades on container restart via its own entrypoint — running `occ upgrade` or checking `occ status` immediately after `docker compose up -d` can race that internal process and falsely show the old version for ~30 seconds. The emailed command block now includes a `sleep 30` before the confirming status check. See dedicated section above for full troubleshooting detail and the mock-harness verification method used to confirm the fix.
- **PLEX32 backup destination:** `/export/kingdezigns-all/Docker/plex/config/backups/` — plex and tautulli subdirs receive tar archives every 3 days. Must be `www-data:ncshare 2770` or writes will fail.
- PLEX32 has SSH key installed on NAS08 rufusking account — used for backup disk stats only.
- **NAS08 root SSH key installed on PLEX32** — required for `plex32_reboot.sh` remote reboot script.
- **Remote reboot script:** `/usr/scripts/nas/plex32_reboot.sh` — SSHs into PLEX32, stops containers, reboots, waits for recovery, starts containers, confirms Plex on port 32400 via curl. Registered as disabled OMV Scheduled Job — enable and run manually when needed. Logs to `/var/log/plex32-reboot/`.
- **NAS08→NAS16 sync destination permissions are self-healing** — `nas08_to_nas16_sync.sh` on NAS16 automatically checks and corrects `www-data:ncshare` ownership on `/export/kingdezignsnas-16/Public` before every run. No manual chown/chmod/setfacl needed on NAS16.
- NFS `all_squash` on NAS08 exports is the root cause of rsync permission errors on NAS16 — the sync script handles this transparently via `--no-perms`, `--omit-dir-times`, and the permission precheck.
- **ONLYOFFICE (2026-07-18):** Nextcloud document editing runs via ONLYOFFICE Document Server (Community Edition, free) — Docker container `onlyoffice`, image `onlyoffice/documentserver:latest`, port 9980, public URL `https://office.kingdezigns.com/`. Only the `onlyoffice` Nextcloud app is enabled (eurooffice/officeonline/office disabled — never re-enable more than one Office app simultaneously). Requires `ALLOW_PRIVATE_IP_ADDRESS=true` env var, JWT auth enabled with a shared secret between Nextcloud app config and container `JWT_SECRET`, and a Pi-hole local DNS record for `cloud.kingdezigns.com` → `192.168.150.30` to avoid a NAT hairpin failure. See dedicated section above for full troubleshooting history.
- **DNS fix (2026-07-18):** `/etc/systemd/resolved.conf` had a stray global `DNS=1.1.1.1 8.8.8.8` override that merged with the correct per-interface Pi-hole DNS, causing duplicate/incorrect resolution for internal hostnames. Removed — only `FallbackDNS=9.9.9.9` remains as a last-resort. Docker containers require a restart to pick up host DNS changes.
- **Synaplan (2026-07-18):** `synaplan_integration` Nextcloud app is installed but non-functional — it is only a connector; the required Synaplan backend server stack (PHP, MariaDB, Qdrant, optional Ollama) has not been deployed. Deploying it is a separate future project. `allow_local_remote_servers` was set to `true` system-wide to unblock local-network AI backend calls in general (needed for Synaplan and similar future integrations).
- **STOCKPROXY (2026-07-21):** Self-hosted Flask API, Docker container `stockproxy`, own OMV-style Compose project at `~/docker/stockproxy/`, port 5005, public URL `https://stocks.kingdezigns.com/` (proxied via NPM on HAS). Supplies current + historical mutual fund prices to ONLYOFFICE via Yahoo Finance (server-side scrape, bypasses browser CORS block) — Finnhub was evaluated first but has no mutual fund NAV coverage and was dropped from this service. Current-price cache: 15 min in-memory. Historical-price cache: permanent, SQLite at `/opt/stockproxy/data/cache.db`. All endpoints require a shared-secret `key` param (Vaultwarden). Built to support the "HSA Investiments Choices" tab in `Rufus Retirement Account Tracking 2026.xlsx`. See dedicated section above for full config, endpoints, and known limitations.
- **RANK.EQ Err:522 circular reference fix (2026-07-23, working):** The "HSA
Investiments Choices" tab's fund ranking (`RANK.EQ`) broke with a circular-reference
error (Err:522) on any sheet edit — but only after loading correctly — because it
depended on ~50 cells fed by live `FUNDPRICE`/`FUNDPRICE_HIST` network calls, and an
incremental recalc could catch those async cells mid-flight. Fixed by adding a
button-triggered LibreOffice Basic macro (`CopyStockValues`) that snapshots the live
`P`/`Q` price columns into static value-only columns `R`/`S` via
`oDest.setDataArray(oSource.getDataArray())`; percent-change (`T`/`U`) and
`RANK.EQ` were rebuilt to read from the static columns instead of the live ones. `P`/`Q`
remain live and unaffected — only the ranking chain was decoupled. See dedicated section
above for full root-cause analysis, macro code, and diagnostic dead ends (notably:
`ActiveSheet` must be used instead of `Sheets.getByIndex(0)` for multi-tab workbooks, and
a cell-by-cell `getValue()`/`setValue()` loop silently failed where a bulk
`setDataArray()` copy succeeded).
- **Collabora Finnhub removal — STOCKPRICE/STOCKTIME replaced with FUNDPRICE/FUNDTIME
(2026-07-23, working):** `STOCKPRICE`/`STOCKTIME` errored because Finnhub's hostname was
never added to Collabora's `net.lok_allow` allowlist. Rather than add it, Finnhub was
dropped entirely — `FUNDPRICE` now also serves equity tickers (STOCKPROXY doesn't
distinguish fund vs. stock), and a new `FUNDTIME` function reads STOCKPROXY's
`resolved_date` field in place of Finnhub's timestamp. Note: `FUNDTIME` returns a date
only, not time-of-day — a regression from Finnhub's old intraday timestamp, accepted as a
tradeoff for now. `FINNHUB_KEY()` config and both old functions removed from
`StockFunctions.bas`; new `JsonString()` helper added for parsing quoted JSON string
fields. See dedicated section above.
- **Collabora stock/fund price macro integration (2026-07-22, working):** Ported the ONLYOFFICE `FUNDPRICE`/`FUNDPRICE_HIST`/`STOCKPRICE`/`STOCKTIME` custom functions to a LibreOffice Basic module (`StockFunctions.bas`) calling the same STOCKPROXY/Finnhub backends — no backend changes needed. Required: (1) Collabora test copy saved as `.ods` not `.xlsx` — Basic modules don't reliably persist in `.xlsx`; (2) `enable_macros_execution=true` in `coolwsd.xml` (disabled by default post-CVE-2025-24796); (3) added `stocks\.kingdezigns\.com` to the `net.lok_allow` host allowlist in `coolwsd.xml` — this allowlist matches literal hostname/IP-text regex patterns, not DNS-resolved IPs, so the hostname needed its own explicit entry even though it resolves to an already-allowed private IP. Macro edits must be done in desktop LibreOffice (Collabora's browser UI has no working Basic IDE) then re-uploaded. Also fixed during this work: the `stocks.kingdezigns.com` Pi-hole local DNS override existed but was not actually resolving (returning WAN IP) — fixed with `docker restart pihole` (v6's `reloaddns` errored, `restartdns` doesn't exist in v6). See dedicated section above for full troubleshooting detail, exact XML edits, and open follow-up items.