15 KiB
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 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
StopandPreCompactevents; the server-side miner runs the existingmine_convospipeline (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/appdataset up (default on every Unraid).- Ports
8443free on the Unraid host (or change indocker-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:
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
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_TOKENfor defense-in-depth.
3. Create the appdata directories
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
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
# 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:
# 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:
Import-Certificate -FilePath C:\path\to\root.crt -CertStoreLocation Cert:\LocalMachine\Root
Connect AI tools
You'll need mcp-proxy on
each client machine:
uv tool install mcp-proxy
# or:
pip install mcp-proxy
Set environment variables persistently. PowerShell (Windows):
[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:
{
"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:
[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:
{
"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:
chmod +x hooks/mempal_save_hook_remote.sh \
hooks/mempal_precompact_hook_remote.sh
Add to .claude/settings.local.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:
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:
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 metadatakg/— knowledge-graph SQLiteinbox/— 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:
# 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:
# 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:
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:
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
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:
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 internalinCaddyfiletotls your@email. Caddy handles the rest. - Put behind SWAG / Nginx Proxy Manager — drop the Caddy sidecar,
keep
mempalaceexposing 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.