Home Docs Blog Demo

Deployment

Deploy Context Harness as a persistent MCP server, in Docker, CI/CD, or as a static search index.

Running the MCP server

The simplest deployment: run ctx serve mcp as a persistent process.

$ ctx serve mcp --config ./config/ctx.toml
Loaded 2 Lua tool(s):
  POST /tools/echo — Echoes back the input message
  POST /tools/create_jira_ticket — Create a Jira ticket enriched with related context
Listening on 127.0.0.1:7331

The server binds to the address in [server].bind. For Docker or remote access, use 0.0.0.0:

[server]
bind = "0.0.0.0:7331"

Docker deployment

Build a minimal Docker image with all your config, connectors, tools, and agents baked in:

# Dockerfile
FROM rust:1-slim AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y git pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
COPY . .
RUN cargo install --path .

FROM debian:bookworm-slim
RUN apt-get update && \
    apt-get install -y ca-certificates curl git && \
    rm -rf /var/lib/apt/lists/*

COPY --from=builder /usr/local/cargo/bin/ctx /usr/local/bin/ctx
COPY config/ /app/config/
COPY connectors/ /app/connectors/
COPY tools/ /app/tools/
COPY agents/ /app/agents/

WORKDIR /app
RUN mkdir -p /app/data
EXPOSE 7331

HEALTHCHECK --interval=30s --timeout=5s \
  CMD curl -f http://localhost:7331/health || exit 1

ENTRYPOINT ["/bin/bash", "-c"]
CMD ["ctx init --config /app/config/ctx.toml && \
      ctx sync all --full --config /app/config/ctx.toml && \
      ctx embed pending --config /app/config/ctx.toml || true && \
      ctx serve mcp --config /app/config/ctx.toml"]
$ docker build -t context-harness .

$ docker run -d \
    --name ctx \
    -p 7331:7331 \
    -e OPENAI_API_KEY=$OPENAI_API_KEY \
    -v ctx-data:/app/data \
    context-harness

$ curl localhost:7331/health
{"status":"ok","version":"0.1.0"}

# Verify agents are loaded
$ curl -s localhost:7331/agents/list | jq '.agents[] | .name'

Docker Compose

For a production setup with persistent storage, mounted source repos, and auto-restart:

# docker-compose.yml
version: '3.8'
services:
  context-harness:
    build: .
    ports:
      - "7331:7331"
    volumes:
      # Persistent database (survives container restarts)
      - ctx-data:/app/data
      # Config, connectors, tools, agents (live editable)
      - ./config:/app/config:ro
      - ./connectors:/app/connectors:ro
      - ./tools:/app/tools:ro
      - ./agents:/app/agents:ro
      # Mount source repos for filesystem connectors (read-only)
      - ~/dev:/data/repos:ro
      # Mount notes for Obsidian/markdown indexing
      - ~/Documents/notes:/data/notes:ro
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - JIRA_API_TOKEN=${JIRA_API_TOKEN}
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:7331/health"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  ctx-data:
$ docker compose up -d
$ docker compose logs -f context-harness

# Initial sync (run once, takes a while for many repos)
$ docker compose exec context-harness bash -c \
    "ctx sync all --full --config /app/config/ctx.toml && \
     ctx embed pending --config /app/config/ctx.toml"

# Re-sync on a schedule
$ docker compose exec context-harness bash -c \
    "ctx sync all --config /app/config/ctx.toml && \
     ctx embed pending --config /app/config/ctx.toml"

Docker with agents

When deploying with agents, your directory structure should look like:

project/
├── config/
│   └── ctx.toml           # All config: connectors, tools, agents
├── connectors/
│   ├── jira.lua            # Lua connectors
│   └── confluence.lua
├── tools/
│   ├── create-ticket.lua   # Lua tools
│   └── post-slack.lua
├── agents/
│   └── incident-responder.lua  # Lua agents
├── Dockerfile
├── docker-compose.yml
└── data/                   # Mounted as volume
    └── ctx.sqlite

Your ctx.toml references agents alongside connectors and tools:

# Inline agents (static prompts, no Lua needed)
[agents.inline.code-reviewer]
description = "Reviews code against conventions"
tools = ["search", "get"]
system_prompt = "You are a senior code reviewer..."

# Lua agents (dynamic, pre-search context)
[agents.script.incident-responder]
path = "/app/agents/incident-responder.lua"
timeout = 30

Verify agents after startup:

$ curl -s localhost:7331/agents/list | jq '.agents[] | {name, description, source}'
{"name": "code-reviewer", "description": "Reviews code against conventions", "source": "toml"}
{"name": "incident-responder", "description": "Helps triage incidents", "source": "lua"}

Systemd service (Linux)

For bare-metal deployments:

# /etc/systemd/system/context-harness.service
[Unit]
Description=Context Harness MCP Server
After=network.target
Documentation=https://parallax-labs.github.io/context-harness/docs/

[Service]
Type=simple
User=ctx
Group=ctx
WorkingDirectory=/opt/context-harness
ExecStartPre=/usr/local/bin/ctx sync all --full --config /opt/context-harness/config/ctx.toml
ExecStart=/usr/local/bin/ctx serve mcp --config /opt/context-harness/config/ctx.toml
Restart=on-failure
RestartSec=5
Environment=OPENAI_API_KEY=sk-...
Environment=JIRA_API_TOKEN=...

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/context-harness/data
PrivateTmp=true

[Install]
WantedBy=multi-user.target
$ sudo systemctl enable context-harness
$ sudo systemctl start context-harness
$ sudo journalctl -u context-harness -f

CI/CD — build the search index

Build the index in CI and deploy it as an artifact or alongside your documentation:

# .github/workflows/build-context.yml
name: Build Context Index
on:
  push:
    branches: [main]
    paths: ['docs/**', 'src/**']
  schedule:
    - cron: '0 6 * * *'  # Daily at 6 AM

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/
            target/
          key: cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Build ctx
        run: cargo build --release

      - name: Index documentation
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          ./target/release/ctx init --config ./config/ctx.toml
          ./target/release/ctx sync all --full --config ./config/ctx.toml
          ./target/release/ctx embed pending --config ./config/ctx.toml

      - name: Upload database
        uses: actions/upload-artifact@v4
        with:
          name: ctx-database
          path: data/ctx.sqlite
          retention-days: 30

Static site search (no server)

For documentation sites that need search without running a backend:

  1. Export the index as data.json in CI
  2. Include ctx-search.js in your HTML
  3. All search runs in the browser at zero cost
# In your build script:
python3 -c "
import sqlite3, json, sys
db = sqlite3.connect('data/ctx.sqlite')
db.row_factory = sqlite3.Row
docs = [dict(r) for r in db.execute('SELECT id, source, source_id, source_url, title, updated_at, body FROM documents')]
chunks = [dict(r) for r in db.execute('SELECT id, document_id, chunk_index, text FROM chunks')]
json.dump({'documents': docs, 'chunks': chunks}, sys.stdout)
" > site/data.json
<!-- In your HTML -->
<script src="/ctx-search.js"
    data-json="/data.json"
    data-trigger="#search-btn"
    data-placeholder="Search docs...">
</script>

Cron-based re-sync

Keep the index fresh with periodic re-syncs:

# /etc/cron.d/context-harness
# Re-sync every 6 hours
0 */6 * * * /usr/local/bin/ctx sync all --config /opt/context-harness/config/ctx.toml && /usr/local/bin/ctx embed pending --config /opt/context-harness/config/ctx.toml

Or use a systemd timer:

# /etc/systemd/system/ctx-sync.timer
[Unit]
Description=Context Harness periodic sync

[Timer]
OnCalendar=*-*-* 0/6:00:00
Persistent=true

[Install]
WantedBy=timers.target

Production checklist