Role. Sole engineer. I design, run, and maintain the whole setup end to end: local language-model inference on a 128 GB MacBook Pro, and a self-hosted services stack on a Synology NAS with the storage, networking, and backup design under it.
Problem
I wanted to self-host the things I rely on instead of renting them: large language models I could run locally rather than pay for by the token, and the services I depend on day to day, all on hardware I own so the data and the uptime stay mine. The constraints are the hardware on both ends. The language models have to run on a MacBook Pro with no discrete GPU, which means fitting 70-billion- to 120-billion-parameter models into memory. The services run on a Synology NAS built on an AMD Ryzen Embedded R1600 — two physical cores, four threads, no GPU — that has to carry a stack of containers, a photo archive meant to outlive the app that reads it, and a media server that stutters whenever another process spikes the CPU. All of it has to stay dependable and reachable from outside the house with no inbound ports open on the router.
Approach
- Run language models locally on a 128 GB M3 Max through llama.cpp on Metal, LM Studio, and Apple's MLX. The 128 GB of unified memory is what lets 70B-parameter models in 8-bit and larger 120B-class models at reduced precision fit at all on a laptop with no GPU, with inference staying on the machine instead of a cloud API.
- Built a local speech-to-text path: system audio captured through a virtual loopback device into whisper.cpp, so transcription runs on-device rather than through a cloud service.
- Consolidated the self-hosted services into a single Docker Compose project of eight containers on a user-defined bridge network with static per-container addresses, so a container restart never shuffles an IP out from under the reverse proxy in front of it.
- Diagnosed media-server stutter as a bursty CPU problem, not a chronic one: background post-processing would spike all four threads and starve the latency-sensitive media server during a burst. Fixed it with cpuset pinning — reserving a CPU thread for the media server and confining the container stack to the rest. The DSM kernel has no CFS bandwidth controller, so a quota-based limit was off the table and pinning was the tool that actually worked.
- Kept the pinning alive across updates: a container recreate during an image pull drops the cpuset, so an idempotent re-pin script runs on a ten-minute cron and re-applies it.
- Designed the photo archive so the data outlives the viewer. I exported roughly 409 GB from Google Photos, and the archive layout keeps photos as plain dated files I own — date and GPS written back into each file's metadata — with the photo app indexing them read-only rather than owning them. The toolchain to bake the metadata and mount the read-only library is staged; the export is the part that's done.
- Designed durability as a 3-2-1 backup: a nightly database dump so albums and faces survive, an on-site USB copy, and an encrypted offsite copy in an open format. The nightly dump runs today; the USB and offsite legs are still being stood up.
- Placed I/O by tier. The NAS has a five-disk RAID5 of spinning disks for bulk cold storage and a two-disk RAID0 of NVMe SSDs for the fast path. I moved the hot database, generated thumbnails, and download scratch onto the NVMe while keeping the irreplaceable originals on the redundant array, since RAID0 has no redundancy to lose.
- Reclaimed about 2.2 TB by tracking down a stale locked filesystem snapshot that was pinning deleted data, which dropped one volume from 70% to 53% full.
- Wrote a small LAN web app for the household: a bilingual groceries and pantry tracker on Flask backed by SQLite, with a schema-migration path I hardened after an adversarial review of the upgrade and concurrency behavior.
- Exposed only the services that had to be remote through a Cloudflare tunnel with Access in front of them, instead of forwarding ports on the home router. I tried a mesh VPN first and dropped it when the NAS kept losing its tunnel device across reboots.
Impact
- Large models run on the laptop itself, so that inference needs no cloud API and no data leaves the machine.
- The media server stopped stuttering during heavy container activity once a CPU thread was reserved for it, and the pin re-applies itself after updates without my attention.
- Eight services run as one declarative Compose project with fixed addressing, so recreating the stack or a single container is a known, repeatable operation rather than a manual rebuild.
- The photo archive is designed not to depend on the viewer surviving: the files stay mine in an open, dated layout, and the app only reads them.
- Tier-aware placement put random-I/O work on flash and cold bulk on the redundant disks, and the snapshot cleanup gave back about 2.2 TB without buying a drive.
- The household Flask app survives schema upgrades and concurrent writes that an earlier version would have corrupted, because the migration path was hardened against them directly.
- Nothing remote is reachable without authenticating against the access policy first, and the home router has no ports forwarded.
Stack
Apple M3 Max (128 GB unified memory) · llama.cpp / Metal · Apple MLX · LM Studio · whisper.cpp · Synology DSM (AMD Ryzen Embedded R1600, 2 cores / 4 threads) · Docker Compose · user-defined bridge networking with static IPs · cpuset CPU pinning · cron · self-hosted photo archive (read-only external library, designed) · PostgreSQL · Btrfs · RAID5 and RAID0 / NVMe storage tiering · 3-2-1 backup (nightly DB dump live; on-site USB and encrypted offsite in progress) · Flask · SQLite · Cloudflare Tunnel + Access · Linux
This is a personal build I run at home; the specifics here are ones I can walk through end to end in an interview.