edit(content): review batch-1 editorial pass across 24 posts (EN + ZH)

- voice: first-person throughout (we/our -> I/my), matching the post guideline
- privacy: internal LAN IPs replaced with placeholder hostnames
  (n8n.internal, gpu-node.internal); one compose volume path generalised
- ZH twins: Traditional -> Simplified characters (site serves zh-Hans),
  whole-file renormalisation where the twin was still Traditional
- dynamicCta: true on 12 posts so the contextual dual-track CTA renders
- long configs wrapped in <details class="tech-deep-dive"> with a summary
  naming the artefact and line count
- self-hosted-tts: token refresh now writes .tmp + atomic mv, with a note on
  the 401 race the old truncating redirect created
This commit is contained in:
hoelee committed 2026-10-27 03:29:03 +08:00
1 parent 86edd40c74
commit 68771101ea
50 files changed
+478 -375

No files matched your search

@@ -3,6 +3,8 @@ title: "How I Self-Hosted a Mem0 Memory Stack (So My AI Agent Remembers)"
description: "A private, self-hosted memory layer for AI agents — mem0 API + LiteLLM + PostgreSQL with pgvector on a Synology NAS. Semantic memory under your control, not a third party's."
pubDate: 2026-09-16
category: case-studies
blueprint: ai-agent-automation
blueprintLayer: L2-llm-gateway-agents
tags: [mem0, docker, postgres, pgvector, litellm, ai, sharing]
ogImage: /og/self-hosting-mem0-memory-stack.jpg
banner: /banners/self-hosting-mem0-memory-stack.jpg
@@ -43,7 +45,7 @@ the three things memory needs:
1. **Add a memory** — `POST /memories` with a fact or a conversation; it
extracts, dedupes, and stores it (optionally classified by an LLM).
2. **Search** — `GET /search` with a query returns the most relevant stored
2. **Search** — `POST /search` with a JSON payload returns the most relevant stored
facts, ranked by semantic similarity.
3. **User scoping** — memories belong to a `user_id`, so you can keep memory
per customer, per project, or per conversation without cross-talk.
@@ -77,6 +79,9 @@ Three containers on my Synology DS1821+, as a Portainer compose stack:
A representative compose looks like this:
<details>
<summary>📄 docker-compose.yml (30 lines) — Mem0 + LiteLLM + pgvector Production Stack</summary>
```yaml
services:
mem0-postgres:
@@ -102,12 +107,14 @@ services:
OPENAI_API_KEY: ${OPENAI_API_KEY}
OPENAI_API_BASE: http://mem0-litellm:4000/v1 # go through the gateway
MEM0_API_KEY: ${MEM0_API_KEY} # X-Api-Key for /search & /memories
POSTGRES_URL: postgresql://mem0:${MEM0_DB_PASSWORD}@mem0-postgres/mem0
POSTGRES_URL: postgresql://mem0:***@mem0-postgres/mem0
ports:
- "20015:8000"
restart: unless-stopped
```
</details>
(Mine is built from the mem0 repo with the model/fact-extraction directed
through LiteLLM; the exact image tag is me maintaining a local build, and I
keep the real API key out of the compose file — it's an env secret.)
@@ -117,9 +124,9 @@ keep the real API key out of the compose file — it's an env secret.)
Once it's up, the API is refreshingly small. Add a fact (fast, verbatim):
```bash
curl -X POST http://192.168.1.1:20015/memories \
curl -X POST http://nas-node.internal:20015/memories \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MEM0_API_KEY" \
-H "X-Api-Key: ***" \
-d '{
"user_id": "customer-2211",
"text": "Customer 2211 prefers email over WhatsApp for order updates."
@@ -129,9 +136,9 @@ curl -X POST http://192.168.1.1:20015/memories \
Retrieve the most relevant memories later:
```bash
curl -X POST http://192.168.1.1:20015/memories/search \
curl -X POST http://nas-node.internal:20015/search \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MEM0_API_KEY" \
-H "X-Api-Key: ***" \
-d '{
"user_id": "customer-2211",
"query": "how should I reach this customer?",