Deployment Guide
Local development
Prerequisites
- Rust toolchain (stable)
- clang + libclang-dev (for RocksDB)
- Node.js (builds the dashboard, which the server embeds at compile time)
- OpenAI API key for the LLM steps (extraction, consolidation, dream); without one, start with
LOOMEM_ALLOW_KEYLESS=1
Build and run
git clone <repository-url>
cd loomem
# Build (dashboard first: the server embeds it at compile time)
cp entities.toml.example entities.toml
(cd loomem-dashboard && npm ci && npm run build)
cargo build --release -p loomem-server
# Local embedding model (required with the default embedding_provider = "local")
./scripts/fetch-embedding-model.sh
# Set environment
export OPENAI_API_KEY="sk-..." # or: export LOOMEM_ALLOW_KEYLESS=1
export LOOMEM_AUTH_TOKEN="your-secret-token"
# Run
./target/release/loomem-server
# Verify
curl http://localhost:3030/health
First memory
curl -X POST http://localhost:3030/v1/store \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"content": "Hello, this is my first memory"}'
Local embeddings (no OpenAI dependency)
Set embedding_provider = "local" in config.toml under [llm] — this is the default on a fresh install. Uses the tract ONNX runtime — pure Rust, no native dependencies, and nothing leaves the machine. The model is a separate download (./scripts/fetch-embedding-model.sh); without it the server refuses to start (refusing cloud fallback) instead of falling back to OpenAI.
Note: consolidation, extraction, and dream use OpenAI completions (gpt-4.1-mini). Without an OPENAI_API_KEY the default config refuses to start unless LOOMEM_ALLOW_KEYLESS=1 is set, and then those steps fail (memory_ingest returns a missing-key error); embeddings and search stay fully local either way.
Docker
Build
docker build -t loomem .
Run
docker run -d \
--name loomem \
-p 3030:3030 \
-v loomem-data:/data \
-e OPENAI_API_KEY="sk-..." \
-e LOOMEM_EMBEDDING_PROVIDER=openai \
-e LOOMEM_EMBEDDING_DIM=1536 \
-e LOOMEM_AUTH_TOKEN="your-secret-token" \
-e LOOMEM_AT_REST_MASTER_KEY="base64-encoded-32-bytes" \
loomem
The Dockerfile automatically:
- Binds to 0.0.0.0 (accessible from outside container)
- Sets data directory to /data (mount a volume here)
- Requires LOOMEM_AUTH_TOKEN and LOOMEM_AT_REST_MASTER_KEY at startup — the server refuses to start without them (deliberate opt-outs: LOOMEM_ALLOW_UNAUTH=1, LOOMEM_AT_REST_EXPECT_ENABLED=0)
- Enables per-stream rate limiting ([rate_limit]; over-limit requests get HTTP 429 with Retry-After)
- Ships without the local embedding model — set LOOMEM_EMBEDDING_PROVIDER=openai and LOOMEM_EMBEDDING_DIM=1536 (as above), or the server refuses to start. Stored text is then sent to the OpenAI embeddings API. A database already built with 384-dim local vectors needs loomem-server --reembed after the switch.
Docker Compose
version: '3.8'
services:
loomem:
build: .
ports:
- "3030:3030"
volumes:
- loomem-data:/data
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- LOOMEM_EMBEDDING_PROVIDER=openai
- LOOMEM_EMBEDDING_DIM=1536
- LOOMEM_AUTH_TOKEN=${LOOMEM_AUTH_TOKEN}
- LOOMEM_AT_REST_MASTER_KEY=${LOOMEM_AT_REST_MASTER_KEY}
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
loomem-data:
Railway
One-click deploy
- Fork the repository
- Connect it to Railway
- Create new project → Deploy from GitHub repo
- Add environment variables:
-
OPENAI_API_KEY— your OpenAI API key -LOOMEM_EMBEDDING_PROVIDER=openaiandLOOMEM_EMBEDDING_DIM=1536— the image has no local embedding model -LOOMEM_AUTH_TOKEN— admin token (generate a random string) -LOOMEM_AT_REST_MASTER_KEY— a base64-encoded 32-byte master key, required by the Docker image; keep a separate secure copy for recovery - Add a volume mounted at
/datafor persistent storage - Deploy
Configuration
If you deploy to Railway, add a railway.toml at the repo root (not shipped by default):
[build]
dockerfilePath = "Dockerfile"
[deploy]
healthcheckPath = "/health"
healthcheckTimeout = 30
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 3
The PORT environment variable is automatically set by Railway and overrides server.port in config.
Custom domain
After deployment, you can add a custom domain in Railway settings. Update SERVER_ORIGIN env var to match (required for OAuth).
Container entrypoint and startup migrations
The container image starts via scripts/docker-entrypoint.sh (works on any container platform). It supports an optional one-time data migration before the server starts:
| Env var | Purpose |
|---|---|
LOOMEM_MIGRATE_GRAPH_STREAMS_ON_START |
Set to 1 to run loomem-migrate --migrate-graph-entity-streams before server start (re-stamps graph entities/edges whose stream disagrees with the chunks they reference). Unset = skip. |
LOOMEM_MIGRATE_GRAPH_STREAMS_COMMIT |
Set to 1 to actually write changes. Default is a dry run. |
LOOMEM_MIGRATE_GRAPH_STREAMS_MANIFEST_DIR |
Where migration manifests are written. Defaults to /data/graph-migration-plans. |
The migration is idempotent (already-correct entities are skipped) and creates a backup checkpoint before the first write when committing. After a successful migration, unset the ON_START flag and redeploy so subsequent restarts take the no-op fast path.
Other loomem-migrate subcommands (run manually, not via the entrypoint): --validate-graph-entity-streams and --sample-embeddings.
Production checklist
Security
- [ ] Set strong
LOOMEM_AUTH_TOKEN(64+ random hex chars) — without it the server accepts every request (local passthrough mode) - [ ] Move API keys to environment variables (not in config.toml)
- [ ] Review PII settings — ensure
pii.enabled = true - [ ] Consider enabling at-rest encryption (
LOOMEM_AT_REST_MASTER_KEY) — see SECURITY.md - [ ] Set up HTTPS (managed platforms provide this; Docker needs a reverse proxy)
- [ ] Behind a reverse proxy, set
LOOMEM_TRUST_FORWARDED_FOR=1so the/oauth/registerrate limit keys on the client address your proxy appends toX-Forwarded-For, instead of lumping every client into the proxy's own bucket — leave it unset when Loomem is exposed directly (the header is caller-controlled there)
If the authentication token leaks, replace the env var value, restart the service, and verify that a request using the old token is rejected. Master-key rotation is not currently supported: changing the at-rest master key does not re-wrap existing data keys and makes encrypted data unreadable. See Security overview.
Performance
- [ ] Mount
/dataon fast storage (SSD) - [ ] Set
storage.rocksdb.write_buffer_sizebased on available RAM - [ ] Review
storage.tantivy.heap_size_mb— more RAM = faster indexing - [ ] Consider enabling
search.cachefor high-traffic deployments
Reliability
- [ ] Enable
storage.intent_log.enabled = truefor crash recovery - [ ] Set
storage.intent_log.sync_on_write = truefor durability - [ ] Configure backup:
worker.backup.enabled = true - [ ] Set appropriate
resource_guardsfor your environment - [ ] Monitor with
GET /v1/statusendpoint
Cost control
- [ ] Set
cost.daily_cap_usdto prevent runaway LLM costs - [ ] Set
cost.alert_threshold_usdfor early warnings - [ ] Review
dream.cost_cap_usd_per_runper dream session - [ ] Monitor daily costs via RocksDB cost column family
Connecting MCP clients
Claude.ai (web)
- Settings → Customize → Connectors
- Add new connector
- Enter URL:
https://your-loomem.app/mcp - Complete OAuth — enter API key when prompted
Claude Desktop
Edit claude_desktop_config.json:
{
"mcpServers": {
"loomem": {
"url": "https://your-loomem.app/mcp",
"headers": {
"Authorization": "Bearer loom_your_api_key"
}
}
}
}
Restart Claude Desktop.
The
urlform above is for a remote instance reachable over HTTPS. If Loomem runs locally over plain HTTP (http://127.0.0.1:<port>), the desktop app can't use a barehttp://localhostURL — bridge it to stdio with"command": "npx", "args": ["-y", "mcp-remote", "http://127.0.0.1:3030/mcp", "--allow-http"]. See the User Guide.
Claude Code (CLI / VS Code)
Add to .mcp.json in your project or global config:
{
"mcpServers": {
"loomem": {
"url": "https://your-loomem.app/mcp",
"headers": {
"Authorization": "Bearer loom_your_api_key"
}
}
}
}
Logging
Text logs (default)
RUST_LOG=debug ./target/release/loomem-server
JSON logs (for Grafana / ELK)
LOOMEM_LOG_FORMAT=json ./target/release/loomem-server
Backups
Automatic
Enable in config:
[worker.backup]
enabled = true
interval_secs = 43200 # every 12 hours
max_copies = 2 # keep last 2 backups
Checkpoints are written to {data_dir}/backups/checkpoint-<timestamp>/. For an out-of-band backup, copy that directory (or snapshot the volume) while the server is running — checkpoints are consistent point-in-time copies. See Backup and Restore for details, including the extra requirements for encrypted instances.
Restore
Before taking any restore actions, read and follow the full restore procedure, including the separate intent log, the master key for encrypted backups, index rebuilding, and verification.
Upgrading
- Pull latest code:
git pull - Build:
cargo build --release -p loomem-server - Stop old instance
- Start new binary
Loomem handles schema migrations automatically:
- RocksDB: backward-compatible (new fields get defaults)
- Tantivy: auto-rebuild on schema version mismatch
- Config: new keys are required — check config.toml after upgrade
CI
GitHub Actions runs on every push/PR to main:
- cargo check --workspace
- cargo test --workspace --lib
- cargo clippy -- -D warnings
- cargo fmt --check
Security notes
- Never hardcode API keys in
config.toml— useOPENAI_API_KEYenv var instead. Theapi_key_envfield specifies which env var to read. config_snapshot.tomlfiles (created by eval runs) are gitignored to prevent accidental secret leaks.- All dependencies use permissive licenses (MIT, Apache-2.0, BSD, ISC).