Deploy to production
The production image is a multi-stage, non-root Alpine build. The three supported deployment paths are:
| Path | File | Use it for |
|---|---|---|
| Docker Compose | docker-compose.prod.yml |
Single-host / VPS |
| Kubernetes | docs/k8s/ (coming soon) |
Cluster deployments |
| Manual | apps/mcp-server/Dockerfile |
Custom infra |
To run one persistent server that all AI agents share as primary memory, see the agent memory server runbook.
Prerequisites
Section titled “Prerequisites”- Docker 25+ with BuildKit (
DOCKER_BUILDKIT=1) - Docker Compose v2.24+
- A
.env.prodfile based on.env.example
Building the image
Section titled “Building the image”# From the repository rootdocker build \ --file apps/mcp-server/Dockerfile \ --tag engram-mcp-server:latest \ .The multi-stage build:
- deps — installs all workspace deps from the locked lockfile.
- builder — compiles TypeScript (
nest build) and runspnpm deployto produce a flat production bundle at/prod. - production — copies only the pruned bundle; runs as a non-root
engramuser.
Image properties
Section titled “Image properties”| Property | Value |
|---|---|
| Base | node:22-alpine |
| User | engram (non-root UID 1000) |
| Port | 3000 |
| Default profile | standard |
| Entrypoint | node dist/main.js |
Single-host deployment (Docker Compose)
Section titled “Single-host deployment (Docker Compose)”1. Create the env file
Section titled “1. Create the env file”cp .env.example .env.prod# Edit .env.prod and set at minimum:# POSTGRES_PASSWORD, MCP_ADMIN_TOKENEmbeddings default to local Ollama (EMBEDDING_PROVIDER=ollama) — no API key
needed. To opt in to OpenAI instead, set EMBEDDING_PROVIDER=openai and
OPENAI_API_KEY in .env.prod.
2. Run database migrations
Section titled “2. Run database migrations”docker compose -f docker-compose.prod.yml run --rm mcp-server \ sh -c 'node_modules/.bin/prisma migrate deploy'3. Start all services
Section titled “3. Start all services”docker compose -f docker-compose.prod.yml --env-file .env.prod up -d3a. Embeddings (Ollama)
Section titled “3a. Embeddings (Ollama)”The prod compose file defaults EMBEDDING_PROVIDER to ollama and
OLLAMA_URL to http://ollama:11434 — a bundled Ollama service behind the
opt-in ollama compose profile (with an ollama_data volume for pulled
models). Start it with the profile and pull the default model once:
docker compose -f docker-compose.prod.yml --env-file .env.prod --profile ollama up -ddocker compose -f docker-compose.prod.yml exec ollama ollama pull nomic-embed-textSkip the profile if Ollama already runs on the host (point OLLAMA_URL at
the host’s endpoint) or if you opted in to OpenAI. If Ollama is unreachable
or the model is not pulled, writes still succeed — memories are stored
without vectors and picked up by a later reindex. For GPU inference inside
the container, add the standard Docker GPU configuration (e.g. the NVIDIA
container toolkit and a gpus reservation); host installs use the GPU
automatically.
4. Verify health
Section titled “4. Verify health”curl http://localhost:3000/healthcurl http://localhost:3000/health/readycurl http://localhost:3000/health/metricsProfile selection
Section titled “Profile selection”Set DEPLOYMENT_PROFILE in .env.prod. Both profiles run on the same
PostgreSQL backing store; the difference is the auth stack:
| Value | Backing store | Auth stack |
|---|---|---|
lite |
PostgreSQL + pgvector | Not wired (single-user) |
standard |
PostgreSQL + pgvector | Multi-tenant: auth, per-agent keys, rate limits (default) |
Note: PostgreSQL is the only backing service
docker-compose.prod.ymlprovisions. Choosingstandardoverliteneeds no extra services — see Migrate lite to standard for the auth configurationstandardrequires.
Observability
Section titled “Observability”Prometheus metrics
Section titled “Prometheus metrics”GET /health/metrics returns Prometheus text format. Scrape it with a
Prometheus job:
scrape_configs: - job_name: engram static_configs: - targets: ['engram-mcp-server:3000'] metrics_path: /health/metricsThe full metric catalogue lives in the observability reference.
OpenTelemetry tracing
Section titled “OpenTelemetry tracing”Set OTEL_EXPORTER_OTLP_ENDPOINT to enable distributed tracing. When
unset the SDK is never loaded and there is zero overhead:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318OTEL_SERVICE_NAME=engram-mcp-serverHTTP and Express spans are emitted automatically. Memory operation spans
can be added via @opentelemetry/api in service code. See
Enable observability for the full
tracing setup, including a local Jaeger walkthrough.
Image in CI
Section titled “Image in CI”The docker-build job in .github/workflows/ci.yml builds and smoke-tests
the image on every push to main and on pull requests, but never pushes
it (push: false) — CI is build validation only. Published images come
exclusively from the release workflow below.
Releases (publishing to GHCR)
Section titled “Releases (publishing to GHCR)”The release workflow (.github/workflows/release.yml) publishes
ghcr.io/osirison/engram/mcp-server — the image that
docker-compose.prod.yml pulls. It runs when a git tag matching v* is
pushed and authenticates with the workflow’s own GITHUB_TOKEN
(packages: write); no extra registry secret is required.
Cutting a release
Section titled “Cutting a release”git checkout main && git pullgit tag v1.2.3git push origin v1.2.3The workflow then:
- Builds the image and smoke-tests it (boots the
liteprofile against a throwaway Postgres and polls/health) before anything is published. - Pushes the image with BuildKit provenance and SBOM attestations attached to the manifest.
- Records a GitHub build-provenance attestation for the pushed digest.
- Creates a GitHub release for the tag with auto-generated notes
(tags containing a
-, e.g.v1.3.0-rc.1, are marked pre-release).
Published image tags
Section titled “Published image tags”| Tag | Example | Notes |
|---|---|---|
<major>.<minor>.<patch> |
1.2.3 |
Exact release |
<major>.<minor> |
1.2 |
Latest patch of the minor line |
<major> |
1 |
Latest release of the major line |
sha-<commit> |
sha-6c93444… |
Immutable; pins the exact build source |
latest |
latest |
Non-prerelease releases only |
Selecting a version in production
Section titled “Selecting a version in production”docker-compose.prod.yml uses image: ghcr.io/osirison/engram/mcp-server:${IMAGE_TAG:-latest}.
Pin a specific version in .env.prod instead of relying on latest:
IMAGE_TAG=1.2.3Verifying a pulled image
Section titled “Verifying a pulled image”# GitHub build-provenance attestationgh attestation verify oci://ghcr.io/osirison/engram/mcp-server:1.2.3 \ --repo osirison/engram
# BuildKit provenance / SBOM attached to the manifestdocker buildx imagetools inspect ghcr.io/osirison/engram/mcp-server:1.2.3 \ --format '{{ json .Provenance }}'Updating
Section titled “Updating”# Pull the released image (set IMAGE_TAG in .env.prod to move versions)docker compose -f docker-compose.prod.yml pull mcp-serverdocker compose -f docker-compose.prod.yml up -d mcp-server
# After schema changesdocker compose -f docker-compose.prod.yml run --rm mcp-server \ node_modules/.bin/prisma migrate deploySecurity considerations
Section titled “Security considerations”- All secrets are passed via environment variables; never baked into the image.
- The container runs as user
engram(non-root) on a minimalnode:22-alpinebase. - The Postgres port is not published externally in
docker-compose.prod.yml. - Review the OWASP security checklist before going to production.