513 lines
15 KiB
Markdown
513 lines
15 KiB
Markdown
# MemPalace on Unraid — server-mode deployment
|
||
|
||
This directory contains everything needed to run MemPalace as a shared
|
||
memory server on an Unraid box and connect multiple AI tools (Claude
|
||
Code, Codex, Antigravity, or any MCP-compatible client) to a single
|
||
persistent palace.
|
||
|
||
If you only use one machine, you don't need any of this — install
|
||
mempalace locally per the main [README](../../README.md) and you're
|
||
done. This guide is for users running the same AI tools across multiple
|
||
machines who want one shared memory.
|
||
|
||
---
|
||
|
||
## What you get
|
||
|
||
```
|
||
home LAN
|
||
┌───────────────────────────────────┐
|
||
│ Unraid (always on) │
|
||
│ ┌────────────────────────────┐ │
|
||
│ │ caddy :8443 (TLS + auth) │ │
|
||
│ │ ├─ /sse → mcp-proxy │ │
|
||
│ │ └─ /ingest → ingest API │ │
|
||
│ │ mempalace (single process) │ │
|
||
│ │ ├─ mcp-proxy :8765 │ │
|
||
│ │ └─ ingest :8766 │ │
|
||
│ └────────────────────────────┘ │
|
||
│ /mnt/user/appdata/mempalace/ │
|
||
│ ├─ palace/ ChromaDB │
|
||
│ ├─ kg/ knowledge graph │
|
||
│ └─ inbox/ uploaded sessions │
|
||
└───────────────────────────────────┘
|
||
│ │ │
|
||
┌─────┴─┐ ┌────┴──┐ ┌───┴──────┐
|
||
│ box A │ │ box B │ │ box C │
|
||
│ Claude│ │ Codex │ │ Antigrav │
|
||
└───────┘ └───────┘ └──────────┘
|
||
```
|
||
|
||
* **One palace, many clients.** Search and write target the same
|
||
ChromaDB index regardless of which machine you're on.
|
||
* **Auto-save hooks work across machines.** Each client's session
|
||
transcripts get pushed to the server on `Stop` and `PreCompact`
|
||
events; the server-side miner runs the existing `mine_convos`
|
||
pipeline (entity detection, room assignment, dedup, idempotency).
|
||
* **Single shared secret.** One bearer token gates both MCP and
|
||
transcript ingest at the Caddy edge.
|
||
|
||
What this is **not**: a multi-tenant cloud product. There's one palace,
|
||
one token, no per-user isolation. It's designed for a single user with
|
||
multiple machines.
|
||
|
||
---
|
||
|
||
## Files in this directory
|
||
|
||
| File | Purpose |
|
||
|---|---|
|
||
| `docker-compose.yml` | Two-container stack: `mempalace` + `caddy` sidecar. |
|
||
| `Caddyfile` | Caddy config: bearer-token auth, self-signed TLS, SSE-aware reverse proxy. |
|
||
| `mempalace-server.xml` | dockerMan template for a single-container, **no-auth, LAN-trust-only** install (compose path is the recommended one). |
|
||
| `README.md` | This file. |
|
||
|
||
The `Dockerfile` and `.dockerignore` live at the repo root — the compose
|
||
build context is `../..` so it can reach them.
|
||
|
||
---
|
||
|
||
## Prerequisites
|
||
|
||
* Unraid 6.12+ with Docker enabled (default).
|
||
* The **Compose Manager** plugin from Community Apps. Required for the
|
||
recommended (auth-enabled) path. The dockerMan template path doesn't
|
||
need it but has no auth.
|
||
* `/mnt/user/appdata` set up (default on every Unraid).
|
||
* Ports `8443` free on the Unraid host (or change in `docker-compose.yml`).
|
||
|
||
You do **not** need Tailscale, WireGuard, a domain name, a public IP,
|
||
SWAG, or NPM. The stack is self-contained.
|
||
|
||
---
|
||
|
||
## Install (recommended: compose with auth)
|
||
|
||
### 1. Get the repo onto Unraid
|
||
|
||
SSH to Unraid, pick a path on a regular share (not `/boot`, not
|
||
`/mnt/cache` directly), and clone or copy the repo:
|
||
|
||
```bash
|
||
mkdir -p /mnt/user/system/build
|
||
cd /mnt/user/system/build
|
||
git clone <your-fork-or-rsync-source> mempalace
|
||
cd mempalace/deploy/unraid
|
||
```
|
||
|
||
### 2. Mint a bearer token
|
||
|
||
```bash
|
||
TOKEN=$(openssl rand -hex 32)
|
||
echo "MEMPAL_TOKEN=$TOKEN" > .env
|
||
chmod 600 .env
|
||
echo "Token: $TOKEN" # save to a password manager — you'll set this on each client
|
||
```
|
||
|
||
`MEMPAL_TOKEN` is read from `.env` by `docker compose`. The same token
|
||
is forwarded to:
|
||
|
||
* Caddy, which checks `Authorization: Bearer <token>` on every request.
|
||
* The in-container ingest server as `MEMPALACE_INGEST_TOKEN` for
|
||
defense-in-depth.
|
||
|
||
### 3. Create the appdata directories
|
||
|
||
```bash
|
||
mkdir -p /mnt/user/appdata/mempalace \
|
||
/mnt/user/appdata/mempalace-caddy/data \
|
||
/mnt/user/appdata/mempalace-caddy/config
|
||
chown -R 99:100 /mnt/user/appdata/mempalace
|
||
chown -R 99:100 /mnt/user/appdata/mempalace-caddy
|
||
```
|
||
|
||
The Caddy data dir holds Caddy's auto-generated root CA — back it up
|
||
so re-deploys keep the same cert (clients won't have to re-trust it).
|
||
|
||
### 4. Build and start
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
First build downloads Python 3.13-slim and pip-installs `mempalace` +
|
||
`mcp-proxy` (~3–5 min on a Celeron, faster on real hardware).
|
||
|
||
### 5. Verify
|
||
|
||
```bash
|
||
# unauth'd liveness probe
|
||
curl -k https://<unraid-ip>:8443/healthz
|
||
# → {"status":"ok","version":"3.3.x"}
|
||
|
||
# bearer-checked endpoint should 401 without the token
|
||
curl -ki https://<unraid-ip>:8443/ingest/transcript
|
||
# HTTP/2 401
|
||
|
||
# ...and accept a request with it
|
||
curl -k -H "Authorization: Bearer $TOKEN" https://<unraid-ip>:8443/healthz
|
||
# → 200 OK
|
||
```
|
||
|
||
If you see all of the above, the server is up and the auth gate is
|
||
working.
|
||
|
||
### 6. (Optional) Trust Caddy's root CA on each client
|
||
|
||
Caddy's `tls internal` directive auto-generates a self-signed root CA
|
||
on first start. Clients must either trust that CA or skip TLS
|
||
verification (`-k` for curl, `MEMPAL_REMOTE_INSECURE=1` for hooks,
|
||
disabled SSL verify for the MCP client).
|
||
|
||
To trust it once and stop seeing TLS warnings:
|
||
|
||
```bash
|
||
# On Unraid:
|
||
cat /mnt/user/appdata/mempalace-caddy/data/caddy/pki/authorities/local/root.crt
|
||
```
|
||
|
||
Copy that PEM block to each Windows client and import into the
|
||
**Trusted Root Certification Authorities** store via `certmgr.msc`,
|
||
or via PowerShell:
|
||
|
||
```powershell
|
||
Import-Certificate -FilePath C:\path\to\root.crt -CertStoreLocation Cert:\LocalMachine\Root
|
||
```
|
||
|
||
---
|
||
|
||
## Connect AI tools
|
||
|
||
You'll need [`mcp-proxy`](https://github.com/sparfenyuk/mcp-proxy) on
|
||
each client machine:
|
||
|
||
```bash
|
||
uv tool install mcp-proxy
|
||
# or:
|
||
pip install mcp-proxy
|
||
```
|
||
|
||
Set environment variables persistently. **PowerShell** (Windows):
|
||
|
||
```powershell
|
||
[Environment]::SetEnvironmentVariable("MEMPAL_REMOTE_URL", "https://<unraid-ip>:8443", "User")
|
||
[Environment]::SetEnvironmentVariable("MEMPAL_REMOTE_TOKEN", "<the-token>", "User")
|
||
# Drop this once you've trusted Caddy's root CA:
|
||
[Environment]::SetEnvironmentVariable("MEMPAL_REMOTE_INSECURE", "1", "User")
|
||
```
|
||
|
||
**Bash/Zsh** (macOS/Linux): add the same three exports to
|
||
`~/.zshrc` / `~/.bashrc`.
|
||
|
||
### Claude Code
|
||
|
||
Add to `~/.claude.json` (user-scoped) or `.mcp.json` in the project:
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"mempalace": {
|
||
"command": "mcp-proxy",
|
||
"args": [
|
||
"https://<unraid-ip>:8443/sse",
|
||
"--headers", "Authorization", "Bearer <the-token>"
|
||
],
|
||
"env": {
|
||
"PYTHONHTTPSVERIFY": "0"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Drop the `env` block once Caddy's root CA is trusted on the client.
|
||
|
||
### Codex CLI
|
||
|
||
Add to `~/.codex/config.toml`:
|
||
|
||
```toml
|
||
[mcp_servers.mempalace]
|
||
command = "mcp-proxy"
|
||
args = [
|
||
"https://<unraid-ip>:8443/sse",
|
||
"--headers", "Authorization", "Bearer <the-token>",
|
||
]
|
||
|
||
[mcp_servers.mempalace.env]
|
||
PYTHONHTTPSVERIFY = "0"
|
||
```
|
||
|
||
### Antigravity
|
||
|
||
Antigravity uses the Windsurf-derived MCP layout. Open the IDE's
|
||
MCP settings UI (Settings → AI → MCP Servers) and add:
|
||
|
||
```json
|
||
{
|
||
"mempalace": {
|
||
"command": "mcp-proxy",
|
||
"args": [
|
||
"https://<unraid-ip>:8443/sse",
|
||
"--headers", "Authorization", "Bearer <the-token>"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
Or edit `~/.antigravity/mcp.json` directly with the same shape.
|
||
|
||
### Verify each client
|
||
|
||
In any of the three tools, start a session and call:
|
||
|
||
> "Use mempalace_status to show palace stats."
|
||
|
||
Expected: a JSON blob with `total_drawers`, wing/room breakdown, etc.
|
||
A 401 means the token is wrong; a connection error means the
|
||
URL/cert is wrong.
|
||
|
||
---
|
||
|
||
## Set up auto-save hooks
|
||
|
||
The `_remote.sh` hook variants in `../../hooks/` push transcripts to
|
||
the server instead of running `mempalace mine` locally. They share the
|
||
same env-var contract as the MCP client config above.
|
||
|
||
### Claude Code
|
||
|
||
Make the scripts executable:
|
||
|
||
```bash
|
||
chmod +x hooks/mempal_save_hook_remote.sh \
|
||
hooks/mempal_precompact_hook_remote.sh
|
||
```
|
||
|
||
Add to `.claude/settings.local.json`:
|
||
|
||
```json
|
||
{
|
||
"hooks": {
|
||
"Stop": [{
|
||
"matcher": "*",
|
||
"hooks": [{
|
||
"type": "command",
|
||
"command": "/abs/path/to/hooks/mempal_save_hook_remote.sh",
|
||
"timeout": 30
|
||
}]
|
||
}],
|
||
"PreCompact": [{
|
||
"hooks": [{
|
||
"type": "command",
|
||
"command": "/abs/path/to/hooks/mempal_precompact_hook_remote.sh",
|
||
"timeout": 60
|
||
}]
|
||
}]
|
||
}
|
||
}
|
||
```
|
||
|
||
### Codex CLI
|
||
|
||
Add to `.codex/hooks.json` with the same shape — the scripts are
|
||
hook-host-agnostic.
|
||
|
||
### What the hooks do
|
||
|
||
| Hook | Trigger | Behavior |
|
||
|---|---|---|
|
||
| `mempal_save_hook_remote.sh` | Every 15 user messages (configurable via `SAVE_INTERVAL` env var) | Backgrounded `curl` POSTs the active transcript to `/ingest/transcript`. Returns immediately so the AI doesn't stall. Idempotent — failed retries are safe. |
|
||
| `mempal_precompact_hook_remote.sh` | Right before context compaction | Synchronous `curl` POST. Blocks until the upload completes (or the hook timeout fires) so memory is durable before context shrinks. |
|
||
|
||
Both write logs to `~/.mempalace/hook_state/hook.log`. Tail it during
|
||
setup to confirm uploads are landing.
|
||
|
||
### Optional env vars
|
||
|
||
| Variable | Default | Purpose |
|
||
|---|---|---|
|
||
| `MEMPAL_REMOTE_URL` | *(required)* | Server base URL, e.g. `https://unraid.local:8443`. |
|
||
| `MEMPAL_REMOTE_TOKEN` | *(required)* | Bearer token. |
|
||
| `MEMPAL_REMOTE_INSECURE` | unset | Set to `1` to skip TLS verification. Use only with `tls internal`. |
|
||
| `MEMPAL_REMOTE_WING` | unset | Force a specific wing for this client's transcripts. Default: server derives wing from session id. |
|
||
| `SAVE_INTERVAL` | `15` | Messages between save-hook fires. |
|
||
|
||
---
|
||
|
||
## Backfilling history
|
||
|
||
The hooks only capture sessions going forward. To mine **past**
|
||
transcripts into the remote palace, on each client run:
|
||
|
||
```bash
|
||
curl -k -X POST \
|
||
-H "Authorization: Bearer $MEMPAL_REMOTE_TOKEN" \
|
||
-H "X-Session-Id: backfill-$(hostname)-$(date +%s)" \
|
||
-H "X-Wing: backfill" \
|
||
--data-binary @/path/to/some-session.jsonl \
|
||
"$MEMPAL_REMOTE_URL/ingest/transcript"
|
||
```
|
||
|
||
For a whole directory of past sessions, loop:
|
||
|
||
```bash
|
||
for f in ~/.claude/projects/**/*.jsonl; do
|
||
curl -k -X POST \
|
||
-H "Authorization: Bearer $MEMPAL_REMOTE_TOKEN" \
|
||
-H "X-Session-Id: $(basename "$f" .jsonl)" \
|
||
--data-binary @"$f" \
|
||
"$MEMPAL_REMOTE_URL/ingest/transcript"
|
||
done
|
||
```
|
||
|
||
The server-side miner is idempotent — re-uploading the same transcript
|
||
won't double-file.
|
||
|
||
---
|
||
|
||
## Backups
|
||
|
||
Everything that matters lives in `/mnt/user/appdata/mempalace/`:
|
||
|
||
* `palace/` — ChromaDB vector index + SQLite metadata
|
||
* `kg/` — knowledge-graph SQLite
|
||
* `inbox/` — uploaded transcripts (kept for re-mining if needed)
|
||
|
||
Add it to your **CA Backup / Appdata Backup** schedule. Losing this
|
||
directory loses all memory.
|
||
|
||
The Caddy data dir (`/mnt/user/appdata/mempalace-caddy/data/`) is also
|
||
worth backing up — it contains the auto-generated root CA. Without it,
|
||
re-deploys regenerate the CA and clients have to re-trust it.
|
||
|
||
---
|
||
|
||
## dockerMan template (no-auth, LAN-trust-only)
|
||
|
||
If you don't want auth and trust your LAN absolutely (no other people,
|
||
no untrusted IoT, no guests), the `mempalace-server.xml` template gives
|
||
you a single-container, dockerMan-compatible install:
|
||
|
||
```bash
|
||
# Build the image:
|
||
cd /mnt/user/system/build/mempalace
|
||
docker build -t mempalace-server:latest .
|
||
|
||
# Install the template:
|
||
cp deploy/unraid/mempalace-server.xml \
|
||
/boot/config/plugins/dockerMan/templates-user/my-MemPalace.xml
|
||
```
|
||
|
||
Then in the Unraid WebUI: Docker → Add Container → "Select a template" →
|
||
**MemPalace** → Apply.
|
||
|
||
This path skips Caddy entirely. The MCP SSE endpoint is published bare
|
||
on `:8765`, no TLS, no auth. Anyone on the LAN can read and write the
|
||
palace. **Only use this if you understand and accept that.**
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
### `mcp-proxy` connects but tool calls hang
|
||
|
||
Caddy is buffering SSE responses. Verify `flush_interval -1` is set in
|
||
the Caddyfile and that Caddy version is 2.7+ (the compose pulls
|
||
`caddy:2-alpine` which is current).
|
||
|
||
### 401 from every request
|
||
|
||
The token in the client's MCP config doesn't match the server's
|
||
`MEMPAL_TOKEN`. Print both to confirm:
|
||
|
||
```bash
|
||
# On Unraid:
|
||
grep MEMPAL_TOKEN /mnt/user/system/build/mempalace/deploy/unraid/.env
|
||
|
||
# On client (PowerShell):
|
||
[Environment]::GetEnvironmentVariable("MEMPAL_REMOTE_TOKEN", "User")
|
||
```
|
||
|
||
### `MineAlreadyRunning` errors in hook logs
|
||
|
||
Two clients hit the ingest endpoint simultaneously. The server-side
|
||
miner serializes via `mine_lock` and rejects the second one. The hook
|
||
is idempotent — the next save catches up. If you see this constantly,
|
||
raise `SAVE_INTERVAL` on the chattier client.
|
||
|
||
### Caddy logs `tls: handshake failure`
|
||
|
||
Client doesn't trust the self-signed cert. Either trust the root CA
|
||
(see step 6 in install) or set `MEMPAL_REMOTE_INSECURE=1` /
|
||
`PYTHONHTTPSVERIFY=0` on that client.
|
||
|
||
### Container can't start: "address already in use"
|
||
|
||
Port 8443 is taken (commonly by Unraid's WebUI HTTPS or another
|
||
service). Edit `docker-compose.yml` and change the host-side mapping:
|
||
|
||
```yaml
|
||
ports:
|
||
- "9443:8443" # change 9443 to whatever's free
|
||
```
|
||
|
||
Update `MEMPAL_REMOTE_URL` on every client to match.
|
||
|
||
### Embedding model download stalls on first request
|
||
|
||
The ~80 MB MiniLM ONNX model downloads from HuggingFace on first
|
||
use. Slow connections can time out the initial mining call. Pre-warm
|
||
it manually:
|
||
|
||
```bash
|
||
docker exec mempalace python -c \
|
||
"from chromadb.utils.embedding_functions import ONNXMiniLM_L6_V2; ONNXMiniLM_L6_V2()(['warmup'])"
|
||
```
|
||
|
||
Subsequent uses load from `/data/.cache/chroma/` — ~50 ms.
|
||
|
||
### Logs
|
||
|
||
```bash
|
||
docker logs mempalace # MCP server, ingest server
|
||
docker logs mempalace-caddy # auth gate, TLS, access logs
|
||
tail -f ~/.mempalace/hook_state/hook.log # client-side hook activity
|
||
```
|
||
|
||
---
|
||
|
||
## Updating
|
||
|
||
When this repo updates upstream:
|
||
|
||
```bash
|
||
cd /mnt/user/system/build/mempalace
|
||
git pull
|
||
cd deploy/unraid
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Compose only rebuilds the `mempalace` service (the image hash
|
||
changes); Caddy is pinned to `caddy:2-alpine` and pulls latest within
|
||
the 2.x line.
|
||
|
||
Your palace data and Caddy CA persist across rebuilds because they're
|
||
on volumes outside the container.
|
||
|
||
---
|
||
|
||
## Going further
|
||
|
||
* **Replace self-signed TLS with Let's Encrypt** — point a real domain at
|
||
Unraid (DDNS or otherwise), open port 80 for ACME challenge, and
|
||
change `tls internal` in `Caddyfile` to `tls your@email`. Caddy
|
||
handles the rest.
|
||
* **Put behind SWAG / Nginx Proxy Manager** — drop the Caddy sidecar,
|
||
keep `mempalace` exposing 8765/8766 internally only, and add the
|
||
routes to your existing reverse proxy. Bearer-token auth and SSE
|
||
pass-through must be configured manually.
|
||
* **Per-machine wings** — set `MEMPAL_REMOTE_WING=<machinename>` on
|
||
each client so transcripts file under separate wings; cross-wing
|
||
search still works via the palace graph.
|