| 1 | # clover's snow globe |
| 2 | |
| 3 | This codebase contains declarative configuration to run my home server, a |
| 4 | computer sitting in my closet used daily to power my activities. It first and |
| 5 | foremost acts as a file store for my ongoing and archived projects for |
| 6 | [paper clover]. Secondly, it contains a ton of apps for me and friends to use, |
| 7 | from Jellyfin and Navidrome for media consumption to git hosting and render |
| 8 | farms. |
| 9 | |
| 10 | The system is built out of a NixOS base (just to install the system itself), |
| 11 | using managed docker containers (managed via Nomad) and virtual machines to run |
| 12 | all the real services. There is a lot of custom tooling to spawn interactive |
| 13 | testing environments. For example, without even pushing any code, I can edit the |
| 14 | configuration of a service or upgrade it, then spin up an isolated domain like |
| 15 | `shale-4c84f9da.staging.paperclover.net` to verify changes. Then, pushing to |
| 16 | production ensures that the new deployment is healthy before routing traffic to |
| 17 | it -- It's kind of like "easy kubernetes." |
| 18 | |
| 19 | [paper clover]: https://paperclover.net |
| 20 | |
| 21 | ## deployment loop |
| 22 | |
| 23 | ```sh |
| 24 | python3 tools/deploy.py stage shale # preview the working files, even without a commit message |
| 25 | python3 tools/deploy.py stage shale # update the same URL and staging data after another edit |
| 26 | python3 tools/deploy.py publish # upload the described, conflict-free main commit for GUI review |
| 27 | python3 tools/deploy.py prod # upload and deploy main directly |
| 28 | ``` |
| 29 | |
| 30 | Production always deploys a snapshot exported from `main`, never working files |
| 31 | or a stage. The dashboard's **deploy main** page shows the uploaded commit and |
| 32 | its message; deploying applies the full repository configuration and retains |
| 33 | production data. A newer upload invalidates an older deployment confirmation. |
| 34 | Sibling application build sources declared in `build-source.json` are bundled |
| 35 | at upload time. Their frozen contents are part of the release digest. |
| 36 | |
| 37 | `main` joins the infra-2 and home-infra histories. Its tree contains infra-2; |
| 38 | the retired configuration remains available in the home-infra parent history. |
| 39 | |
| 40 | ## installer |
| 41 | |
| 42 | After publishing main, build the prepared USB image on an x86 Linux host: |
| 43 | |
| 44 | ```sh |
| 45 | nix build --extra-experimental-features 'nix-command flakes' path:/opt/studio/main#installer |
| 46 | ``` |
| 47 | |
| 48 | The ISO is in `result/iso/`. It boots a live installer with this Mac's SSH key, |
| 49 | ZFS and migration tools, the uploaded repository at `/etc/infra-2`, and a cached |
| 50 | dashboard image. It does not install automatically. The physical installation |
| 51 | uses `#zenith` after generating its hardware configuration; the existing data |
| 52 | pool and service state follow the [handoff](tools/legacy-handoff.md). |
| 53 | |
| 54 | On Zenith, the live installer uses the same wired addresses and gateway as the |
| 55 | installed system. Once firmware boots the USB, connect from this Mac with |
| 56 | `ssh root@10.0.0.1`. SSH starts automatically and accepts the admin key only; |
| 57 | no monitor, local login, or DHCP address lookup is needed after USB boot. |
| 58 | |
| 59 | ## filesystem layout |
| 60 | |
| 61 | The computer mounts the ZFS root dataset under `/srv`, meaning "server," loosely |
| 62 | following the linux convention. Within, it's a unique structure: |
| 63 | |
| 64 | ``` |
| 65 | srv/ |
| 66 | +--- clover/ [dataset] user-data storage, what I mount as /Volumes/clover |
| 67 | +--- Archive/<year> personal archive. one folder per year, scrambled within. |
| 68 | +--- Asset/ resources, sample packs, audio plugins, stock videos |
| 69 | +--- Blender/ |
| 70 | +--- Font/ (moved from zenith Documents/Font, will be stable index) |
| 71 | +--- Samples/ |
| 72 | +--- Texture/ |
| 73 | +--- Video/ |
| 74 | +--- Documents/ |
| 75 | +--- Project/ currently active project files |
| 76 | +--- Media/ [dataset] files from the world-wide-web. managed partially manually |
| 77 | +--- jellyfin/ |
| 78 | +--- mirror/ |
| 79 | +--- music/ (used by navidrome) |
| 80 | +--- music-intake/ (to be manually indexed) |
| 81 | +--- seedbox/ |
| 82 | +--- vm/ (virtual machine images) |
| 83 | +--- Published/ source of truth for `paperclover.net/file` |
| 84 | +--- prod/ [dataset] production application data |
| 85 | +--- shale/ [dataset] (each service is its own dataset) |
| 86 | +--- ... |
| 87 | +--- staging/ staging deployments use this space for temporary clones |
| 88 | +--- postgres-9f06f74b/ [dataset] |
| 89 | +--- vm/ [dataset] virtual machines |
| 90 | +--- clover-sandbox/ [dataset] |
| 91 | ``` |
| 92 | |
| 93 | Media is it's own dataset so it can be snapshotted independently of my personal |
| 94 | data (less frequent, lower retention), and all my personal files are on the same |
| 95 | dataset to allow fast move/copying between top level folders. Services use their |
| 96 | own datasets to implement copy-on-write forks. |
| 97 | |
| 98 | ## testing domains on a Mac |
| 99 | |
| 100 | [tools/mac-domains.py](tools/mac-domains.py) routes `.studio.test` and its nested |
| 101 | subdomains through the rehearsal VM's HTTPS, DNS, and login services. Start the |
| 102 | rehearsal SSH forwards first, then run: |
| 103 | |
| 104 | ```sh |
| 105 | sudo /usr/bin/python3 tools/mac-domains.py start /path/to/rehearsal-ca.crt |
| 106 | ``` |
| 107 | |
| 108 | The relay binds loopback ports, drops administrator privileges, and passes TLS |
| 109 | through to the VM. Local UDP DNS queries use the SSH tunnel's TCP DNS connection. |
| 110 | Existing certificate trust is preserved. Mac DNS and hosts |
| 111 | settings are backed up before editing; `sudo /usr/bin/python3 tools/mac-domains.py |
| 112 | stop` restores them and stops the relay. Undo refuses to overwrite later edits. |
| 113 | After a Mac restart, run stop and start again to restart the relay. |
| 114 | |
| 115 | The dashboard package uses `home-dashboard`. Existing state paths, environment |
| 116 | names, service IDs, and telemetry names keep their old names for compatibility. |
| 117 | |
| 118 | ## dashboard boundary |
| 119 | |
| 120 | NixOS runs `studio-dashboard` in a non-root Podman container with a read-only |
| 121 | root, private network, resource limits, and explicit data mounts. The small |
| 122 | `studio-host` service authenticates the dashboard UID on its Unix socket and |
| 123 | performs bounded ZFS, VM, deployment, host-sampling, and identity operations. |
| 124 | Host control sockets and management credentials stay outside the container. |
| 125 | |
| 126 | The MCP tab manages separate observability, agent, and Shale catalogs through |
| 127 | the existing Keycloak realm. Each connection has explicit service, machine, or |
| 128 | repository grants; Shale credentials belong to the signed-in user. Agent Relay's |
| 129 | existing outbound client protocol connects to the Rust server. |
| 130 | |
| 131 | Build the image with `nix build .#dashboard-image` on Linux. Run |
| 132 | `python3 tools/dashboard-unit-test.py --output /tmp/dashboard-checks.json` on the |
| 133 | rehearsal VM to exercise the generated NixOS units, containment, IAM, and MCP |
| 134 | connectors with disposable fixtures. `--relay-agent-dir` includes the existing |
| 135 | Agent Relay client interoperability check; `--browser-ready-file` temporarily |
| 136 | routes the public dashboard to the fixture for browser and SSO load checks. |