Deployment

Phases 1–12 · ~25 min read v1.0 Phase 11 Phase 12

Prerequisites

xbrain deploys as a Docker Compose stack on a Linux VM. GCP e2-medium is the recommended starting point for Phase 1 (~25€/mo). All services run as Docker containers — no Kubernetes required.

VM Sizing by Phase

Phase 1 runs on a 4 GB VM (e2-medium, ~25€/mo on GCP). Before starting Phase 2, upgrade to 8 GB (e2-standard-2, ~49€/mo). Phase 3+ may require 16 GB depending on load.

Requirement Details
Cloud VM GCP e2-medium (4 GB, 2 vCPU) for Phase 1
Operating System Ubuntu 24.04 LTS
Docker 24.0+ (install via official script)
Docker Compose v2 (included with Docker or via apt install docker-compose-plugin)
Firewall TCP 80 and 443 open (GCP firewall rules)
Domain (optional) For HTTPS via Cloudflare DNS proxy
Google OAuth App Client ID + Secret from Google Cloud Console (for SSO)

Phase 1: Core Stack

Phase 1 deploys nginx, PostgreSQL, Qdrant, memory-api, LibreChat, Open WebUI, and the memory bridges. This is the minimal stack that satisfies the core memory invariant: every conversation is stored with the 7-field tagging contract enforced.

Step 1 — Create a GCP VM

bash# Create e2-medium VM (GCP Console or gcloud CLI)
gcloud compute instances create xbrain-vm \
  --zone=europe-west1-b \
  --machine-type=e2-medium \
  --image-family=ubuntu-2404-lts \
  --image-project=ubuntu-os-cloud \
  --boot-disk-size=50GB \
  --tags=http-server,https-server

# Open HTTP/HTTPS ports
gcloud compute firewall-rules create xbrain-http \
  --allow tcp:80,tcp:443 \
  --target-tags http-server,https-server

Step 2 — Install Docker

bashssh user@YOUR_VM_IP

# Install Docker (official script)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker

# Verify
docker --version         # Docker 24.x+
docker compose version   # Docker Compose version v2.x

Step 3 — Clone and configure

bashgit clone https://github.com/mrboups/xbrain.git
cd xbrain/infrastructure
cp .env.example .env
nano .env  # Fill in your values (see Configuration page)

Required .env values for Phase 1:

.env — Phase 1 requiredPOSTGRES_PASSWORD=<strong-random-password>
GOOGLE_CLIENT_ID=<from-google-cloud-console>
GOOGLE_CLIENT_SECRET=<from-google-cloud-console>
BRIDGE_SHARED_SECRET=$(openssl rand -hex 32)
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
MEILI_MASTER_KEY=$(openssl rand -hex 32)
PIPELINE_API_KEY=$(openssl rand -hex 32)

Step 4 — Start Phase 1

bashdocker compose up -d

# Monitor startup (takes ~60s for all healthchecks to pass)
docker compose ps
watch docker compose ps   # Wait for all to show "healthy"

Step 5 — Configure nginx

bash# Edit nginx config with your VM IP or domain
nano infrastructure/nginx/conf.d/10-xbrain.conf

# Restart nginx to apply
docker compose restart nginx

Step 6 — Verify Phase 1

bashbash infrastructure/scripts/verify-phase1.sh
# Expected: PASS: 6 / 6

First Login

Access LibreChat at http://YOUR_VM_IP. Sign in with Google — the email must match a configured team scope. Open WebUI is available at http://YOUR_VM_IP/open-webui.

Phase 2: Intelligent Memory

Phase 2 adds agent-runtime (LangGraph), Langfuse observability, mem0, Redis, MinIO, and ClickHouse. This phase requires more RAM — upgrade before starting.

VM Upgrade Required

Before starting Phase 2, upgrade your VM from e2-medium (4 GB) to e2-standard-2 (8 GB). Adding Phase 2 services to a 4 GB VM will cause OOM kills.

bash — resize VM# GCP Console: stop VM → Edit → Change machine type → e2-standard-2 → Start
# Or via gcloud CLI:
gcloud compute instances stop xbrain-vm
gcloud compute instances set-machine-type xbrain-vm --machine-type=e2-standard-2
gcloud compute instances start xbrain-vm

Add to .env:

.env — Phase 2 additionsLANGFUSE_PUBLIC_KEY=<from-langfuse-setup>
LANGFUSE_SECRET_KEY=<from-langfuse-setup>
bash# Phase 2 services are already defined in docker-compose.yml
docker compose up -d
bash infrastructure/scripts/verify-phase2.sh
# Expected: PASS: 6 / 6

Langfuse UI is available at http://YOUR_VM_IP:3000 (or obs.yourdomain.com if using a custom domain). On first boot, create an admin account and generate your LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY.

Phase 3: Graph + Integrations

Phase 3 adds Neo4j (knowledge graph), mcp-gateway, mcp-scraper, mcp-drive-read, mcp-calendar, and drive-sync. MCP tools are registered with memory-api after startup.

Add to .env:

