Goal: run famstack on a Mac safely. Install, lifecycle, troubleshoot, back up. No code changes from this role - that is the engineer role (see dev.md).
For full prose, see ../admin-guide.md. This file is the compact decision layer; the admin guide is the manual.
stack.toml(per-host config) + each stacklet'sstacklet.toml→ render.envon everystack up..envis derived, never edit it.- Every stacklet lives under
stacklets/<id>/, or under~/famstack-extensions/<id>/if it does not ship with famstack. All persistent data lives under~/famstack-data/<id>/. No exceptions. - State is derived, not stored. Container existence == "running"; container absence + data dir absence == "available". There is no enabled-list.
- macOS on Apple Silicon (M1+).
- 16 GB RAM minimum; 32 GB recommended (AI quality, especially non-English).
- Homebrew installed. It is the only manual dep - it pulls everything else.
- OrbStack (preferred) or Docker Desktop running.
If a precondition is missing, ./stack prints exactly what to do. Don't improvise.
| Command | Idempotent | Destructive | Touches |
|---|---|---|---|
./stack |
yes | no | interactive installer (first-run path) |
./stack up <id>... |
yes | no | starts containers, renders .env, runs hooks |
./stack down <id>... |
yes | no | stops containers; data preserved. Several ids: spaces or commas |
./stack down all |
yes | no | stops every running stacklet in reverse dep order |
./stack restart [<id>...] |
yes | no | down + up; several ids as for down; with no argument, only the stacklets running stale code |
./stack destroy <id>... |
yes | YES | removes containers + ~/famstack-data/<id>/ + secrets |
./stack uninstall |
yes | YES, EVERYTHING | destroys every stacklet, network, all data, config |
./stack update [<tag>] |
yes | no | moves the checkout to a release; restarts nothing, prints what to restart |
./stack list |
yes | no | reports state |
./stack status |
yes | no | runs health checks |
./stack logs <id> |
yes | no | tail container logs |
./stack errors |
yes | no | recent error logs (24h) |
./stack host |
yes | no | disk / memory / uptime |
./stack config [--secrets] |
yes | no | prints resolved config |
./stack ai models |
yes | no | lists installed AI models |
./stack ai switch <url> |
yes | no | points [ai] at an AI server the stack does not manage; checks it, installs nothing, restarts nothing. Warns when the address is outside the home network |
./stack ai switch managed |
yes | no | moves [ai] to the engine the stack runs here: runs stack up ai, installing oMLX when missing; keeps the previous server if that fails |
./stack setup ai |
yes | no | re-runs AI install (model swap path) |
./stack infra dns-token |
yes | no | stores or replaces the DNS provider API token (HTTPS in domain mode); ./stack up infra applies it |
Output contract: every command returns JSON when piped or when --json is passed. Force human output with --pretty. Exit code 0 == success.
Applying config/compose changes to a running stacklet: stack up <id> is the apply command — it re-renders .env and recreates that stacklet's containers. It force-recreates unconditionally (docker.py compose_up), because compose's config hash does not cover env_file contents, so a plain up -d would leave a container on stale env. That makes up and restart (= down + up) similar in cost; restart stops everything first and runs the stop hooks. Plain docker compose restart does neither: it won't re-render env or create new services.
stack upis always safe to re-run. Failures leave the system in a valid intermediate state; re-running picks up where it failed..envis a derived artifact. Never edit by hand. Overwritten on everystack up..stack/secrets.tomlis gitignored and contains generated passwords + tokens. Treat as a password export.- Data lives outside the repo at
~/famstack-data/<id>/. That's the only mandatory backup. ~/.omlx/models/holds LLM model files. Re-downloadable; not indata_dir.- Extension stacklets are source code, and nothing backs them up.
~/famstack-extensions/<id>/holds stacklets famstack does not ship. Nothing creates it, nothing deletes it, and it is outside the data dir the backup rule covers. Push it to a remote.[core] extension_dirs(a list, first entry wins a clash and is the one mounted into the bot runner) adds or moves search paths. - A stage is a claim, not a fault.
stack listandstack statusgroup extensions in their own section and show a stage that is not stable. The warning onstack upmeans the stacklet can change or disappear between releases. Do not put anything irreplaceable in it. - Two famstack instances cannot run on the same Mac at the same time - container names collide (
stack-<id>).stack downthe active one first.
Refuse without explicit, scoped human approval:
| Action | Why |
|---|---|
./stack destroy <id> |
Deletes ~/famstack-data/<id>/ permanently. No undo. |
./stack uninstall |
Wipes every stacklet's data, secrets, runtime state, and config. |
rm -rf ~/famstack-data/... |
Bypasses the CLI's safety prompts. |
Editing .env directly |
Will be silently overwritten next stack up. |
Editing .stack/secrets.toml |
Breaks every stacklet that depends on the changed secret. |
Moving data_dir while stacklets are running |
Bind mounts break. stack down all first, then move, then stack up. |
Changing stack.toml [core] language |
Re-seeds Paperless taxonomy. Orphan tags require manual cleanup. |
Switching [core] domain empty ↔ non-empty |
Switches port mode ↔ domain mode. Needs *.<domain> and <domain> DNS records to the Mac, and the infra stacklet up. |
| Pointing the router's DNS at the Mac (infra) | Every device on the LAN then resolves through AdGuard; the Mac or AdGuard going down takes name resolution for the whole house with it. The admin changes the router, never an agent; note the previous DNS setting first. |
./stack down infra / ./stack destroy infra |
With the router pointing at the Mac, the house loses DNS until infra is back or the router is switched back. |
| Port | Stacklet | Service |
|---|---|---|
| 42000 | core | Link resolver for /go/* (tool endpoints are internal to the stack network) |
| 42010 | photos | Immich web + API |
| 42020 | docs | Paperless-ngx |
| 42030 | messages | Element web |
| 42031 | messages | Synapse (Matrix homeserver - phones connect here) |
| 42040 | code | Forgejo web |
| 222 | code | Forgejo SSH (macOS uses 22 itself) |
| 42050 | chatai | Open WebUI |
| 42060 | ai | oMLX |
| 42062 | ai | Whisper |
| 42070 | memory | Family wiki (Quartz) |
| 42080 | infra | AdGuard admin (domain mode, Mac only) |
| 42081 | infra | AdGuard setup wizard, first run (domain mode, Mac only) |
| 53, 80, 443 | infra | DNS and the Caddy proxy (domain mode, whole LAN) |
Port collisions: do not silently rebind. Surface them. The user's fix is "stop the offender" or switch to domain mode.
| Symptom | First check | Likely cause |
|---|---|---|
| "Docker is not running" | OrbStack/Docker Desktop icon present? | App not started. |
| "Port 42xxx already in use" | lsof -nP -iTCP:<port> -sTCP:LISTEN |
Another service grabbed it. |
| Phone can't reach server | Same Wi-Fi? Guest network isolating clients? | Network isolation. |
| "Server not Matrix" on phone | ./stack status → messages |
Synapse down. ./stack restart messages. |
| Mac IP keeps changing | Router DHCP behaviour | Set DHCP reservation on the router. |
| AI install fails at whisper.cpp build | xcode-select -p |
Xcode CLT missing. xcode-select --install. |
brew: command not found after install |
PATH on Apple Silicon | Add eval "$(/opt/homebrew/bin/brew shellenv)" to ~/.zshrc. |
| LLM OOM / very slow | RAM tier | Edit [ai] default to smaller model, then ./stack setup ai. |
stack list shows ai as remote or localhost |
[ai] provider in stack.toml |
The stack uses an AI server it does not manage (stack ai switch): another machine, or an app on this Mac (localhost). stack up ai keeps it; stack ai switch managed switches back to this Mac. |
| Voice messages fail on a remote AI setup | ./stack ai switch <url> output, "Voice messages" line |
The AI server lists no speech-to-text model. Pass --whisper <url> for a speech server. |
| Disk full | ./stack host |
Likely the photo library. Move data_dir to external SSD. |
| Element warns "browser not supported", then never loads | Which address is open? | Port mode over the LAN IP: plain HTTP is not a secure context. On the server Mac open http://localhost:42030; other computers use the Element desktop app with http://<ip>:42031. |
| Wiki pages stale after filings | docker logs stack-memory-curator |
Curator debounces (~3 min quiet) before rebuilding; topic pages wait for the nightly sweep. Manual override: ./stack memory wiki update (what the last filings touched) or ./stack memory wiki update --all (everything, as at night). |
| Wiki shows old content entirely | docker logs stack-memory-curator |
The curator owns the vault git pull (the wiki container is a read-only view). Curator down/stuck → vault and wiki go stale together. |
| Curator logs "waiting for vault" | ./stack status → memory, code |
Vault not cloned yet — the memory install hooks own the initial clone. |
For symptoms not on this table: ./stack logs <id> + ./stack errors, paste output to the user. Do not invent fixes.
A release is a git tag. ./stack update moves the checkout to one and names what that staled; ./stack restart with no argument then restarts exactly those. --dry-run shows the plan, --yes skips the confirmation, a tag argument picks a release other than the newest.
- It never restarts anything. New code on disk is not new code running; recreating containers is the admin's decision. The command names what the release staled: stacklets it changed and that are running, or every running stacklet when anything under
lib/changed. ./stack restartwith no argument applies it. Every container is stamped atstack upwith the commit it started from (stack.commit), sostack listandstack doctorreport a stacklet running code the checkout has moved past, and a barerestartrestarts those. A container with no stamp is reported as unknown, never guessed about.- Local edits are set aside and put back. If they collide with the release the whole update winds back: same release as before, edits in place, stash empty. Never leave a tree carrying conflict markers, a compose file with them in it does not parse.
- Before v0.3.0-beta.4 there is no
update. By hand:git fetch --tags && git checkout <tag> && ./stack doctor, then restart whatgit diff --name-only <old> <new> -- stacklets/names. - Works from a branch or a fork. On
mainit moves to the tag and says the branch is left behind (git switch mainreturns). Tags are fetched from every remote, so a fork needs the project added as a remote or there is nothing to update to. - A tag checkout is a detached HEAD.
git pullfails there. Never suggestgit pull origin mainas the fix: it succeeds and silently moves the instance onto unreleased code while./stack versionstill reports the last release. ./stack versionnow says what is running, not what the source constant claims:0.3.0-beta.3-104-g9a5bdc9-dirtyis the tag, the distance past it, the commit, and an unclean tree.version,status,list,doctorandupdateall print the same string../stack doctoradds a warning when a newer release exists, without fetching, so that half is as fresh as the lastgit fetch.- The instance survives a tag switch. Config, secrets, data and
~/<product>-extensions/are all outside git.
Full prose: ../admin-guide.md § Updating.
~/famstack-data/- mandatory. Time Machine, restic, or rsync. Pick one.stack.toml,users.toml,.stack/secrets.toml- small, irreplaceable, gitignored. One-liner:tar czf famstack-config-$(date +%F).tgz stack.toml users.toml .stack/~/famstack-extensions/- only if the instance has one. Source code for stacklets famstack does not ship; a git remote counts.~/.omlx/models/- re-downloadable; skip.
This role describes operating an existing famstack install. Do NOT, from the operator role:
- Edit code in
stacklets/,lib/, ortests/. That's the engineer role; see dev.md. - Create or merge PRs.
- Run
./stack uninstallto "start fresh" without explicit user approval. - Suggest exact config strings (URLs, ports, IPs, field names) from memory. Verify against
stack.toml, the user guide, or./stack config.
When in doubt: ./stack status and ask.