Agent Memory
Self-hosting

Deployment

Build the Agent Memory image, run it with PostgreSQL and Redis using Docker Compose, apply migrations and create the first tenant and API key.

This guide deploys a single-host stack with Docker Compose: PostgreSQL with pgvector, Redis, a one-off migration job, the API and a maintenance worker. The same pieces map directly onto any container platform.

Build the image

The image is built from the root of the source repository. It contains the API host, the console host and the web console:

docker build -t agent-memory:1.0 --build-arg GIT_SHA=$(git rev-parse --short HEAD) .

GIT_SHA is optional; it is reported by GET /api/version. The image listens on port 8080, runs as a non-root user and its default entrypoint is the API host (dotnet /app/api/AgentMemory.Api.dll). The console host is at /app/console/AgentMemory.dll.

Prepare secrets

Create an .env file next to your compose file — never commit it:

IMAGE=agent-memory:1.0

# PostgreSQL owner (runs migrations) and the two runtime roles the migration creates
PG_OWNER_USER=pgadmin
PG_OWNER_PASSWORD=change-me
APP_RUNTIME_PASSWORD=change-me-at-least-16-chars
CONTROL_PLANE_PASSWORD=change-me-at-least-16-chars

# Public URL of this deployment
PUBLIC_URL=https://memory.example.com

# Key-ring encryption certificate (required in Production), base64 PKCS#12 with an empty password
DATAPROTECTION_CERT=

# Built-in OAuth server (see "Choose an authentication mode")
OAUTH_SIGNING_CERT=
OAUTH_ENCRYPTION_CERT=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# Optional model providers
ANTHROPIC_API_KEY=

Generate a certificate for the key ring (and, separately, for OAuth signing and encryption) like this:

openssl req -x509 -newkey rsa:2048 -nodes -days 730 -subj "/CN=agent-memory-keyring" -keyout k.key -out k.crt
openssl pkcs12 -export -inkey k.key -in k.crt -passout pass: | base64 | tr -d '\n'

Write the compose file

services:
  postgres:
    image: pgvector/pgvector:pg17
    restart: unless-stopped
    command: ["postgres", "-c", "max_connections=200"]
    environment:
      POSTGRES_USER: ${PG_OWNER_USER}
      POSTGRES_PASSWORD: ${PG_OWNER_PASSWORD}
    volumes:
      - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${PG_OWNER_USER}"]
      interval: 5s
      retries: 20

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redisdata:/data

  migrate:
    image: ${IMAGE}
    entrypoint: ["dotnet", "/app/console/AgentMemory.dll", "--migrate"]
    restart: "no"
    depends_on:
      postgres: { condition: service_healthy }
    environment: &migrate-env
      DOTNET_ENVIRONMENT: Production
      ConnectionStrings__SharedDbMigrations: Host=postgres;Database=tenant_shared;Username=${PG_OWNER_USER};Password=${PG_OWNER_PASSWORD}
      ConnectionStrings__ControlPlaneDbMigrations: Host=postgres;Database=agentmemory;Username=${PG_OWNER_USER};Password=${PG_OWNER_PASSWORD}
      ConnectionStrings__PlatformDbMigrations: Host=postgres;Database=agentmemory_platform;Username=${PG_OWNER_USER};Password=${PG_OWNER_PASSWORD}
      ConnectionStrings__SharedDb: Host=postgres;Database=tenant_shared;Username=${PG_OWNER_USER};Password=${PG_OWNER_PASSWORD}
      ConnectionStrings__ControlPlaneDb: Host=postgres;Database=agentmemory;Username=${PG_OWNER_USER};Password=${PG_OWNER_PASSWORD}
      ConnectionStrings__PlatformDb: Host=postgres;Database=agentmemory_platform;Username=${PG_OWNER_USER};Password=${PG_OWNER_PASSWORD}
      ConnectionStrings__Redis: redis:6379,abortConnect=False
      APP_RUNTIME_PASSWORD: ${APP_RUNTIME_PASSWORD}
      CONTROL_PLANE_PASSWORD: ${CONTROL_PLANE_PASSWORD}

  api:
    image: ${IMAGE}
    restart: unless-stopped
    depends_on:
      migrate: { condition: service_completed_successfully }
      redis: { condition: service_started }
    ports:
      - "8080:8080"
    environment: &app-env
      ASPNETCORE_ENVIRONMENT: Production
      ConnectionStrings__SharedDb: Host=postgres;Database=tenant_shared;Username=app_runtime;Password=${APP_RUNTIME_PASSWORD}
      ConnectionStrings__PlatformDb: Host=postgres;Database=agentmemory_platform;Username=app_runtime;Password=${APP_RUNTIME_PASSWORD}
      ConnectionStrings__ControlPlaneDb: Host=postgres;Database=agentmemory;Username=control_plane;Password=${CONTROL_PLANE_PASSWORD}
      ConnectionStrings__Redis: redis:6379,abortConnect=False
      Database__ApplyMigrationsOnStartup: "false"   # the migrate job owns schema changes
      App__PublicBaseUrl: ${PUBLIC_URL}
      DataProtection__KeyEncryptionCertificate: ${DATAPROTECTION_CERT}
      OAuth__Enabled: "true"
      OAuth__Issuer: ${PUBLIC_URL}
      OAuth__SigningCertificate: ${OAUTH_SIGNING_CERT}
      OAuth__EncryptionCertificate: ${OAUTH_ENCRYPTION_CERT}
      Auth__Google__ClientId: ${GOOGLE_CLIENT_ID}
      Auth__Google__ClientSecret: ${GOOGLE_CLIENT_SECRET}
      RateLimit__PermitPerMinute: "120"
      Llm__Provider: anthropic
      Llm__Anthropic__ApiKey: ${ANTHROPIC_API_KEY}
      Deployment__NoPaidAi: "false"
      Embedding__Provider: hashing

  worker:
    image: ${IMAGE}
    restart: unless-stopped
    entrypoint: ["dotnet", "/app/console/AgentMemory.dll"]
    depends_on:
      migrate: { condition: service_completed_successfully }
    environment:
      <<: *app-env
      Runtime__Mode: service

