diff --git a/AGENTS.md b/AGENTS.md
index 6f64946fc..9a5a55467 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -117,7 +117,7 @@ Hook scripts in `src/hooks/` are standalone Node.js scripts (no iii-sdk import).
## Current Stats (v0.9.28)
- 54 MCP tools (8 visible by default, `AGENTMEMORY_TOOLS=all` for all)
-- 129 REST endpoints
+- 130 REST endpoints
- 6 MCP resources, 3 MCP prompts
- 12 hooks, 15 skills
- 260+ iii functions
diff --git a/README.md b/README.md
index eb6bd1fa6..7e15144f1 100644
--- a/README.md
+++ b/README.md
@@ -1499,7 +1499,7 @@ Create `~/.agentmemory/.env`:

-129 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer ` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
+130 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer ` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
Key endpoints
diff --git a/deploy/docker-compose/.env.example b/deploy/docker-compose/.env.example
new file mode 100644
index 000000000..72219fbb4
--- /dev/null
+++ b/deploy/docker-compose/.env.example
@@ -0,0 +1,39 @@
+# ==========================================
+# AgentMemory Configuration
+# ==========================================
+
+# ------------------------------------------
+# 1. LLM Provider (Deepseek Flash V4 / openrouter)
+# ------------------------------------------
+# To use Deepseek, you can use OpenRouter or direct DeepSeek API (OpenAI compatible)
+# Uncomment one of the following approaches:
+
+# Approach A: OpenRouter (Recommended for Deepseek)
+OPENROUTER_API_KEY=your_openrouter_api_key_here
+OPENROUTER_MODEL=deepseek/deepseek-chat
+
+# Approach B: Direct DeepSeek API
+# OPENAI_API_KEY=your_deepseek_api_key_here
+# OPENAI_BASE_URL=https://api.deepseek.com
+# OPENAI_MODEL=deepseek-chat
+
+# ------------------------------------------
+# 2. Embeddings Configuration (CPU-only Local)
+# ------------------------------------------
+# Set embedding provider to local
+EMBEDDING_PROVIDER=local
+
+# Default model is Xenova/all-MiniLM-L6-v2. If you change it,
+# make sure you update the dimensions to match your model!
+# NOTE: The custom model support requires building from the latest modified source.
+LOCAL_EMBEDDING_MODEL=Xenova/all-MiniLM-L6-v2
+LOCAL_EMBEDDING_DIMENSIONS=384
+
+# ------------------------------------------
+# 3. AgentMemory Features
+# ------------------------------------------
+# Run LLM compression on every observation batch (Warning: uses LLM tokens)
+AGENTMEMORY_AUTO_COMPRESS=false
+
+# Inject recalled memories back into agent prompts
+AGENTMEMORY_INJECT_CONTEXT=false
diff --git a/deploy/docker-compose/Dockerfile b/deploy/docker-compose/Dockerfile
new file mode 100644
index 000000000..3e6b77c36
--- /dev/null
+++ b/deploy/docker-compose/Dockerfile
@@ -0,0 +1,39 @@
+# Build stage
+FROM node:22-slim AS builder
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY . .
+RUN npm run build
+RUN npm prune --production
+
+# Final stage
+FROM iiidev/iii:0.11.2 AS iii-image
+
+FROM node:22-slim
+WORKDIR /opt/agentmemory
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends openssl ca-certificates tini gosu curl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=iii-image /app/iii /usr/local/bin/iii
+COPY --from=builder /app/package*.json ./
+COPY --from=builder /app/node_modules ./node_modules
+COPY --from=builder /app/dist ./dist
+
+# Symlink so `agentmemory` works globally
+RUN ln -s /opt/agentmemory/dist/cli.mjs /usr/local/bin/agentmemory \
+ && chmod +x /opt/agentmemory/dist/cli.mjs
+
+ENV AGENTMEMORY_III_VERSION=0.11.2 \
+ TINI_SUBREAPER=1
+
+COPY deploy/docker-compose/entrypoint.sh /usr/local/bin/agentmemory-entrypoint.sh
+RUN chmod 0755 /usr/local/bin/agentmemory-entrypoint.sh
+
+EXPOSE 3111
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
+ CMD curl -fsS http://127.0.0.1:3111/agentmemory/livez || exit 1
+
+ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/agentmemory-entrypoint.sh"]
diff --git a/deploy/docker-compose/docker-compose.yml b/deploy/docker-compose/docker-compose.yml
new file mode 100644
index 000000000..dd6a0c6bd
--- /dev/null
+++ b/deploy/docker-compose/docker-compose.yml
@@ -0,0 +1,30 @@
+services:
+ agentmemory:
+ build:
+ context: ../..
+ dockerfile: deploy/docker-compose/Dockerfile
+ restart: unless-stopped
+ env_file:
+ - .env
+ expose:
+ - "3111"
+ ports:
+ - "127.0.0.1:3111:3111"
+ - "127.0.0.1:3112:3112"
+ - "127.0.0.1:3113:3113"
+ volumes:
+ - agentmemory-data:/data
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3111/agentmemory/livez || exit 1"]
+ interval: 30s
+ timeout: 5s
+ start_period: 30s
+ retries: 3
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "3"
+
+volumes:
+ agentmemory-data:
diff --git a/deploy/docker-compose/entrypoint.sh b/deploy/docker-compose/entrypoint.sh
new file mode 100644
index 000000000..3a3cef7bf
--- /dev/null
+++ b/deploy/docker-compose/entrypoint.sh
@@ -0,0 +1,97 @@
+#!/bin/sh
+# agentmemory first-boot entrypoint.
+#
+# Runs as root so it can:
+# 1. Overwrite the iii-config.yaml with a deploy-tuned version that
+# binds 0.0.0.0 and uses absolute /data paths.
+# 2. chown the platform-mounted /data volume to the runtime user
+# (managed platforms mount volumes root-owned 755 by default).
+# 3. Generate the HMAC secret on first boot and persist it to
+# /data/.hmac (chmod 600) so the secret survives restarts.
+#
+# Then it execs the agentmemory CLI under gosu as the unprivileged
+# `node` user.
+
+set -eu
+
+DATA_DIR="${AGENTMEMORY_DATA_DIR:-/data}"
+HMAC_FILE="${AGENTMEMORY_HMAC_FILE:-/data/.hmac}"
+RUN_AS="node:node"
+III_CONFIG="/opt/agentmemory/dist/iii-config.yaml"
+
+mkdir -p "$DATA_DIR"
+chown -R "$RUN_AS" "$DATA_DIR"
+
+cat > "$III_CONFIG" <<'EOF'
+workers:
+ - name: iii-http
+ config:
+ port: 3111
+ host: 0.0.0.0
+ default_timeout: 180000
+ cors:
+ allowed_origins:
+ - "http://localhost:3111"
+ - "http://localhost:3113"
+ - "http://127.0.0.1:3111"
+ - "http://127.0.0.1:3113"
+ allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]
+ - name: iii-state
+ config:
+ adapter:
+ name: kv
+ config:
+ store_method: file_based
+ file_path: /data/state_store.db
+ - name: iii-queue
+ config:
+ adapter:
+ name: builtin
+ - name: iii-pubsub
+ config:
+ adapter:
+ name: local
+ - name: iii-cron
+ config:
+ adapter:
+ name: kv
+ - name: iii-stream
+ config:
+ port: 3112
+ host: 0.0.0.0
+ adapter:
+ name: kv
+ config:
+ store_method: file_based
+ file_path: /data/stream_store
+ - name: iii-observability
+ config:
+ enabled: true
+ service_name: agentmemory
+ exporter: memory
+ sampling_ratio: 1.0
+ metrics_enabled: true
+ logs_enabled: true
+ logs_console_output: true
+EOF
+chown "$RUN_AS" "$III_CONFIG"
+
+if [ ! -s "$HMAC_FILE" ]; then
+ SECRET="$(openssl rand -hex 32)"
+ umask 077
+ printf '%s\n' "$SECRET" > "$HMAC_FILE"
+ chmod 600 "$HMAC_FILE"
+ chown "$RUN_AS" "$HMAC_FILE"
+ echo "================================================================"
+ echo "agentmemory: generated HMAC secret on first boot"
+ echo "AGENTMEMORY_SECRET=$SECRET"
+ echo "Copy this value now. It will not be printed again."
+ echo "Stored at: $HMAC_FILE (chmod 600)"
+ echo "To rotate: delete $HMAC_FILE on the persistent volume and restart."
+ echo "================================================================"
+fi
+
+AGENTMEMORY_SECRET="$(cat "$HMAC_FILE")"
+export AGENTMEMORY_SECRET
+
+exec gosu "$RUN_AS" agentmemory "$@"
diff --git a/src/index.ts b/src/index.ts
index 5f66d76c9..198a6dc3d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -540,7 +540,7 @@ async function main() {
`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`,
);
bootLog(
- `REST API: 129 endpoints at http://localhost:${config.restPort}/agentmemory/*`,
+ `REST API: 130 endpoints at http://localhost:${config.restPort}/agentmemory/*`,
);
bootLog(
`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`,
diff --git a/src/providers/embedding/local.ts b/src/providers/embedding/local.ts
index 7646815df..db5d4057f 100644
--- a/src/providers/embedding/local.ts
+++ b/src/providers/embedding/local.ts
@@ -1,4 +1,5 @@
import type { EmbeddingProvider } from "../../types.js";
+import { getEnvVar } from "../../config.js";
type FeatureExtractor = (
texts: string[],
@@ -7,8 +8,21 @@ type FeatureExtractor = (
export class LocalEmbeddingProvider implements EmbeddingProvider {
readonly name = "local";
- readonly dimensions = 384;
+ readonly dimensions: number;
private extractor: FeatureExtractor | null = null;
+ private model: string;
+
+ constructor() {
+ this.model = getEnvVar("LOCAL_EMBEDDING_MODEL") || "Xenova/all-MiniLM-L6-v2";
+ const dim = getEnvVar("LOCAL_EMBEDDING_DIMENSIONS");
+ const dimensions = dim ? Number(dim) : 384;
+ if (!Number.isSafeInteger(dimensions) || dimensions <= 0) {
+ throw new Error(
+ "LOCAL_EMBEDDING_DIMENSIONS must be a positive safe integer",
+ );
+ }
+ this.dimensions = dimensions;
+ }
async embed(text: string): Promise {
const [result] = await this.embedBatch([text]);
@@ -39,7 +53,7 @@ export class LocalEmbeddingProvider implements EmbeddingProvider {
}
this.extractor = (await transformers.pipeline(
"feature-extraction",
- "Xenova/all-MiniLM-L6-v2",
+ this.model,
{ dtype: "q8" },
)) as FeatureExtractor;
return this.extractor;