.env — Phase 3 additionsNEO4J_PASSWORD=<strong-password>
OAUTH_CREDENTIALS_ENCRYPTION_KEY=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
# GOOGLE_CLIENT_SECRET must already be set from Phase 1
bashdocker compose up -d

# Register MCP tools with memory-api
bash infrastructure/scripts/register-mcp-tools.sh

# Verify
bash infrastructure/scripts/verify-phase3.sh
# Expected: PASS: 7 / 7

Google Drive OAuth Setup

For Drive sync, you must add your VM's callback URL to your Google OAuth app's Authorized Redirect URIs: https://api.yourdomain.com/v1/oauth/callback. Users then authorize from their LibreChat profile page.

Phase 4: MCP Completion

Phase 4 adds mcp-deck (PPTX generation) and upgrades the LibreChat bridge for full multi-tool support. No new environment variables are required.

bash# Build mcp-deck (local build required)
docker compose build mcp-deck

# Restart stack
docker compose up -d

# Verify
bash infrastructure/scripts/verify-phase4.sh
# Expected: PASS: 8 / 8

Phase 5: Team Platform

Phase 5 adds graphiti-service (temporal fact extraction), Chrome extension web clipper, and projects-dashboard. Auth was originally an OAuth App + long-lived GITHUB_API_PAT — both retired in Phase 12 in favour of the xbrain GitHub App (see Phase 12 below).

Add to .env:

.env — Phase 5 additionsGRAPHITI_SERVICE_URL=http://graphiti-service:8300
GITHUB_ORG=your-github-org        # Display only post-Phase 12
bashdocker compose up -d
bash infrastructure/scripts/verify-phase5.sh
# Expected: PASS: 8 / 8

GITHUB_API_PAT removed in Phase 12

The legacy long-lived GITHUB_API_PAT env var has been removed. All org-membership lookups now flow through the xbrain GitHub App's short-lived installation tokens (ghs_, ~1h TTL). See the Phase 12 section for the GitHub App registration runbook.

Phase 11: Brain Monitor + Superadmin Dashboard

Phase 11 introduces the universal Brain Monitor (single feed across the 7 brainable entity types), the /account/admin/ superadmin dashboard, and a daily hard-purge cron container (brain-janitor).

Migrations (apply automatically on memory-api boot):

Add to .env:

.env — Phase 11 additions# Superadmins for /v1/admin/brain/*.
# Comma-separated subs (e.g. "github:mrboups,google:1087654...").
# Empty value (or unset) disables ALL superadmin endpoints (lockdown kill-switch).
ADMIN_USER_SUBS=github:mrboups

# MinIO (used by /v1/admin/brain/storage — N/A on miss, never 500).
MINIO_URL=http://xbrain-minio:9000
MINIO_ACCESS_KEY=...
MINIO_SECRET_KEY=...
MINIO_BUCKET=xbrain-default
bashdocker compose up -d brain-janitor memory-api
bash infrastructure/scripts/verify-phase11.sh
# Expected: PASS: 5 / 5 (with 11 SKIPped optional fixtures)

brain-janitor health

The brain-janitor container runs at 03:00 UTC daily. Liveness sentinel: /tmp/brain-janitor-alive must be touched within the last 25 hours. Verification assertion 11 SKIPs on cold start (sentinel not yet created); FAILs after the first cycle if the sentinel is stale.

Phase 12: GitHub App Migration

Phase 12 migrates auth from the legacy OAuth App + long-lived GITHUB_API_PAT to a GitHub App. The new App supports multi-callback (web + Chrome extension), short-lived installation tokens (ghs_ ~1h), user-token refresh (ghu_/ghr_, ~6 month sliding window), and install/uninstall webhooks.

Step 1 — Derive the Chrome extension ID FIRST

The GitHub App's callback URL must include https://<ext-id>.chromiumapp.org/. The Chrome extension uses a manifest.json "key" to pin the extension ID so it stays stable across dev machines. For Phase 12 the canonical ID is:

chrome-ext-idanigikcnmldoklcmogffmgcojdhhficb

See the Chrome Extension guide for the keypair-generation + manifest "key" derivation procedure.

Step 2 — Register the App on GitHub

At github.com/settings/appsNew GitHub App:

Step 3 — Capture secrets, encode the PEM

After saving the App, capture:

bash — encode PEM as single linebase64 -w 0 < xbrain.<date>.private-key.pem
# macOS: base64 -i xbrain.<date>.private-key.pem | tr -d '\n'

Add to .env (5 new vars in Phase 12; GITHUB_APP_SLUG is optional):

.env — Phase 12 additionsGITHUB_APP_ID=3743573
GITHUB_APP_SLUG=xbrain
GITHUB_APP_CLIENT_ID=Iv23liVnZvIN0Lo6isof
GITHUB_APP_CLIENT_SECRET=<from App settings>
GITHUB_APP_PRIVATE_KEY_B64=<single-line base64 of the PEM>
GITHUB_APP_WEBHOOK_SECRET=<from Step 2>

# REMOVE from .env (replaced by App JWT + installation tokens):
# GITHUB_API_PAT=
# GITHUB_ORG_PAT=

