Skip to content

Local with Docker Compose

A single-machine deployment from zero to a working API. The active stack is four services: the AI Engine API, the Content Engine, Qdrant, and Redis.

1. Prerequisites

  • Docker and Docker Compose installed.
  • This repository checked out.

2. Configure

cd ai-engine-api
cp .env.example .env

Edit .env: set QDRANT_API_URL=http://qdrant:6333, REDIS_URL=redis://redis:6379/0, and your LLM key (OPENROUTER_API_KEY) or a local Ollama URL. Leave QDRANT_API_KEY empty for a local Qdrant.

3. A complete compose file

Save this as docker-compose.local.yml next to the API:

services:
  qdrant:
    image: qdrant/qdrant:latest
    ports: ["6333:6333", "6334:6334"]
    volumes: ["qdrant_data:/qdrant/storage"]

  redis:
    image: redis:7
    ports: ["6379:6379"]
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 10

  api:
    build: .
    env_file: .env
    environment:
      QDRANT_API_URL: http://qdrant:6333
      REDIS_URL: redis://redis:6379/0
    depends_on:
      redis: { condition: service_healthy }
      qdrant: { condition: service_started }
    ports: ["8000:8000"]
    restart: unless-stopped

  content-engine:
    image: python:3.12-slim
    working_dir: /app
    volumes: ["../content-engine:/app"]
    environment:
      QDRANT_API_URL: http://qdrant:6333
      COLLECTION_NAME: ${COLLECTION_NAME}
      EMBEDDING_MODEL: ${EMBEDDING_MODEL}
    command: bash -c "pip install -e '.[api,qdrant,embed]' && content-engine"
    depends_on:
      qdrant: { condition: service_started }
    ports: ["8002:8002"]

volumes:
  qdrant_data:

4. Bring it up

docker compose -f docker-compose.local.yml up -d --build

5. Load content

The Qdrant collection starts empty. Ingest documents through the Content Engine:

curl -X POST http://localhost:8002/ingest \
  -H "Content-Type: application/json" \
  -d '[{"id":"841","title":"Forced labour","text":"...","content_type":"text_item",
        "tags":[{"facet":"theme","label":"forced_labour","weight":1.0}]}]'

Or sync from a configured Omeka instance:

curl -X POST http://localhost:8002/sync/omeka -H "Content-Type: application/json" -d '{"max_items":500}'

6. Verify

curl http://localhost:8000/                       # {"status":"ok", ...}
curl "http://localhost:8000/api/search?q=Bergen-Belsen"
curl http://localhost:8002/health                 # {"status":"ok"}

Open the interactive references at AI Engine API and Content Manager API.

7. Optional: behavioral capture

To capture behavior, point a RudderStack source at the recsys ingest webhook (POST /api/ingest). Events land in the Redis buffer and are folded into the user model. See Online serving model.

Troubleshooting

  • Empty recommendations: the collection is not populated, or COLLECTION_NAME differs between the API and the Content Engine. See the consistency rule in Configuration.
  • API boots but recsys is fake: REDIS_URL or QDRANT_API_URL is unset; the engine fell back to in-memory stores.
  • No narratives: check the LLM variables (OPENROUTER_API_KEY or OLLAMA_BASE_URL).