volumes:
  pgdata:
  redisdata:

init-db.sql creates the three databases on first boot:

CREATE DATABASE agentmemory;          -- control plane
CREATE DATABASE tenant_shared;        -- shared tenant data
CREATE DATABASE agentmemory_platform; -- platform memory

Start the stack

docker compose --env-file .env up -d
docker compose logs -f migrate      # exits 0 when the schema is current
curl -s http://localhost:8080/health/ready

The migrate job creates the app_runtime role (which cannot bypass row-level security) and the control_plane role, installs pgvector, applies every migration and grants permissions. The API and worker start only after it succeeds.

Alternatively, the API can migrate itself: leave Database__ApplyMigrationsOnStartup at its default (true) and give the API the owner connection strings (ConnectionStrings__*Migrations) and both role passwords. It then applies pending migrations before serving, serialized with a database lock across replicas, and refuses to start if they fail.

Create the first tenant and key

Sign in to the console at <PUBLIC_URL>/app/ to create an organization, a project and keys through the UI.

Without an identity provider — or to script it — use the API host's one-shot modes. Each runs, prints its result and exits:

# A tenant, a workspace and one API key bound to the named agents (shown once)
docker compose run --rm --no-deps api --bootstrap-key --tenant-name acme --agents support-bot,claude-code
tenant    3f6c…
workspace 9a1e…
agents    support-bot,claude-code
key       amk_…
The key is shown once. Store it now.

The bootstrap key has the read, write, feedback, curate and delete capabilities, so erasure is possible with the only credential there is. It cannot act for end users; mint further keys in the console or with the provisioning API for that. Each run creates a new tenant.

For repeatable setups, --provision-tenant <spec.json> [--out <result.json>] creates a tenant with its workspaces, applications, keys and tenant admins from a JSON spec, idempotently:

{
  "name": "acme",
  "workspaces": [{ "name": "support" }],
  "applications": [{ "name": "web-chat", "workspace": "support" }],
  "keys": [{ "label": "support-bot", "workspace": "support", "agentIds": ["support-bot"], "canActForUsers": true }],
  "tenantAdmins": ["admin@acme.example"]
}

Put TLS in front

Terminate HTTPS in a reverse proxy or load balancer and forward to port 8080. MCP responses stream, so disable response buffering and allow long reads for /v1/mcp. Set App__PublicBaseUrl and OAuth__Issuer to the exact public URL clients use — MCP clients compare the advertised resource metadata with the URL they called — and list your proxy's network in ForwardedHeaders__TrustedNetworks__0.

Choose an authentication mode

In Production the API refuses to start unless one of these is configured:

ModeSettingsNotes
Built-in OAuth serverOAuth__Enabled=true, OAuth__Issuer, OAuth__SigningCertificate, OAuth__EncryptionCertificate, plus a sign-in source: Google (Auth__Google__ClientId/ClientSecret) or an OpenID Connect provider such as Entra ID (Auth__Oidc__Authority/ClientId/ClientSecret)Keyless MCP sign-in and console login. Without the two certificates the server uses ephemeral keys and every restart invalidates issued tokens.
External identity providerAuth__Authority, Auth__Audience, Auth__AdminAudienceTokens from your OIDC provider; principals must be provisioned. Enables the control-plane API for service principals.
API keys onlyDeployment__AirGapped=trueNo identity provider at all — see Air-gapped. Cloud LLM providers and other outbound integrations are refused in this mode.

API keys work in every mode.

Idle-run consolidation

Runs that agents forget to consolidate are consolidated by the maintenance loop after MemoryMaintenance:IdleTimeoutMinutes (30) of inactivity. Run it in one place:

  • the worker service above (console host with Runtime__Mode=service), or
  • inside the API, by setting MemoryMaintenance__Host=api (and dropping the worker).

Scaling out

The API is stateless; run several replicas behind the load balancer. Keep the database connection budget in mind — every replica holds a pool per database (Db__MaxPoolSize, default 30) — and raise PostgreSQL's max_connections accordingly. Use Api__MaxConcurrentRequests to shed load gracefully under pressure.

Next steps

On this page