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 memoryStart 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/readyThe 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-codetenant 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:
| Mode | Settings | Notes |
|---|---|---|
| Built-in OAuth server | OAuth__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 provider | Auth__Authority, Auth__Audience, Auth__AdminAudience | Tokens from your OIDC provider; principals must be provisioned. Enables the control-plane API for service principals. |
| API keys only | Deployment__AirGapped=true | No 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
workerservice above (console host withRuntime__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.