# Deployment

> Mirrored from [`iblai/ontology`](https://github.com/iblai/ontology) · [`docs/deployment.md`](https://github.com/iblai/ontology/blob/main/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](/developer/guides/ontology-platform-integration)).

---

## 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](https://github.com/iblai/ontology/blob/main/docs/components/05-service-discovery.md#the-read-only-safety-verification-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](/developer/guides/ontology-identity#entra-id-app-registration-the-minimum) — only basic auth scopes, no App Roles needed).
- A **DNS record** pointing `ontology.<university>.edu` at 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](https://github.com/iblai/ontology/blob/main/docs/components/06-provisioning.md).

```yaml
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-internal`** is marked **`internal: true`** — containers on it **cannot reach the internet**. The databases, MCP Toolbox, custom MCP servers, and sync engine all live here only.
- **`ontology-exposed`** is a normal bridge that can reach outside.
- Only **`ontology-gateway`** and **`proxy`** bridge 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](https://github.com/iblai/ontology/blob/main/docs/components/04-mcp-outbound.md).

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-internal` is `internal: 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:

```bash
# 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:

```bash
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.yaml` must contain only sources you have actually configured and can
> reach. For this reason the higher-ed PeopleSoft sample is kept **out** of the
> default `tools.yaml` — it lives in `config/tools.higher-ed.example.yaml` (paired
> with `config/roles.higher-ed.example.yaml`). Merge it in only when you have a
> real PeopleSoft to point at; otherwise the `mcp-toolbox` container 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-toolbox` to the source's docker network
  and set `*_HOST` to the container name (e.g. `my-postgres`, `my-mysql`).
- **Remote sources:** `ontology-internal` is `internal: true` (no egress), so add
  `mcp-toolbox` to an egress-capable network and set `*_HOST` to the remote
  hostname/IP.

## Operating the Stack

The `ontology deploy` commands wrap Docker Compose (see [CLI reference](https://github.com/iblai/ontology/blob/main/docs/components/07-cli.md#ontology-deploy--docker-compose-lifecycle)):

```bash
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.**
1. Clone the deployment repo onto the server; `cp .env.example .env` and fill it in.
2. Start core infra: `docker compose up -d ontology-db vector-store mcp-toolbox`.
3. Verify Toolbox reaches PeopleSoft (call `get-student-enrollment`).
4. Run the initial full sync: `docker compose up -d sync-engine`; watch logs.
5. 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](/developer/guides/ontology-identity)).
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](/developer/guides/ontology-platform-integration)).
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)

12. Deploy the Canvas and Slate MCP servers (`docker compose up -d mcp-canvas mcp-slate`).
13. Extend sync schedules so student memory files include Canvas grades and Slate admissions data.
14. Build and test the Financial Aid Agent and the Advising Agent.
15. 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)

16. Add the remaining MCP servers: Navigate, LDAP/AD, and others.
17. Build department agents (IT Help Desk, Facilities, HR).
18. Enable vector search over text memories via ChromaDB.
19. Build out the admin dashboard (sync monitoring, audit-log viewer, role management).
20. Production-harden: TLS certificates, backup schedules, monitoring/alerting, log aggregation.
21. Load test concurrent users across roles; verify response times and source-system impact.

### Phase 4 — Advanced (ongoing)

22. **Event-driven sync** via webhooks from PeopleSoft / Canvas / Slate for real-time updates to critical data (new holds, registration changes, grade submissions).
23. Student-facing self-service agents with limited scope.
24. Cross-institutional templates — package the stack so a new deployment is *clone → fill credentials → `docker compose up`*.
25. **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](/developer/guides/ontology-architecture)
- Network/identity gating at the gateway: [components/04-mcp-outbound.md](https://github.com/iblai/ontology/blob/main/docs/components/04-mcp-outbound.md), [identity.md](/developer/guides/ontology-identity)
- Registering with the platform: [platform-integration.md](/developer/guides/ontology-platform-integration)
- Operating the stack from the CLI: [components/07-cli.md](https://github.com/iblai/ontology/blob/main/docs/components/07-cli.md)