Step 4 — Restart and verify

bashdocker compose up -d memory-api
docker exec xbrain-memory-api printenv | grep ^GITHUB_APP_   # must show 6 vars

export MEMAPI_HOST=https://api.grooveos.app
export GITHUB_APP_CLIENT_ID=Iv23liVnZvIN0Lo6isof
export GITHUB_APP_WEBHOOK_SECRET=<value from .env>
export TEST_GITHUB_ORG=dejavudev   # any org with the App installed

bash infrastructure/scripts/verify-phase12.sh
# Expected: PASS ≥ 13/13 (some assertions SKIP without fixtures)

Step 5 — Install the App on an org

Visit https://github.com/apps/<your-slug>Configure → pick your org → Install on All repositories. The POST /v1/webhooks/github/installation webhook fires within ~30 s and creates the installations row. If the webhook is delayed, the next signin self-heals via the hybrid installation lookup.

Phase 12 retires GITHUB_API_PAT entirely

After Phase 12 LIVE, verify-phase12.sh assertion 6 fails the deploy if any import of GITHUB_API_PAT remains in apps/memory-api/. The legacy OAuth App (Ov23liy7tZekl0uEztoj) should be revoked 24h+ after the new App passes UAT — see the oauth-app-revocation.md runbook in .planning/KB/.

HTTPS with Cloudflare

Cloudflare provides free HTTPS via their DNS proxy. Add your domain to Cloudflare, point an A record to your VM IP, and enable the proxy (orange cloud icon). Cloudflare terminates TLS and proxies HTTP to your VM on port 80.

Cloudflare Proxy Mode

With the orange cloud enabled, Cloudflare handles TLS — your nginx only needs to listen on port 80. Set SSL/TLS mode to "Flexible" in Cloudflare if your VM has no certificate, or "Full" if you add a self-signed cert on the VM.

Key nginx configuration for proxying to services:

nginx — 10-xbrain.conf (excerpt)server {
    listen 80;
    server_name x.yourdomain.com;

    location / {
        proxy_pass         http://librechat:3080;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_read_timeout 300s;
    }
}

server {
    listen 80;
    server_name api.yourdomain.com;

    location / {
        proxy_pass       http://memory-api:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Domains used in the reference deployment:

Backups

xbrain ships with backup and restore scripts for PostgreSQL, Qdrant, and Neo4j volumes. Run backups on a cron schedule or before major upgrades.

bash# Create a full backup (dumps PostgreSQL + archives named volumes)
bash infrastructure/scripts/backup.sh
# Output: backups/backup_20260501_120000.tar.gz

# Restore on a fresh VM (after cloning repo + setting .env)
bash infrastructure/scripts/restore.sh backups/backup_20260501_120000.tar.gz

Recommended cron schedule for automated backups:

crontab# Daily backup at 2:00 AM UTC
0 2 * * * cd /home/user/xbrain/infrastructure && bash scripts/backup.sh >> /var/log/xbrain-backup.log 2>&1

Monitoring VM Resources

With 25+ containers running, memory and disk usage require regular monitoring. These commands give you an instant view of the stack's health.

bash# Real-time container memory and CPU usage
docker stats --no-stream

# Check disk space (Docker layers + volumes are the main consumers)
df -h

# See how much Docker is using
docker system df

# If disk usage exceeds 90%, clean up unused images and stopped containers
docker system prune --volumes -f

Disk Usage Warning

The VM disk (50 GB recommended) can fill up quickly with Docker image layers, logs, and Qdrant vector data. If disk usage exceeds 95%, services will fail to write data. Run docker system prune or expand the GCP boot disk before adding new services or phases.

Check for containers in a restart loop or unhealthy state:

bash# List all containers with health status
docker compose ps

# Tail logs for a specific service
docker compose logs -f memory-api

# Check recent events (OOM kills, restarts)
docker events --since 1h

Upgrading

To upgrade xbrain to a newer version, pull the latest code and rebuild changed images. Always run a backup before upgrading.

bash# 1. Take a backup first
bash infrastructure/scripts/backup.sh

# 2. Pull latest code
git pull origin main

# 3. Pull updated Docker images
docker compose pull

# 4. Rebuild locally-built services (memory-api, agent-runtime, bridges, MCP tools)
docker compose build

# 5. Restart the stack
docker compose up -d

# 6. Check for any failed healthchecks
docker compose ps

Troubleshooting

Services not starting

Check logs for the failing service:

bashdocker compose logs memory-api
docker compose logs librechat
docker compose logs postgres

LibreChat can't connect to memory-api

Ensure BRIDGE_SHARED_SECRET is identical in both the bridge environment and the memory-api environment. Mismatched secrets cause 401 errors on every memory write.

OAuth login fails

Verify that GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set correctly and that the OAuth callback URI is registered in Google Cloud Console. The expected callback URL is https://api.yourdomain.com/v1/oauth/callback.

Out of memory (OOM) kills

If you see containers restarting with exit code 137, the VM is running out of memory. Check docker stats to identify the largest consumer and either upgrade the VM or disable non-essential services.

Next Steps