diff --git a/docs/docs.json b/docs/docs.json
index 8a4c958654..9b4cbb318d 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -302,7 +302,8 @@
"group": "Migration",
"icon": "arrow-right",
"pages": [
- "migration/oss-v2-to-v3"
+ "migration/oss-v2-to-v3",
+ "migration/server-pgvector-upgrade"
]
},
{
diff --git a/docs/llms.txt b/docs/llms.txt
index 947d76f709..cd94cbf2d3 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -228,6 +228,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
- [OSS v2 to v3 Migration](https://docs.mem0.ai/migration/oss-v2-to-v3) [OSS]: Use when upgrading a self-hosted deployment across major versions.
- [Platform v2 to v3 Migration](https://docs.mem0.ai/migration/platform-v2-to-v3) [Platform]: Use when upgrading a Platform integration across major versions.
- [API Changes](https://docs.mem0.ai/migration/api-changes) [Both]: Use when the upgrade involves API surface changes.
+- [Server pgvector Image Upgrade](https://docs.mem0.ai/migration/server-pgvector-upgrade) [OSS]: Use when upgrading the self-hosted server Docker image from ankane/pgvector to pgvector/pgvector.
- [Changelog](https://docs.mem0.ai/changelog/highlights) [Both]: Use when the user asks what shipped recently.
## Open Source
diff --git a/docs/migration/server-pgvector-upgrade.mdx b/docs/migration/server-pgvector-upgrade.mdx
new file mode 100644
index 0000000000..629c67485f
--- /dev/null
+++ b/docs/migration/server-pgvector-upgrade.mdx
@@ -0,0 +1,158 @@
+---
+title: "Server: Upgrading the pgvector Docker Image"
+description: "Migrate your self-hosted Mem0 server from the archived ankane/pgvector image to the official pgvector/pgvector image."
+icon: "arrow-right"
+iconType: "solid"
+---
+
+## Overview
+
+The self-hosted Mem0 server has upgraded its PostgreSQL Docker image:
+
+| | Before | After |
+| --- | --- | --- |
+| Docker image | `ankane/pgvector:v0.5.1` | `pgvector/pgvector:pg17` |
+| PostgreSQL | 15 | 17 |
+| pgvector | 0.5.1 | 0.8.0 |
+| Credentials | Hardcoded `postgres` / `postgres` | Set via `POSTGRES_USER` / `POSTGRES_PASSWORD` env vars |
+
+
+The `ankane/pgvector` image is **archived and no longer maintained**. The new `pgvector/pgvector` image is the official distribution maintained by the pgvector project.
+
+
+
+**Should you migrate?**
+- You are running the Mem0 server via `docker-compose.yaml` in the `server/` directory.
+- You want to stay on a maintained, actively-patched PostgreSQL + pgvector image.
+- You want pgvector 0.8.0 features (improved HNSW performance, parallel index builds).
+
+
+## Fresh Installs
+
+No migration is needed. Copy the example env file, set your password, and start the stack:
+
+```bash
+cd server
+cp .env.example .env
+# Edit .env — set POSTGRES_PASSWORD (required) and OPENAI_API_KEY at minimum
+make up
+```
+
+## Migrating an Existing Install
+
+PostgreSQL 17 cannot read data files created by PostgreSQL 15 directly. You need to export your data from the old container and import it into the new one.
+
+### 1. Back Up Your Data
+
+With the **old** stack still running:
+
+```bash
+cd server
+docker compose exec -T postgres pg_dumpall -U postgres > mem0_backup.sql
+```
+
+Verify the dump is non-empty:
+
+```bash
+ls -lh mem0_backup.sql
+```
+
+
+Do not skip this step. The next step permanently deletes your Postgres data volume.
+
+
+### 2. Stop the Old Stack and Remove the Volume
+
+```bash
+docker compose down
+docker compose down -v
+```
+
+### 3. Update Your `.env`
+
+Postgres credentials are no longer hardcoded in `docker-compose.yaml`. Add them to your `.env`:
+
+```bash
+POSTGRES_HOST=postgres
+POSTGRES_PORT=5432
+POSTGRES_DB=postgres
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD= # required — compose will refuse to start without it
+POSTGRES_COLLECTION_NAME=memories
+```
+
+
+`POSTGRES_PASSWORD` is **required** — `docker compose up` will refuse to start without it. If you previously relied on the hardcoded default, set `POSTGRES_PASSWORD=postgres`.
+
+
+### 4. Start Only Postgres
+
+Start **only** the Postgres container first — do **not** start the mem0 API yet.
+The API runs `alembic upgrade head` on startup, which creates empty tables that
+would conflict with the restore.
+
+```bash
+docker compose up -d postgres
+```
+
+Wait for Postgres to become healthy:
+
+```bash
+docker compose exec -T postgres pg_isready -q && echo "ready" || echo "not ready"
+```
+
+### 5. Restore Your Data
+
+```bash
+docker compose exec -T postgres psql -U postgres < mem0_backup.sql
+```
+
+You may see notices like `role "postgres" already exists` — these are safe to ignore.
+
+
+You must restore **before** starting the mem0 API container. The API runs
+database migrations on startup which create empty tables — restoring after
+that would fail with duplicate-key errors and lose your API keys and settings.
+
+
+### 6. Start the API
+
+Now start the mem0 API container. Alembic will detect the existing tables and
+only apply any new migrations:
+
+```bash
+docker compose up -d mem0
+```
+
+### 7. Verify
+
+```bash
+# Check service health
+cd server && make health
+
+# Confirm memories are accessible
+curl -s http://localhost:8888/memories?user_id= \
+ -H "X-API-Key: "
+```
+
+## Rollback
+
+If something goes wrong, revert the image tag in `docker-compose.yaml`:
+
+```yaml
+postgres:
+ image: ankane/pgvector:v0.5.1
+```
+
+Then destroy the new volume, start the old image, and restore from your backup:
+
+```bash
+docker compose down -v
+docker compose up -d --build
+docker compose exec -T postgres psql -U postgres < mem0_backup.sql
+```
+
+## Need Help?
+
+- Join our [Discord community](https://mem0.ai/discord) for real-time support
+- Open an issue on [GitHub](https://github.com/mem0ai/mem0/issues)
diff --git a/server/.env.example b/server/.env.example
index 114db1239b..a338355115 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -4,12 +4,13 @@ OPENAI_API_KEY=
# ANTHROPIC_API_KEY=
# GOOGLE_API_KEY=
-POSTGRES_HOST=
-POSTGRES_PORT=
-POSTGRES_DB=
-POSTGRES_USER=
+# Postgres — POSTGRES_PASSWORD is required; docker-compose will not start without it.
+POSTGRES_HOST=postgres
+POSTGRES_PORT=5432
+POSTGRES_DB=postgres
+POSTGRES_USER=postgres
POSTGRES_PASSWORD=
-POSTGRES_COLLECTION_NAME=
+POSTGRES_COLLECTION_NAME=memories
ADMIN_API_KEY=
JWT_SECRET=
diff --git a/server/README.md b/server/README.md
index 37d1535569..941d2601ad 100644
--- a/server/README.md
+++ b/server/README.md
@@ -2,8 +2,24 @@
Mem0 ships a self-hosted FastAPI server plus a local dashboard. It is secure by default, supports dashboard login and API keys, and exposes OpenAPI docs at `/docs`.
+> **Upgrading?** The Postgres image changed from the archived `ankane/pgvector:v0.5.1`
+> to the official `pgvector/pgvector:pg17`, and `POSTGRES_PASSWORD` is now a required
+> env var. If you have an existing install, see
+> [Migrating from ankane/pgvector to pgvector/pgvector](#migrating-from-ankanepgvector-to-pgvectorpgvector)
+> before running `docker compose up`.
+
## Quick Start
+### Prerequisites
+
+Copy the example env file and set a Postgres password (required):
+
+```bash
+cd server
+cp .env.example .env
+# Edit .env — at minimum set POSTGRES_PASSWORD and OPENAI_API_KEY
+```
+
### Agent-first
Run one command; the terminal prints the admin email, password, and first API key.
@@ -121,6 +137,140 @@ The dashboard sets the following response headers on every path (see `server/das
Together these prevent iframe embedding, sniffing of mislabelled MIME types, and cross-origin referrer leaks. Harden further behind your own reverse proxy if needed.
+## Migrating from `ankane/pgvector` to `pgvector/pgvector`
+
+The `ankane/pgvector` Docker image is archived and no longer maintained. This release
+replaces it with the official `pgvector/pgvector:pg17` image (PostgreSQL 17, pgvector 0.8.0).
+
+**What changed:**
+
+| | Before | After |
+|---|---|---|
+| Docker image | `ankane/pgvector:v0.5.1` | `pgvector/pgvector:pg17` |
+| PostgreSQL version | 15 | 17 |
+| pgvector version | 0.5.1 | 0.8.0 |
+| Credentials | Hardcoded `postgres`/`postgres` | Driven by `POSTGRES_USER` / `POSTGRES_PASSWORD` env vars |
+
+### Fresh installs (no existing data)
+
+No migration needed. Copy `.env.example` to `.env`, set `POSTGRES_PASSWORD`, and run:
+
+```bash
+cd server
+make up
+```
+
+### Existing installs (preserving data)
+
+PostgreSQL 17 cannot read data files written by PostgreSQL 15 directly.
+You must export your data first, then import it into the new container.
+
+**1. Export your data from the old container**
+
+With the old stack still running:
+
+```bash
+cd server
+
+# Dump all databases (mem0 memories + mem0_app auth/config data)
+docker compose exec -T postgres pg_dumpall -U postgres > mem0_backup.sql
+```
+
+Verify the dump file is non-empty:
+
+```bash
+ls -lh mem0_backup.sql
+```
+
+**2. Stop the old stack and remove the old volume**
+
+```bash
+# Stop containers
+docker compose down
+
+# Remove the old Postgres data volume
+docker compose down -v
+```
+
+> **Warning:** `docker compose down -v` deletes the `postgres_db` volume permanently.
+> Only run this after you have verified your backup.
+
+**3. Update your `.env`**
+
+The Postgres credentials are no longer hardcoded in `docker-compose.yaml`.
+Add them to your `.env` file (or verify they match your old setup):
+
+```bash
+POSTGRES_HOST=postgres
+POSTGRES_PORT=5432
+POSTGRES_DB=postgres
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD= # required — compose will refuse to start without it
+POSTGRES_COLLECTION_NAME=memories
+```
+
+If you previously relied on the hardcoded defaults (`postgres`/`postgres`), set
+`POSTGRES_PASSWORD=postgres` to keep the same credentials.
+
+**4. Start only Postgres**
+
+Start **only** the Postgres container first — do not start the mem0 API yet.
+The API runs `alembic upgrade head` on startup, which creates empty tables that
+would conflict with the restore.
+
+```bash
+docker compose up -d postgres
+```
+
+Wait for the Postgres healthcheck to pass:
+
+```bash
+docker compose exec -T postgres pg_isready -q && echo "ready" || echo "not ready"
+```
+
+**5. Restore your data**
+
+```bash
+docker compose exec -T postgres psql -U postgres < mem0_backup.sql
+```
+
+You may see notices like `role "postgres" already exists` — these are harmless.
+
+> **Important:** You must restore before starting the mem0 API container. The API
+> runs database migrations on startup which create empty tables — restoring after
+> that would fail with duplicate-key errors and lose your API keys and settings.
+
+**6. Start the API**
+
+Now start the mem0 API container. Alembic will detect the existing tables and
+only apply any new migrations:
+
+```bash
+docker compose up -d mem0
+```
+
+**7. Verify**
+
+```bash
+# Check the API is healthy
+make health
+
+# Confirm your memories are present
+curl -s http://localhost:8888/memories?user_id= -H "X-API-Key: "
+```
+
+### Rollback
+
+If you need to revert, restore the old image tag in `docker-compose.yaml`:
+
+```yaml
+postgres:
+ image: ankane/pgvector:v0.5.1
+```
+
+Then `docker compose down -v`, `docker compose up -d --build`, and restore from
+`mem0_backup.sql` into the old container the same way.
+
## Reference
Additional product and API documentation lives at [docs.mem0.ai](https://docs.mem0.ai/open-source/overview).
diff --git a/server/auth.py b/server/auth.py
index 47a5cfb341..f1f9f8513b 100644
--- a/server/auth.py
+++ b/server/auth.py
@@ -3,16 +3,15 @@
import uuid
from datetime import datetime, timedelta, timezone
+from db import get_db
from fastapi import Depends, HTTPException, Request
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
+from models import APIKey, RefreshTokenJti, User
from passlib.context import CryptContext
from sqlalchemy import select, update
from sqlalchemy.orm import Session
-from db import get_db
-from models import APIKey, RefreshTokenJti, User
-
JWT_SECRET = os.environ.get("JWT_SECRET", "")
JWT_ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
@@ -184,3 +183,33 @@ async def require_auth(
return default_user
raise HTTPException(status_code=401, detail="Authentication required.")
return user
+
+
+_BOOTSTRAP_ADMIN = User(
+ id=uuid.UUID(int=0), name="admin_api_key", email="", password_hash="", role="admin", created_at=datetime.min.replace(tzinfo=timezone.utc),
+)
+
+
+async def require_admin(
+ request: Request,
+ user: User | None = Depends(verify_auth),
+ db: Session = Depends(get_db),
+) -> User:
+ """Like require_auth but also enforces admin role.
+
+ ADMIN_API_KEY and AUTH_DISABLED callers are treated as admin even when
+ the users table is empty (fresh-deploy bootstrap).
+ """
+ auth_type = getattr(request.state, "auth_type", "none")
+ if user is None:
+ if auth_type in {"admin_api_key", "disabled"}:
+ default_user = _get_default_user(db)
+ if default_user is not None:
+ if default_user.role != "admin":
+ raise HTTPException(status_code=403, detail="Admin role required.")
+ return default_user
+ return _BOOTSTRAP_ADMIN
+ raise HTTPException(status_code=401, detail="Authentication required.")
+ if user.role != "admin":
+ raise HTTPException(status_code=403, detail="Admin role required.")
+ return user
diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml
index 766a26d7f0..edb543d42f 100644
--- a/server/docker-compose.yaml
+++ b/server/docker-compose.yaml
@@ -30,16 +30,16 @@ services:
- MEM0_TELEMETRY=${MEM0_TELEMETRY:-true}
postgres:
- image: ankane/pgvector:v0.5.1
+ image: pgvector/pgvector:pg17
restart: on-failure
shm_size: "128mb"
networks:
- mem0_network
environment:
- - POSTGRES_USER=postgres
- - POSTGRES_PASSWORD=postgres
+ - POSTGRES_USER=${POSTGRES_USER:-postgres}
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
healthcheck:
- test: ["CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres"]
+ test: ["CMD-SHELL", "pg_isready -q -d postgres -U ${POSTGRES_USER:-postgres}"]
interval: 5s
timeout: 5s
retries: 5
diff --git a/server/main.py b/server/main.py
index 098712bf11..cdaf12e724 100644
--- a/server/main.py
+++ b/server/main.py
@@ -5,7 +5,7 @@
from typing import Any, Dict, List, Optional
import telemetry
-from auth import ADMIN_API_KEY, AUTH_DISABLED, JWT_SECRET, verify_auth
+from auth import ADMIN_API_KEY, AUTH_DISABLED, JWT_SECRET, require_admin, verify_auth
from db import SessionLocal
from dotenv import load_dotenv
from errors import (
@@ -316,8 +316,8 @@ def list_bundled_providers(_auth=Depends(verify_auth)):
@app.post("/configure", summary="Configure Mem0")
-def set_config(config: Dict[str, Any], _auth=Depends(verify_auth)):
- """Set memory configuration."""
+def set_config(config: Dict[str, Any], _auth=Depends(require_admin)):
+ """Set memory configuration. Requires admin role."""
_validate_bundled_providers(config)
update_config(config)
return {"message": "Configuration set successfully"}
@@ -391,19 +391,25 @@ def _list_all_memories(limit: int = ALL_MEMORIES_LIMIT) -> Dict[str, Any]:
@app.get("/memories", summary="Get memories")
def get_all_memories(
+ request: Request,
user_id: Optional[str] = None,
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
_auth=Depends(verify_auth),
):
- """Retrieve stored memories. Lists all memories when no identifier is provided."""
+ """Retrieve stored memories. Lists all memories when no identifier is provided (admin only)."""
try:
if not any([user_id, run_id, agent_id]):
+ auth_type = getattr(request.state, "auth_type", "none")
+ if _auth is not None and _auth.role != "admin" and auth_type not in {"admin_api_key", "disabled"}:
+ raise HTTPException(status_code=403, detail="Admin role required to list all memories.")
return _list_all_memories()
filters = {
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v is not None
}
return get_memory_instance().get_all(filters=filters)
+ except HTTPException:
+ raise
except Exception:
raise upstream_error()
@@ -479,9 +485,9 @@ def delete_all_memories(
user_id: Optional[str] = None,
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
- _auth=Depends(verify_auth),
+ _auth=Depends(require_admin),
):
- """Delete all memories for a given identifier."""
+ """Delete all memories for a given identifier. Requires admin role."""
if not any([user_id, run_id, agent_id]):
raise HTTPException(status_code=400, detail="At least one identifier is required.")
try:
@@ -495,8 +501,8 @@ def delete_all_memories(
@app.post("/reset", summary="Reset all memories")
-def reset_memory(_auth=Depends(verify_auth)):
- """Completely reset stored memories."""
+def reset_memory(_auth=Depends(require_admin)):
+ """Completely reset stored memories. Requires admin role."""
try:
get_memory_instance().reset()
return {"message": "All memories reset"}
diff --git a/server/routers/entities.py b/server/routers/entities.py
index 5a5a0a42b5..621b67d4e2 100644
--- a/server/routers/entities.py
+++ b/server/routers/entities.py
@@ -2,11 +2,10 @@
from datetime import datetime
from typing import Any, Literal, Optional
+from auth import require_admin, verify_auth
+from errors import upstream_error
from fastapi import APIRouter, Depends
from pydantic import BaseModel
-
-from auth import verify_auth
-from errors import upstream_error
from schemas import MessageResponse
from server_state import get_memory_instance
@@ -69,7 +68,7 @@ def list_entities(_auth=Depends(verify_auth)):
@router.delete("/{entity_type}/{entity_id}", response_model=MessageResponse)
-def delete_entity(entity_type: EntityType, entity_id: str, _auth=Depends(verify_auth)):
+def delete_entity(entity_type: EntityType, entity_id: str, _auth=Depends(require_admin)):
try:
get_memory_instance().delete_all(**{TYPE_TO_FIELD[entity_type]: entity_id})
except Exception:
diff --git a/server/routers/requests.py b/server/routers/requests.py
index b25ef7c4aa..3e226e30b0 100644
--- a/server/routers/requests.py
+++ b/server/routers/requests.py
@@ -1,15 +1,14 @@
-from datetime import datetime
import uuid
+from datetime import datetime
+from auth import require_admin
+from db import get_db
from fastapi import APIRouter, Depends, Query
+from models import RequestLog
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
-from auth import require_auth
-from db import get_db
-from models import RequestLog, User
-
router = APIRouter(prefix="/requests", tags=["requests"])
@@ -30,7 +29,7 @@ class RequestLogItem(BaseModel):
@router.get("", response_model=list[RequestLogItem])
def list_requests(
- user: User = Depends(require_auth),
+ _auth=Depends(require_admin),
db: Session = Depends(get_db),
limit: int = Query(default=50, ge=1, le=200),
):