famstack is an opinionated, batteries-included stack built on established open source projects (Immich, Paperless, Matrix, MLX). The glue is the part that matters: everything is wired together and reachable through chat, so your family actually uses it instead of just you.
This guide is for the person who runs the server: hardware, install, the stacklets, day-to-day operations, troubleshooting. One page on purpose. Search it with Cmd+F.
Living with famstack rather than running it? The User Guide covers everything your family does in chat: filing documents, saving links, voice memos, asking questions.
If you get stuck, jump into Discord or open an issue on GitHub.
Prefer to watch? Full install walkthrough on YouTube: 19 minutes, clone to a working document archive with mobile access. The whole famstack channel has the short clips too.
⚠️ Local network only. famstack runs plain HTTP and trusts your LAN. Never install it on a machine reachable from the public internet: no VPS, no forwarded ports, no DMZ. For access from outside the house, use a VPN (Tailscale works well and is free for families).
If you already have Homebrew:
git clone https://github.com/famstack-dev/famstack.git
cd famstack
./stackThe installer asks four or five questions, brings up Matrix and Element, and prints a sign-in URL. Open it and you have a working family chat.
Everything else is one command at a time:
./stack up photos # Immich (photo library, mobile backup)
./stack up docs # Paperless-ngx (document archive with OCR)
./stack up ai # Local AI (oMLX + Whisper + Piper TTS)
./stack up code # Forgejo (private git server)
./stack up memory # The memory of your family brain. The curated knowledge.Verify:
./stack status
./stack listIf you do not have Homebrew yet, see Install below.
Do this before you sign anyone in on a phone.
famstack is a server. Every phone, every Immich app, every Element X session points at the Mac's IP on your home network. If your router hands the Mac a different DHCP lease next week, every device stops connecting and you reconfigure the household.
Pick one, in order of preference:
- DHCP reservation on the router (best). Reserve the Mac's current IP to its MAC address. Five minutes, never breaks again.
- Manual static IP on the Mac. System Settings > Network > Details > TCP/IP > Configure IPv4: Manually. Choose an address inside your subnet but outside the router's DHCP range.
- Bonjour name as a fallback. Use
<mac-name>.local(System Settings > General > Sharing > Local hostname) if your router lets mDNS through. Less reliable than option 1 or 2.
If the IP changes later, every phone has to be repointed. Save yourself the family meeting.
famstack runs on Apple Silicon. Whisper, Piper and oMLX all use Metal GPU acceleration. Intel Macs technically run Docker, but the AI parts will be unusably slow.
| Resource | Minimum | Recommended | Why |
|---|---|---|---|
| CPU | Apple Silicon (M1) | M2 or newer | Metal GPU is mandatory for the AI stacklet. |
| RAM | 16 GB | 32 GB or more | See the RAM note below. |
| Disk | 60 GB free | 256 GB+ free | Software is small. Photo library and AI models are not. |
| Network | Local LAN | Wired ethernet for the host | Stable host link helps phone uploads. |
The AI stacklet picks an LLM at install time based on sysctl hw.memsize:
| RAM | Default model | Notes |
|---|---|---|
| 32 GB or more | Qwen3.6-35B-A3B UD 4-bit | Best quality, including for German. |
| 16 GB | Qwen3.5-9B 4-bit | Lightweight. English is fine. German output is noticeably weaker, especially on document classification with long, formal text. Qwen3.6 has no small variant, so this tier stays on 3.5. |
If your household uses German (or any non-English language) and you care about how documents get tagged, transcribed and summarized, 32 GB is the sweet spot. 16 GB works but you will feel the difference. You can always switch models later by editing [ai] default in stack.toml and running ./stack setup ai.
The chatai stacklet (Open WebUI) is also memory-hungry. On 16 GB Macs, skip it. Local AI still works for document classification and voice transcription without a chat UI.
What actually fills the disk:
- Docker images: roughly 5 to 8 GB across all stacklets.
- Whisper model: 1.5 GB (large-v3-turbo).
- LLM model: 2.5 GB (9B 4-bit) up to 25 GB (35B 8-bit).
- Photo library: depends on you. Plan for what you would back up to iCloud, plus about 20 percent overhead for Immich thumbnails.
- Documents: small. Even 10 years of household paperwork rarely exceeds a few GB after OCR.
data_dir defaults to ~/famstack-data but accepts any path. If your internal SSD is tight, point it at an external drive in stack.toml before running ./stack up photos for the first time.
One dependency: Homebrew.
Installing Homebrew pulls in the Xcode Command Line Tools (which gives you git and python3), and the famstack installer uses brew to install OrbStack and, if you enable the AI stacklet, oMLX plus its build dependencies. So if you have Homebrew, you have everything you need.
If it is not installed:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"Follow the post-install instructions to add brew to your shell $PATH. Open a fresh terminal afterwards.
You do not need to install OrbStack, Docker, oMLX, cmake or anything else by hand. The installer handles them.
famstack uses the 420xx range to avoid conflicts with anything you might already run:
| Port | Service |
|---|---|
| 42010 | photos (Immich) |
| 42020 | docs (Paperless-ngx) |
| 42030 | messages (Element web) |
| 42031 | messages (Synapse, Matrix homeserver) |
| 42040 | code (Forgejo) |
| 42050 | chatai (Open WebUI) |
| 42060 | ai (oMLX) |
| 42061 | ai (LM Studio, optional GGUF backend) |
| 42062 | ai (Whisper) |
| 42063 | ai (Piper TTS) |
| 42070 | memory (family wiki) |
If a port is taken, stop the offender or switch to domain mode (see stack-reference.md).
Pick a directory, e.g. ~/server/famstack. Do not put it inside iCloud Drive or Dropbox. The file watchers will fight Docker.
mkdir -p ~/server && cd ~/server
git clone https://github.com/famstack-dev/famstack.git
cd famstackOptional: pin to the latest tag instead of main. main is the working branch and is usually fine, but tags are what we test.
git fetch --tags
git checkout $(git describe --tags --abbrev=0) # latest release tagThe current tags are listed on the releases page.
Sanity check:
ls -l stack
./stack --help./stack is a 4-line bash wrapper that boots the Python CLI from lib/. It uses your system python3, no virtualenvs, no pip install. That is intentional.
./stackWhat it does, in order:
- Checks Homebrew. If missing, prints the install one-liner and waits.
- Checks Docker. If missing, offers to install OrbStack via Homebrew. If installed but not running, asks you to start it and waits.
- Asks for a family name. Becomes your Matrix server identity. Permanent. Pick something short and lowercase-friendly.
muelleris fine; "The Müller-Schmidt Family" gets sanitized tomueller-schmidt. - Asks for your first name. Becomes the admin account on every stacklet. Default password is your first name in lowercase. Change it at first login.
- Asks for additional family members. Empty input ends the loop. Each member gets their own account on each stacklet.
- Writes config. Three files appear:
stack.toml: central config, gitignored, yours to edit.users.toml: family roster, gitignored..stack/secrets.toml: auto-generated passwords for service accounts. Treat like a password manager export.
- Brings up
messagesandcore. Pulls Synapse, Element and Postgres images (about 600 MB), starts them, creates Matrix accounts, seeds two default rooms (#famchat,#famstack). - Prints a sign-in URL. Something like
http://192.168.1.42:42030.
Total time on a fresh Mac with a decent connection: 5 to 10 minutes.
If something fails partway through, fix the underlying issue (usually Docker not running) and run ./stack again. The installer is idempotent.
Open the URL the installer printed. You should see Element. If your browser warns "this browser is not supported", click Continue anyway. Element works in Safari, Chrome, Firefox and Edge despite the warning.
- Username: your first name, lowercase.
- Password: same as the username.
Change the password right after first login (Element > Settings > Account).
You will see two rooms:
#Family Chat: where the family talks. Photos, voice memos, life.#Server Room: where the server talks back. Status, alerts, install confirmations.#Memories: a place to store your memories, voice diaries, stories, funny moments.
The browser login is enough to verify the install. The point of famstack is that your family uses it from their pockets. Five minutes per device.
Element X is the modern Matrix client for a Whats-App like chat. Built by the same team that builds Synapse. Install it from the App Store or Play Store. Do not install plain "Element" (older client, still works but no longer recommended).
Sign-in flow:
-
Open Element X. Tap Sign in.
-
When asked for the homeserver, tap Edit or Other and enter:
http://<mac-lan-ip>:42031That is the Synapse port (
42031), not the Element web port (42030). Phones talk to Synapse directly. -
Element X warns the connection is not HTTPS. Correct. famstack runs unencrypted on your LAN by default, because trusting your own router is fine and Let's Encrypt does not issue certs for
192.168.x.x. Confirm and continue. (For HTTPS, set updomainmode and Caddy will issue certs. See stack-reference.md.) -
Username: first name, lowercase. Password: same, unless changed in the browser.
-
Set up encryption. First device on the account: Reset identity. Subsequent devices: Verify with the QR-code flow from an already-signed-in session.
-
Allow notifications when prompted. Without this, you do not get pings for new messages.
Verify: send a message from the phone. It should appear in the browser session within a second. Reverse it. If both directions work, you are done. If only one direction works, it is almost always a notification permission issue, not a Matrix issue.
Repeat for every family member. Each one logs in with their own first-name account.
Once you bring up the photos stacklet (next section), install Immich on every phone:
- Open the Immich mobile app.
- Server URL:
http://<mac-lan-ip>:42010. - Sign in with the family member's first name and password.
- Enable Background backup. Photos sync automatically from then on.
This is the part that replaces iCloud Photos for the family.
- "Cannot reach server": phone is on a different Wi-Fi network than the Mac. Guest Wi-Fi often isolates clients. Switch to the same network.
- "Server not Matrix":
messagesstacklet not healthy. Check./stack status, restart with./stack restart messages. - Notifications stop after a day: phone killed Element X in the background. iOS: Background App Refresh on for Element X. Android: exempt it from battery optimization.
- Mac changed IP: revisit Before you start. DHCP reservation on the router fixes this for good.
Each stacklet except core and messages is opt-in. Mix and match as you like. messages is technically optional, but without it the whole point of famstack (operating from your phone) goes away.
famstack also ships a lightweight bot runtime. Bots are tiny helpers that automate things in your family chat: filing documents, transcribing voice memos, managing the stack itself. Some are also experimenting with nanobot-based agent flows.
Matrix homeserver (Synapse) plus the Element web client. Your family's private chat, and the messaging backbone of famstack.
Why Matrix? Element X has native apps for every platform: iOS, Android, macOS, Windows, browser. Install once per device and forget about it. Matrix was built for federated public servers with full E2E encryption; we do not need any of that for a local setup, and the config options are complex. famstack hides that complexity. We did not find a better free messenger covering all platforms. (See ADR-004.)
./stack up messages # set up by the installer, usually already runningThe installer creates #famchat and #famstack and seeds an account for every family member. Default password is the first name in lowercase. Change it after first login.
| Element web port | 42030 |
| Synapse port (mobile clients) | 42031 |
| Data | ~/famstack-data/messages/synapse/, ~/famstack-data/messages/postgres/ |
Useful commands:
./stack messages users
./stack messages send <room> "<message>"
./stack messages room list
./stack messages setupImmich-based photo library, with native mobile apps and automatic background upload.
Why Immich? It is the most complete self-hosted photo solution: face recognition, maps, memories, albums, shared libraries. Native iOS and Android apps with background sync. Your family does not need to change their habits. They take photos, photos sync.
./stack up photosInstall the Immich app on every phone, point it at http://<mac-lan-ip>:42010, log in. Done. Every family member gets their own account and library.
| Port | 42010 |
| Data | ~/famstack-data/photos/library/, ~/famstack-data/photos/postgres/ |
Import media from a mounted drive, NAS share, or local folder. The importer tracks progress per source path, so imports can be paused, resumed, and interleaved across multiple sources.
./stack photos import --source /Volumes/files/Bilder/2020 # scan a folder
./stack photos import --status # show all sources + progress
./stack photos import --proceed 100 # upload next 100 files
./stack photos import --proceed 10 --dry-run # preview, no upload
./stack photos import --proceed 10 --verify # spot-check EXIF first
./stack photos import --proceed 500 --force # skip hash dedup (faster)
./stack photos import --album "Vacation 2023" # upload into a specific album
./stack photos import --proceed 500 --album-per-year # auto-bucket by EXIF yearDeduplication is two-layered: a local SHA-256 hash ledger skips files already imported in previous runs, and Immich's server-side check catches files uploaded via the mobile app or web UI. The hash step can bottleneck large imports — use --force to skip it when you know there are no duplicates (Immich still deduplicates server-side). Switching --source starts a new cursor; going back to a previous source picks up where you left off.
Auto-sort by year. For unsorted photo dumps, --album-per-year reads the EXIF date from each file, groups by year, and creates one album per year automatically. Falls back to filename patterns (20180415_...) when EXIF is missing. Files without a detectable year go into "Unknown Year".
WhatsApp images. Phone backups often contain hundreds of WhatsApp forwarded images (IMG-*-WA*) that pollute the timeline. After importing, archive them in one pass:
./stack photos album archive --pattern whatsappThis hides them from the timeline, detaches them from albums, and keeps the files in the library. Use unarchive to bring them back.
Bulk operations on albums and assets. Archive junk, move assets between albums, or clear an album — all from the command line.
./stack photos album archive --pattern whatsapp # archive WhatsApp images
./stack photos album archive --pattern '*.gif' # archive all GIFs
./stack photos album archive --from "Imported" # archive an entire album
./stack photos album archive --from "Imported" --pattern whatsapp # combine both
./stack photos album unarchive --pattern whatsapp # undo — restore to timelineArchive detaches assets from albums by default. Use --keep-albums to keep album memberships.
./stack photos album copy --from "Imported" --to "Family" # copy all assets
./stack photos album clear --from "Imported" # remove all from album
./stack photos album move --from "Imported" --to "2024" # copy + clear in one passCopy, clear, and move accept --pattern to filter by filename glob (e.g. --pattern '*.jpg'). The alias whatsapp expands to IMG-*-WA*. Album names are matched case-insensitively; --to creates the album if it doesn't exist.
All commands support --dry-run to preview and --api-key to use a specific user's key (defaults to the import key from secrets.toml).
Paperless-ngx archive with OCR and structured classification. Receipts, letters, contracts, tax documents. Search across everything by content.
Why Paperless? Gold standard for self-hosted document management. Mature project, large community. famstack makes the document inbox available on every phone through the chat: photograph a receipt, send it to the #documents room, and it gets filed.
./stack up docsThe archivist bot creates a #documents room in your chat. AI classifies and tags incoming documents when the ai stacklet is running. Without ai, documents are still stored and searchable by OCR text, just not auto-tagged. Type show 42 in #documents to read a document; type any term to search.
On first setup, famstack seeds Paperless with common document categories and types in your configured language. The LLM picks from these when classifying, so tags stay consistent. See stacklets/docs/taxonomy.toml.
Memory vault. With the code stacklet up, every filed document and every captured URL or pasted note also lands as a Markdown file in your Forgejo repo family/memory. Documents and correspondents live under the shared bucket (family/ by default, configurable via stack.toml [core] shared_bucket). Personal captures route to the sender's own bucket: Homer's pastes end up under homer/notes/ or homer/bookmarks/, Marge's under hers. Paperless stays the canonical store; the memory vault is the browsable, git-versioned human view. Install the memory stacklet to seed the ontology and render the vault as the family wiki.
| Port | 42020 |
| Data | ~/famstack-data/docs/paperless/, ~/famstack-data/docs/postgres/, ~/famstack-data/docs/consume/ |
Useful commands:
./stack docs show <id> # current state
./stack docs classify <id> # re-run AI tagging
./stack docs reformat <id> # re-run OCR to clean markdown
./stack docs reprocess <id> # full pipeline, respects bot.toml
./stack docs mirror <id> # publish to Forgejo mirror
./stack docs tags # list tags with document counts
./stack docs tags merge <from> <to> # merge duplicate tags
./stack docs tags prune --lang <de|en> # drop unused seeded tagsEvery write command accepts --dry (or --dry-run) to preview. reprocess is clean-slate: running it twice does not accumulate old tags.
Local AI engine. Powers document classification, voice transcription, and text-to-speech. The heavy one.
Why oMLX? Runs MLX-native models directly on Apple's Metal GPU with a smart SSD caching layer. Faster than Ollama on Apple Silicon, and serves models larger than your RAM by spilling to disk. (See ADR-009 and the benchmarks.)
./stack up aiFirst-run setup takes 10 to 20 minutes:
- Asks managed oMLX (default, recommended) or external OpenAI-compatible endpoint.
- Installs oMLX via Homebrew.
- Installs
cmakeandffmpegif missing. - Clones and builds whisper.cpp with Metal support (1 to 2 minutes of compilation).
- Downloads the Whisper large-v3-turbo model (1.5 GB).
- Downloads the LLM picked for your RAM tier (2.5 to 25 GB).
- Starts oMLX as a Homebrew service, Whisper as a launchd service.
- Brings up the Piper TTS container.
Why native instead of Docker? Metal GPU acceleration does not pass through Docker on macOS cleanly. Roughly 10x performance difference. Docker for orchestrated services, native for the GPU work.
Once up, voice messages in chat get transcribed automatically and the document classifier starts using the LLM.
| oMLX port | 42060 |
| Whisper port | 42062 |
| Data | ~/famstack-data/ai/ (Whisper, TTS), ~/.omlx/models/ (LLMs) |
Useful commands:
./stack ai models
./stack ai download <model-id>
./stack setup aiTo switch LLM models: edit [ai] default in stack.toml (alternatives are listed as commented lines), then ./stack setup ai.
Open WebUI: a ChatGPT-style interface for your local LLM.
Why Open WebUI? Most polished open source chat UI. Multiple models, conversation history, file uploads. Feels like ChatGPT but runs locally.
The tradeoff: another container using RAM. Skip on 16 GB Macs. AI still works for document classification and voice transcription without a chat UI.
./stack up chatai| Port | 42050 |
| Data | ~/famstack-data/chatai/ |
Forgejo: a community fork of Gitea. Lightweight private git server.
Why Forgejo? Simple, fast, low resource usage. Useful if you want the docs stacklet's "mirror to git" feature, or somewhere to version-track household text files.
./stack up code| HTTP port | 42040 |
| SSH clone port | 222 (macOS uses 22 for itself) |
| Data | ~/famstack-data/code/ |
The persistent layer of the household brain: a curated knowledge vault, rendered as a browsable family wiki. Needs the code stacklet.
Why a wiki? Filed documents are answers waiting for questions. The memory stacklet turns what the archivist files into a wiki of your family life: a home page, a page per family member, topic pages, correspondents. Everything lives as Markdown in the family/memory Forgejo repo, so every change is a git commit you can review or revert. Prefer Obsidian? Clone the repo and open it as a vault; the wiki is just a nicer pair of eyes on the same files.
./stack up memorySetup seeds the vault with three things: the classification ontology (the topics and document types the archivist files against), household facts, and a hand-curated correspondents layer with aliases, so "Springfield Insurance" and "Springfield Ins. Co." resolve to the same page instead of becoming duplicates.
The wiki maintains itself. A curator sidecar (stack-memory-curator) watches the vault: when new filings settle it regenerates the pages of the family members involved plus the home page (a couple of LLM calls, a few minutes after the burst), and once a night it rebuilds everything — topic pages, cross-references, the lot — while the GPU has nothing better to do. ./stack memory wiki stays available as the manual trigger, and [memory] in stack.toml holds the knobs (see Configuration). Edits happen in Forgejo (every wiki page links to its source), so the commit log doubles as the household's learning history.
| Wiki port | 42070 (domain mode: wiki.<your-domain>) |
| Data | ~/famstack-data/memory/vault/ (a checkout of Forgejo family/memory) |
Useful commands:
./stack memory search <term> # full-text query over the vault
./stack memory wiki # regenerate home, member, and topic pages with the LLM
./stack memory ontology # sync the vault ontology with the shipped seed
./stack memory lookup <text> # which topic or doctype does a term resolve to?
./stack memory correspondents # inspect the correspondent layer
./stack memory pull # fast-forward the local vault after Forgejo editsThe single config file. Generated by the installer. Yours to edit.
[core]
domain = "" # empty = port mode (recommended to start)
host = "" # empty = auto-detect LAN IP; "localhost" for one machine
data_dir = "~/famstack-data" # where databases, uploads, media live
timezone = "Europe/Berlin"
language = "de" # "de" or "en", used for document tags and UI
[updates]
schedule = "0 0 3 * * *" # Watchtower nightly image updates
[ai]
default = "mlx-community/Qwen3.5-9B-MLX-4bit" # change to match your RAM
language = "en" # "de" for German voice/transcription
[memory]
wiki_auto_rebuild = true # curator refreshes member pages after filings
wiki_rebuild_quiet_secs = 180 # how long the vault must stay quiet first
wiki_nightly = "03:30" # nightly full rebuild, local time ("" disables)Key things to know:
- domain: leave empty to start. Services are reachable via
<host>:<port>. Set a domain later for pretty URLs likephotos.home.internal(requires wildcard DNS on your router). - host: the hostname used in port-mode URLs and hints. Empty = auto-detect the LAN IP (right for households: phones need it). Set to
localhostfor a single-machine setup, or to a fixed name likemac-mini.localif the LAN IP keeps shifting. - data_dir: where all persistent data lives. Back this up. Outside the git repo.
- language: detected from your timezone. Controls which document categories get seeded (German or English). Change it and run
./stack restart docsto seed missing tags. - AI model: installer picks one for your RAM tier. Alternatives are listed as comments in
stack.toml. Switch by uncommenting a different line and running./stack setup ai.
Your family members. Generated by the installer. User accounts are seeded on the first ./stack up of each stacklet.
[[users]]
name = "Homer"
email = "homer@home.local"
role = "admin"
[[users]]
name = "Sarah"
email = "sarah@home.local"
role = "member"Admins get accounts on every stacklet. Members get accounts on the stacklets listed in their stacklets field.
Auto-generated passwords for service accounts. Created once, reused on every ./stack up. Treat like a password manager export.
./stack config # show resolved configuration
./stack config --secrets # include generated passwordsNeeds the
corestacklet running;docs+codeto file what arrives.
famstack can watch IMAP mailboxes and deliver new mail into chat rooms. The mail bot (part of core) fetches; the archivist files the message and its attachments. Configure mailboxes in stack.toml:
[mail]
poll_interval = 120 # seconds between checks
filter_noise = true # drop newsletters/automated mail (default true)
[[mail.accounts]]
name = "family" # used for the secret key and provenance tags
imap_host = "imap.example.org"
imap_port = 993 # 993 = IMAP over SSL (the default)
imap_user = "family@example.org"
ssl = true # default; set false only for a local/test server
folder = "INBOX"
room = "!roomid:yourserver" # where this mailbox delivers
since = "2026-01-01" # optional backfill floor (omit = whole folder)The password never goes in stack.toml or chat. Put it in .stack/secrets.toml, keyed by the account name uppercased:
mail__FAMILY_IMAP_PASSWORD = "<app-password>"Then ./stack restart core to apply.
Check the connection and find folder names:
./stack core mail # all accounts: log in, count INBOX, list folders
./stack core mail --account work # just oneThis logs in read-only and prints the server's real folder names (Gmail's [Gmail]/All Mail, a localized Gesendet, nested paths), which often differ from the webmail labels. Use it to confirm the folder value points where you expect, and to debug a wrong host/password before pointing the bot at the mailbox.
Key things to know:
- Invite both bots into the room.
@mail-botdelivers,@archivist-botfiles. Type the handle into Element's invite box; fresh bot accounts don't show up in directory search. - Where mail files = who is in the room. A room with two or more people files under the shared bucket; a private room or DM (one person) files under that person; a
Topic: ...room files under the topic. Route work mail and family mail to different rooms to keep them separate. - App passwords. Gmail, iCloud and most providers need an app-specific password with IMAP enabled, not your login password.
- Backfill with
since. It is a floor by the date the server received a message. Start narrow (last week) to check it works, then widen (the whole year); widening re-scans and already-filed mail is skipped. Omitsinceto pull the entire folder on first run. - Noise filtering.
filter_noise(default on) drops automated and marketing mail before it reaches the room: newsletters and mailing lists (aList-UnsubscribeorList-Idheader),Precedence: bulk, auto-replies (Auto-Submitted), and machine senders (noreply@,mailer-daemon@,bounce@). Detection is header-only, never body text, so personal mail that merely mentions "unsubscribe" is not dropped. Setfilter_noise = falseto ingest everything. - Read-only. The fetcher never marks mail read or deletes it. It remembers what it has handled by Message-ID and IMAP UID, so restarts don't re-file.
- Links are defanged. URLs in mail are filed as non-clickable plain text, so a phishing link can't be tapped by reflex.
./stack up <stacklet> # start (idempotent, regenerates .env)
./stack down <stacklet> # stop (data stays)
./stack restart <stacklet> # down + up
./stack destroy <stacklet> # stop + delete data (asks for confirmation)
./stack uninstall # destroy everything (use with care)./stack status # what's running, what's healthy
./stack list # all stacklets with enabled/disabled state
./stack logs <stacklet> # tail logs
./stack errors # recent error logs across all stacklets
./stack host # disk, memory, uptime
./stack updates # check for newer Docker images./stack ai models # available AI models in the backend
./stack config # show resolved configuration
./stack config --secrets # include generated passwords
./stack version # print versionAll commands output JSON when piped. Use --json to force it, --pretty to force human output.
This is the part everyone skips and regrets. famstack ships an opt-in backup stacklet for the irreplaceable file-level data (photo originals, scanned documents). It does not yet cover stacklet databases or your config files; for those, layer it with Time Machine or a periodic tar.
Run stack up backup. You need an APFS-formatted external drive plugged in. The setup wizard asks for the disk name, encryption (default: plain APFS), and a nightly time (default 02:00). It then installs a cron entry that runs stack backup sync at that hour, and walks you through granting Full Disk Access to /usr/sbin/cron in System Settings. The FDA grant is what lets the scheduled sync reach the archive disk; without it, cron jobs are sandbox-blocked from /Volumes/* on macOS Catalina and later. One drag-and-drop, then every cron job on this Mac inherits the access.
What gets backed up
| Source | Path on the archive disk |
|---|---|
| Immich photo originals | /Volumes/<disk>/data/photos-library/ |
| Paperless archived PDFs | /Volumes/<disk>/data/docs-media/ |
Postgres databases for both are not backed up yet. You get your files back but lose albums, tags, custom fields, and saved views. Pg-dump snapshots will ship as [[backup.snapshot]] in a later release.
How the protection works
Every file written to the archive gets the kernel uchg flag. macOS refuses to modify or delete uchg files, even with sudo. rsync --ignore-existing means files already in the archive are skipped on every run, so the backup is append-only by design and accidental rm -rf on your main system cannot propagate.
A canary file is a tripwire (named after the canary miners used to take underground to detect bad air). famstack plants a small file with known contents inside ~/famstack-data; before every sync the engine reads it and refuses to proceed if the contents have changed. If something has been encrypting or modifying files under the data directory, the canary will not match what was planted and the sync aborts before opening the archive, so the corrupted state cannot propagate.
Daily operation
stack backup sync # run a sync now (from any context)
stack backup status # last run, source counts, current mount state
stack down backup # remove the cron entry; canary, logs and archive contents stay
stack destroy backup # also remove BACKUP_DATA_DIR; archive disk and Keychain are preservedThe scheduled nightly run leaves the disk mounted between runs (cron cannot trigger eject under the macOS sandbox; files are kernel-locked regardless). Manual stack backup sync from Terminal does eject when it finishes. Results post to the #famstack Matrix room via stacker-bot.
Recovery (no special tooling needed)
Plug the archive disk into any Mac and browse the files in Finder. To copy locked files out:
sudo chflags -R nouchg /Volumes/<disk>/data/photos-library/<folder>/
cp -R /Volumes/<disk>/data/photos-library/<folder>/ ~/recovered/A stack backup restore command and on_restore hooks for database recovery are planned but not yet shipped.
What it protects you from
| Threat | Covered |
|---|---|
| Ransomware encrypts your Mac | Yes (uchg + canary) |
Accidental rm -rf on ~/famstack-data |
Yes (rsync never deletes from the archive) |
| You delete a photo on your phone | Yes (Immich propagates the delete to disk; the archive keeps the original) |
| Vault drive stolen from your house | Only if you opted in to APFS encryption |
| Vault drive hardware failure | No (single physical copy; offsite engine planned) |
| Fire or flood | No (archive is in the same building; offsite engine planned) |
Limitations to know about
Only APFS or HFS+ disks attached via USB or Thunderbolt. Network shares (SMB/NFS/Synology) are refused at probe time because the kernel cannot enforce uchg over a network filesystem. One target only; encrypted offsite via restic is planned, not shipped.
~/famstack-data holds every byte of user data. Even with the backup stacklet running you may want Time Machine or restic against this directory for full coverage (databases, AI model caches).
What is not in ~/famstack-data and needs separate handling:
stack.toml,users.toml,.stack/secrets.tomlin the repo. Small but irreplaceable. Gitignored on purpose, so they will not survive a freshgit clone.- oMLX models (in
~/.omlx/models). Re-downloadable.
A 30-second one-liner for the config:
tar czf famstack-config-$(date +%F).tgz stack.toml users.toml .stack/Stash the tarball on a USB stick or in 1Password.
Container images update themselves nightly at 3am via Watchtower. You do nothing for that.
For the famstack code itself:
git pull
./stack restart <stacklet> # for any stacklet whose code changedIf git pull shows changes to lib/stack/ or to a stacklet you are running, restart that stacklet. If in doubt, restart everything.
Pre-1.0 caveat: occasionally a release changes a config schema or env var. Release notes will say so. Read them before pulling.
./stack uninstallStops every stacklet and deletes its data. Asks for confirmation. There is no undo.
For a single stacklet:
./stack destroy photosoMLX is left alone, since it might be used by something other than famstack:
brew uninstall omlx
brew services stop omlxWhisper.cpp is built inside ~/famstack-data/ai/, so ./stack destroy ai removes it. The launchd plist is unloaded too.
Open OrbStack from Applications or the menu bar. Wait for the icon to turn green. Re-run ./stack.
If both Docker Desktop and OrbStack are installed, the installer prefers OrbStack and warns if it had to fall back. To switch fully, quit Docker Desktop and uncheck "start at login" in its preferences.
Find the offender:
lsof -nP -iTCP:42030 -sTCP:LISTENStop it, or change the port mapping in stacklets/<name>/docker-compose.yml.
Click Continue anyway. Element works in every modern browser. The warning is from an outdated check.
In port mode, make sure the Mac and phone are on the same network. Find the LAN IP:
ipconfig getifaddr en0 # Wi-Fi
ipconfig getifaddr en1 # EthernetUse http://<that-ip>:42031 for Element X, http://<that-ip>:42010 for Immich.
If the IP keeps moving, set a DHCP reservation on the router (see Before you start).
Almost always missing Xcode Command Line Tools. Run:
xcode-select --installThen ./stack up ai again. The installer is idempotent.
Your shell $PATH does not include Homebrew. On Apple Silicon, Homebrew lives at /opt/homebrew/bin/brew. Add to ~/.zshrc:
eval "$(/opt/homebrew/bin/brew shellenv)"Open a new terminal.
Edit [ai] default in stack.toml to a smaller model (alternatives are listed as commented lines), then:
./stack setup ai
./stack ai download <new-model-id>If 32 GB still swaps, close Slack, Chrome and anything else with 50 tabs. Local AI is a rude housemate.
./stack hostShows free space. If the photo library filled the disk, move data_dir to an external SSD: edit stack.toml, stop photos, move the directory, restart.
./stack uninstallRemoves everything. Only use this if you really mean it.
Run the diagnostic first. It logs in and lists the real folders:
./stack core mail- "connection failed" / "authentication failed". Use an app password (not your login password) with IMAP enabled at the provider, and check
imap_host/imap_port(993 for SSL). Thecredential:line reads(EMPTY …)when the secret-store key doesn't match the account name; it must bemail__<NAME>_IMAP_PASSWORD(account name uppercased) in.stack/secrets.toml. - Mail shows in the room but never gets filed. The archivist must be a member of that room. Invite
@archivist-botby typing the handle into Element's invite box (bot accounts don't show in directory search). - Edited
stack.tomlbut nothing changed.stack core mailand the bot read the rendered config, which only refreshes on./stack restart core. Restart after any[mail]change. - Wrong or empty folder. IMAP folder names differ from the webmail labels (Gmail's
[Gmail]/All Mail, a localizedGesendet).stack core maillists the exact names to copy intofolder. - A whole inbox of old mail flooded in. Set a
sincedate floor and start narrow (last week), then widen it. Withsinceunset, the first poll takes the whole folder. - Wanted mail is missing.
filter_noise(default on) drops newsletters and automated mail; setfilter_noise = falseto keep everything. - Threads render oddly on mobile. Element X needs beta features enabled (Settings, then Labs) for threads.
After ~50 days of uptime, new TCP connections start failing or the server feels sluggish despite low CPU. netstat shows thousands of TIME_WAIT connections that never expire:
netstat -an | grep TIME_WAIT | wc -lIf this returns thousands (10k+), you've hit a known XNU kernel bug: a 32-bit unsigned integer overflow in tcp_now freezes the internal TCP timestamp clock after 49 days 17 hours 2 minutes 47 seconds of continuous uptime. Once frozen, TIME_WAIT connections never expire.
Diagnose:
# Check uptime — the bug triggers after ~49.7 days
uptime
# Confirm TIME_WAIT pile-up
netstat -an | grep TIME_WAIT | wc -l
# Check boot time precisely
sysctl kern.boottimeFix: Reboot the Mac. No sysctl, no network restart, no OrbStack restart will clear it — only a full system reboot resets the kernel counter.
Prevent: Reboot the server once a month. A simple cron job as a reminder:
# Weekly uptime check — warns at 40 days so you can plan a reboot
(crontab -l 2>/dev/null; echo '0 9 * * 1 uptime_days=$(($(( $(date +\%s) - $(sysctl -n kern.boottime | grep -o "sec = [0-9]*" | grep -o "[0-9]*") )) / 86400)); [ $uptime_days -ge 40 ] && osascript -e "display notification \"Uptime: ${uptime_days} days — reboot soon (XNU TCP bug at 49d)\" with title \"famstack\""') | crontab -After reboot, all containers come back automatically (OrbStack starts at login and restarts containers with restart: unless-stopped).
./stack logs <stacklet>
./stack errorsOpen an issue with that output, or paste it into Discord. Better with ./stack status attached.
famstack is a small project run by a small group of people who care about it. The faster the community grows, the faster the project gets better. Stop by, say hi, bring your weird homelab questions:
- Discord: where the day-to-day conversation happens. Install help, "is this normal?" questions, what other families are doing with their stacks.
- GitHub: bug reports, feature requests, pull requests. A star is the single biggest signal we have to know whether to keep going.
- Bluesky and X: release notes, benchmarks, occasional opinion.
If famstack saved you from another year of manually copying photos off your phone with a cable, the kindest thing you can do is tell one other family about it. That is how this kind of project survives.
- User Guide: what your family does in chat. Send them this one.
- Stack Reference: how stacklets are wired together, lifecycle, hooks, secrets.
- Creating Stacklets: build your own stacklet.
- Documentation index
