Deployment
Mirrored from
iblai/ontologyยทdocs/deployment.md. This page is generated โ edit it in the repository, not here.
Part of the iblai-ontology architecture. See the architecture overview.
iblai-ontology deploys as a Docker Compose stack on university infrastructure. The stack contains only the knowledge layer โ the data stores, the MCP servers, the identity gateway, and the MCP exposure endpoint. The agent runtime is not part of the stack; it connects remotely over MCP (see platform-integration.md).
Prerequisites
The university provides:
- A Linux server or VM inside their network โ Ubuntu 22.04+, 16 GB RAM minimum, 500 GB disk.
- Docker + Docker Compose installed.
- Network access from that server to the source systems: PeopleSoft Oracle (port 1521), and HTTPS to Canvas / Slate / Navigate APIs.
- A read-only database user with SELECT grants on the relevant schemas (the safety suite will verify this).
- A Canvas admin API token and Slate API credentials (as applicable).
- An Entra ID app registration for iblai-ontology (see identity.md โ only basic auth scopes, no App Roles needed).
- A DNS record pointing
ontology.<university>.eduat the server.
The Stack
docker-compose.yml (knowledge layer only). Annotated and condensed; the canonical file is generated by ontology config init and extended by the provisioning engine.
services:
# --- MCP Inbound: database connectivity (Component 1) ---
mcp-toolbox:
image: us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:1.5.0
volumes: [./config/tools.yaml:/app/tools.yaml]
env_file: .env.mcp
networks: [ontology-internal]
restart: unless-stopped
# --- MCP Inbound: custom API servers (one per source) ---
mcp-canvas: { build: ./mcp-servers/canvas, env_file: .env.canvas, networks: [ontology-internal], restart: unless-stopped }
mcp-slate: { build: ./mcp-servers/slate, env_file: .env.slate, networks: [ontology-internal], restart: unless-stopped }
mcp-navigate: { build: ./mcp-servers/navigate, env_file: .env.navigate, networks: [ontology-internal], restart: unless-stopped }
mcp-ldap: { build: ./mcp-servers/ldap, env_file: .env.ldap, networks: [ontology-internal], restart: unless-stopped }
# --- Knowledge store (Component 2) ---
ontology-db:
image: postgres:16-alpine
volumes:
- ontology-data:/var/lib/postgresql/data
- ./sql/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql
- ./sql/views.sql:/docker-entrypoint-initdb.d/02-views.sql
environment:
POSTGRES_DB: ontology
POSTGRES_USER: ontology
POSTGRES_PASSWORD: ${ONTOLOGY_DB_PASSWORD}
networks: [ontology-internal]
restart: unless-stopped
vector-store:
image: chromadb/chroma:0.6.3
volumes: [vector-data:/chroma/chroma]
environment:
ANONYMIZED_TELEMETRY: "false"
CHROMA_SERVER_AUTHN_PROVIDER: chromadb.auth.token_authn.TokenAuthenticationServerProvider
CHROMA_SERVER_AUTHN_CREDENTIALS: ${CHROMA_TOKEN:?set CHROMA_TOKEN in .env}
networks: [ontology-internal]
restart: unless-stopped
# --- Sync engine (Component 2; Django + Celery) ---
sync-engine:
build: ./sync-engine
volumes:
- ontology-files:/ontology
- ./config/sync-schedules.yaml:/app/config.yaml
environment:
MCP_TOOLBOX_URL: http://mcp-toolbox:5000
MCP_CANVAS_URL: http://mcp-canvas:3000
ONTOLOGY_DB_URL: postgresql://ontology:${ONTOLOGY_DB_PASSWORD}@ontology-db:5432/ontology
CHROMA_URL: http://vector-store:8000
CHROMA_TOKEN: ${CHROMA_TOKEN:?set CHROMA_TOKEN in .env}
depends_on: [mcp-toolbox, mcp-canvas, ontology-db, vector-store]
networks: [ontology-internal]
restart: unless-stopped
# --- MCP Outbound: exposure gateway (Component 4) ---
ontology-gateway:
build: ./gateway
volumes:
- ontology-files:/ontology:ro # read-only mount
- ./config/roles.yaml:/app/roles.yaml
environment:
MCP_TOOLBOX_URL: http://mcp-toolbox:5000
ONTOLOGY_DB_URL: postgresql://ontology:${ONTOLOGY_DB_PASSWORD}@ontology-db:5432/ontology
CHROMA_URL: http://vector-store:8000
CHROMA_TOKEN: ${CHROMA_TOKEN:?set CHROMA_TOKEN in .env}
ENTRA_TENANT_ID: ${ENTRA_TENANT_ID}
ENTRA_CLIENT_ID: ${ENTRA_CLIENT_ID}
depends_on: [mcp-toolbox, ontology-db, vector-store]
networks: [ontology-internal, ontology-exposed] # bridges both networks
restart: unless-stopped
# --- HTTPS reverse proxy ---
proxy:
image: caddy:2-alpine
volumes:
- ./config/Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
ports: ["443:443"]
depends_on: [ontology-gateway]
networks: [ontology-exposed]
restart: unless-stopped
# --- Admin dashboard ---
admin-dashboard:
build: ./admin-dashboard
environment:
ONTOLOGY_DB_URL: postgresql://ontology:${ONTOLOGY_DB_PASSWORD}@ontology-db:5432/ontology
ENTRA_TENANT_ID: ${ENTRA_TENANT_ID}
ENTRA_CLIENT_ID: ${ENTRA_CLIENT_ID}
networks: [ontology-internal, ontology-exposed]
restart: unless-stopped
networks:
ontology-internal:
driver: bridge
internal: true # No external/internet access for internal services
ontology-exposed:
driver: bridge # Gateway and proxy can reach the outside
volumes:
ontology-data:
ontology-files:
vector-data:
caddy-data:
Internal vs. exposed networks
This is the heart of the on-prem security posture:
ontology-internalis markedinternal: trueโ containers on it cannot reach the internet. The databases, MCP Toolbox, custom MCP servers, and sync engine all live here only.ontology-exposedis a normal bridge that can reach outside.- Only
ontology-gatewayandproxybridge both networks. So PeopleSoft credentials, the cache, and the memory files are never directly exposed; the only door in is the gateway, behind the firewall + Entra-token gating described in Component 4.
Note also the gateway mounts ontology-files read-only (:ro) โ the exposure layer can serve memory files but never modify them.
Caddyfile
config/Caddyfile โ Caddy auto-provisions TLS via Let's Encrypt (or use a university-provided certificate):
ontology.alasu.edu {
reverse_proxy ontology-gateway:8080
log { output file /var/log/caddy/access.log }
}
admin.ontology.alasu.edu {
reverse_proxy admin-dashboard:8080
log { output file /var/log/caddy/admin-access.log }
}
Environment File
Credentials are split per MCP server for isolation โ each container loads only its own file via env_file:. Compose will not parse unless every referenced file exists, and each custom MCP server does os.environ["X"] at import, so a missing/empty file crash-loops that one container. Templates for all of them ship as .env.*.example (copy each to the real name; the real files are git-ignored):
| File | Vars | Container |
|---|---|---|
.env | ONTOLOGY_DB_PASSWORD, ENTRA_TENANT_ID, ENTRA_CLIENT_ID | compose ${...} + gateway/admin |
.env.mcp | PEOPLESOFT_DB_HOST/PORT/NAME, PEOPLESOFT_RO_USER/PASSWORD, ONTOLOGY_DB_USER, ONTOLOGY_DB_PASSWORD | mcp-toolbox |
.env.canvas | CANVAS_BASE_URL, CANVAS_API_TOKEN | mcp-canvas |
.env.slate | SLATE_BASE_URL, SLATE_API_KEY | mcp-slate |
.env.navigate | NAVIGATE_BASE_URL, NAVIGATE_API_KEY | mcp-navigate |
.env.ldap | LDAP_URI, LDAP_BIND_DN, LDAP_BIND_PASSWORD, LDAP_BASE_DN | mcp-ldap |
CANVAS_BASE_URL is the instance root only โ no trailing slash, no /api/v1 (the server appends it). The Canvas token must be admin-scoped to look up other users; the tools accept a Canvas user id, a SIS id, or a sis_login_id: ref.
Cloud sources note:
ontology-internalisinternal: true(no internet egress). A cloud source (e.g.*.instructure.com) is unreachable from a container placed only on that network โ reach it from an egress-capable network, or test the MCP server standalone.
Representative .env values:
# PeopleSoft Oracle (read-only credentials provided by the university)
PEOPLESOFT_DB_HOST=psft-db.internal.alasu.edu
PEOPLESOFT_DB_PORT=1521
PEOPLESOFT_DB_NAME=CSPRD
PEOPLESOFT_RO_USER=iblai_readonly
PEOPLESOFT_RO_PASSWORD=<provided-by-university>
# Canvas LMS
CANVAS_BASE_URL=https://alasu.instructure.com
CANVAS_API_TOKEN=<generated-by-canvas-admin>
# Slate CRM
SLATE_BASE_URL=https://apply.alasu.edu
SLATE_API_KEY=<provided-by-slate-admin>
# Local iblai-ontology DB
ONTOLOGY_DB_PASSWORD=<generated-during-setup>
# Microsoft Entra ID
ENTRA_TENANT_ID=<university-tenant-id>
ENTRA_CLIENT_ID=<iblai-ontology-app-client-id>
# Credential encryption (Fernet) for the service registry
ONTOLOGY_CREDENTIAL_KEY=<generated-during-setup>
MCP Toolbox config generation
The mcp-toolbox container does not read config/tools.yaml directly โ that
file is the ontology's authoring DSL (kind: source|tool|toolset documents). The
Toolbox needs its own native format (top-level sources / tools / toolsets
maps). ontology mcp build translates the DSL into
config/generated/toolbox.yaml (git-ignored), which the container mounts:
ontology mcp build # writes config/generated/toolbox.yaml
# ontology deploy up runs this automatically before starting the stack
Translation rules: type โ kind; Oracle's database โ serviceName; ${VAR}
tokens are left intact so the Toolbox expands them from .env.mcp (secrets never
land in the generated file). Tools whose statement is a ${...} raw-SQL
passthrough (e.g. query-ontology-cache) can't be expressed as a Toolbox
parameterized query โ they're served directly by the gateway and excluded
from the generated file. The gateway calls tools via the Toolbox /mcp JSON-RPC
endpoint (the legacy /api/tool/<name> REST path is disabled in Toolbox โฅ1.5).
Eager connection โ important. The Toolbox connects to every source at startup and refuses to start if any source is unreachable. So the active
tools.yamlmust contain only sources you have actually configured and can reach. For this reason the higher-ed PeopleSoft sample is kept out of the defaulttools.yamlโ it lives inconfig/tools.higher-ed.example.yaml(paired withconfig/roles.higher-ed.example.yaml). Merge it in only when you have a real PeopleSoft to point at; otherwise themcp-toolboxcontainer crash-loops.
Reaching source databases (local or remote)
Source connections (client-postgres, client-mysql, โฆ) are host/credential-driven
via .env.mcp, so the same config connects to a local container or a
remote host โ only the network path differs. That path is deployment-specific
and lives in a git-ignored docker-compose.override.yml (copy from
docker-compose.override.example.yml), not the committed compose:
- Local source containers: add
mcp-toolboxto the source's docker network and set*_HOSTto the container name (e.g.my-postgres,my-mysql). - Remote sources:
ontology-internalisinternal: true(no egress), so addmcp-toolboxto an egress-capable network and set*_HOSTto the remote hostname/IP.
Operating the Stack
The ontology deploy commands wrap Docker Compose (see CLI reference):
ontology deploy up --build # start everything
ontology deploy status # container status
ontology deploy logs sync-engine -f --tail 20
ontology deploy restart ontology-gateway
ontology deploy down # stop (add --volumes to wipe data)
$ ontology deploy status
NAME STATUS PORTS
ontology-db running 5432/tcp
vector-store running 8000/tcp
mcp-toolbox running 5000/tcp
mcp-canvas running 3000/tcp
sync-engine running
ontology-gateway running 8080/tcp
proxy running 0.0.0.0:443->443/tcp
admin-dashboard running 8080/tcp
Validate the deployment with ontology health and ontology config validate.
Phased Rollout
Phase 1 โ Foundation (Weeks 1โ3)
Week 1 โ Infrastructure and first source.
- Clone the deployment repo onto the server;
cp .env.example .envand fill it in. - Start core infra:
docker compose up -d ontology-db vector-store mcp-toolbox. - Verify Toolbox reaches PeopleSoft (call
get-student-enrollment). - Run the initial full sync:
docker compose up -d sync-engine; watch logs. - Verify memory files and the Postgres cache populated (
SELECT COUNT(*) FROM students;).
Week 2 โ Exposure and authentication.
6. Configure the Entra ID app registration (identity.md).
7. Start the gateway + proxy: docker compose up -d ontology-gateway proxy.
8. Test token validation with a real Entra token (device-code flow), then verify role scoping: a financial-aid counselor sees finaid + enrollment tools; a student sees only self-service tools; an unassigned user gets the default role (or 403 where applicable).
Week 3 โ Connect the agent runtime and validate end-to-end. 9. Register iblai-ontology as an MCP Server in the ibl.ai platform and attach it to agents (platform-integration.md). 10. Drive an end-to-end agent interaction: user opens chat โ platform triggers in-chat OAuth โ user authenticates โ platform creates ConnectedService + MCPServerConnection โ agent queries the gateway with the user's token โ scoped data returned. 11. Deploy the admin dashboard; cross-check ontology data against PeopleSoft reports (counts, GPAs, holds).
Phase 2 โ Expand sources (Weeks 3โ6)
- Deploy the Canvas and Slate MCP servers (
docker compose up -d mcp-canvas mcp-slate). - Extend sync schedules so student memory files include Canvas grades and Slate admissions data.
- Build and test the Financial Aid Agent and the Advising Agent.
- Verify role-based boundaries: a counselor sees aid data but not HR; a student sees only their own record.
Phase 3 โ Full iblai-ontology (Weeks 6โ10)
- Add the remaining MCP servers: Navigate, LDAP/AD, and others.
- Build department agents (IT Help Desk, Facilities, HR).
- Enable vector search over text memories via ChromaDB.
- Build out the admin dashboard (sync monitoring, audit-log viewer, role management).
- Production-harden: TLS certificates, backup schedules, monitoring/alerting, log aggregation.
- Load test concurrent users across roles; verify response times and source-system impact.
Phase 4 โ Advanced (ongoing)
- Event-driven sync via webhooks from PeopleSoft / Canvas / Slate for real-time updates to critical data (new holds, registration changes, grade submissions).
- Student-facing self-service agents with limited scope.
- Cross-institutional templates โ package the stack so a new deployment is clone โ fill credentials โ
docker compose up. - Write-back with approval workflows โ an agent creates a pending action, a human approves in the UI, and the approved action executes via an MCP write tool.
Related
- The components running in each container: architecture.md
- Network/identity gating at the gateway: components/04-mcp-outbound.md, identity.md
- Registering with the platform: platform-integration.md
- Operating the stack from the CLI: components/07-cli.md