From 8fd6c99ccc6e863e086a45cc87fba0554d8d675b Mon Sep 17 00:00:00 2001 From: gramps Date: Sun, 19 Jul 2026 16:25:57 -0700 Subject: [PATCH] Add Docker containerization stack - Dockerfile: multi-stage Python 3.13-slim, healthcheck, uvicorn CMD - docker-compose.yml: full stack (cAIc, SearXNG, Qdrant, RabbitMQ, llama-server, Ollama) with healthchecks, volumes, secrets - .dockerignore: exclude venv, tests, .git, models, secrets - .env.example: all variables documented with generation hints - scripts/setup.sh: first-run scaffolding (generates .env, secrets, SearXNG config, directories) - searxng-settings.yml.dist: SearXNG config template - models/README.txt: instructions for placing .gguf files - config.py: add HW_STATE_PATH env var (CAIC_HW_STATE_PATH) - hardware.py: read state path from config instead of hardcoded CWD - requirements.txt: add missing psutil and jinja2 --- .dockerignore | 44 +++++++++++++++ .env.example | 53 +++++++++++++++++ Dockerfile | 36 ++++++++++++ config.py | 2 + docker-compose.yml | 116 ++++++++++++++++++++++++++++++++++++++ hardware.py | 4 +- models/README.txt | 9 +++ requirements.txt | 2 + scripts/setup.sh | 80 ++++++++++++++++++++++++++ searxng-settings.yml.dist | 26 +++++++++ 10 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 models/README.txt create mode 100644 scripts/setup.sh create mode 100644 searxng-settings.yml.dist diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dc641aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,44 @@ +# Version control +.git/ +.gitignore + +# Python +venv/ +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +.mypy_cache/ + +# Databases and state (persisted via volumes) +*.db +hardware_state.json + +# Secrets (generated by setup.sh) +.env +secrets/ +searxng/ + +# IDE / editor +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Models (bind-mounted at runtime) +models/ + +# Documentation (not needed in image) +*.md +docs/ +TASKS.md +docker.md +ai.md +CLAUDE.md +scripts/ +tests/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7650526 --- /dev/null +++ b/.env.example @@ -0,0 +1,53 @@ +# ───────────────────────────────────────────────────────────── +# cAIc — Environment configuration +# Copy to .env and fill in values before running docker compose. +# Secrets (lines marked 🔒) should be random — generate with: +# openssl rand -hex 20 +# ───────────────────────────────────────────────────────────── + +# ── Secrets ────────────────────────────────────────────────── +# 🔒 Admin PIN (4 digits, required for first login) +CAIC_ADMIN_PIN= +CAIC_ALLOW_DEFAULT_PIN=false + +# 🔒 API key for IDE completions endpoint (auto-gen if empty) +CAIC_COMPLETIONS_API_KEY= + +# 🔒 RabbitMQ password (must match secrets/rabbitmq_password.txt) +RABBITMQ_PASSWORD= + +# 🔒 SearXNG session key +SEARXNG_SECRET_KEY= + +# ── Service discovery (Docker service hostnames) ───────────── +LLAMA_SERVER_BASE=http://llama-server:8081 +OLLAMA_BASE=http://ollama:11434 +CAIC_SEARXNG_BASE=http://searxng:8080 +CAIC_QDRANT_URL=http://qdrant:6333 +CAIC_EMBED_URL=http://ollama:11434 +CAIC_EMBED_MODEL=all-minilm:latest +CAIC_AMQP_URL=amqp://caic:${RABBITMQ_PASSWORD}@rabbitmq:5672/caic + +# ── llama-server settings ──────────────────────────────────── +# Set this to your .gguf filename (must exist in ./models/) +LLAMA_MODEL= +LLAMA_CTX_SIZE=4096 +LLAMA_N_GPU_LAYERS=0 +LLAMA_RPC_ENDPOINTS= + +# ── RAG / Qdrant ───────────────────────────────────────────── +CAIC_RAG_COLLECTION=caic_rag +CAIC_RAG_MAX_VECTORS=50000 + +# ── Network / security ─────────────────────────────────────── +CAIC_ALLOWED_CIDRS=127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 +CAIC_TRUSTED_ORIGINS= +CAIC_TRUST_X_FORWARDED_FOR=false + +# ── Port overrides (host-side publish) ─────────────────────── +CAIC_EXPOSE_PORT=8080 +SEARXNG_EXPOSE_PORT=8888 +QDRANT_EXPOSE_PORT=6333 +RABBITMQ_EXPOSE_PORT=5672 +LLAMA_EXPOSE_PORT=8081 +OLLAMA_EXPOSE_PORT=11434 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..030e46c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# cAIc — FastAPI application +# Multi-stage build for smaller production image + +# ── Stage 1: build ────────────────────────────────────────── +FROM python:3.13-slim-bookworm AS builder +WORKDIR /build +COPY requirements.txt . +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt + +# ── Stage 2: runtime ──────────────────────────────────────── +FROM python:3.13-slim-bookworm + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /install /usr/local + +WORKDIR /app +COPY . . + +# Persist DB and uploads outside the code layer +ENV CAIC_DB_PATH=/app/data/caic.db \ + CAIC_UPLOAD_DIR=/app/data/uploads \ + CAIC_HOST=0.0.0.0 \ + CAIC_PORT=8080 \ + CAIC_SYSLOG_ADDRESS="" + +RUN mkdir -p /app/data + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD curl -fs http://localhost:8080/ || exit 1 + +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/config.py b/config.py index df3abb0..9bd1135 100644 --- a/config.py +++ b/config.py @@ -6,6 +6,7 @@ import os import re import ipaddress import logging +from pathlib import Path log = logging.getLogger("caic") @@ -76,6 +77,7 @@ BODY_LIMIT_UPLOAD_BYTES = MAX_UPLOAD_BYTES # --- RAG eviction --- RAG_MAX_VECTORS = int(os.environ.get("CAIC_RAG_MAX_VECTORS", "50000")) +HW_STATE_PATH = os.environ.get("CAIC_HW_STATE_PATH", str(Path(__file__).parent / "hardware_state.json")) RAG_EVICTION_HIGH_WATER = 0.80 RAG_EVICTION_LOW_WATER = 0.20 RAG_EVICTION_BATCH = 1000 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..024f503 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,116 @@ +# cAIc — Docker Compose stack +# Coordinator: cAIc + SearXNG + Qdrant + RabbitMQ + llama-server + Ollama +# +# Usage: +# cp .env.example .env # edit with your values +# mkdir -p models secrets searxng +# # place .gguf model in ./models/ +# docker compose up -d + +services: + # ── cAIc (FastAPI) ────────────────────────────────────── + caic: + build: . + ports: + - "${CAIC_EXPOSE_PORT:-8080}:8080" + volumes: + - caic_data:/app/data + secrets: + - rabbitmq_password + environment: + - CAIC_AMQP_SECRET_PATH=/run/secrets/rabbitmq_password + env_file: .env + depends_on: + qdrant: { condition: service_started } + rabbitmq: { condition: service_healthy } + llama-server: { condition: service_healthy } + restart: unless-stopped + + # ── SearXNG (web search) ──────────────────────────────── + searxng: + image: searxng/searxng:latest + ports: + - "${SEARXNG_EXPOSE_PORT:-8888}:8080" + volumes: + - ./searxng/settings.yml:/etc/searxng/settings.yml:ro + - searxng_config:/etc/searxng + environment: + - SEARXNG_BASE_URL=http://localhost:8888 + restart: unless-stopped + + # ── Qdrant (vector DB) ────────────────────────────────── + qdrant: + image: qdrant/qdrant:latest + ports: + - "${QDRANT_EXPOSE_PORT:-6333}:6333" + volumes: + - qdrant_storage:/qdrant/storage + restart: unless-stopped + + # ── RabbitMQ (AMQP broker) ────────────────────────────── + rabbitmq: + image: rabbitmq:4-management + ports: + - "${RABBITMQ_EXPOSE_PORT:-5672}:5672" + volumes: + - rabbitmq_data:/var/lib/rabbitmq + environment: + RABBITMQ_DEFAULT_USER: caic + RABBITMQ_DEFAULT_PASS_FILE: /run/secrets/rabbitmq_password + RABBITMQ_DEFAULT_VHOST: / + secrets: + - rabbitmq_password + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"] + interval: 15s + timeout: 5s + retries: 3 + restart: unless-stopped + + # ── llama-server (LLM inference) ──────────────────────── + llama-server: + image: ghcr.io/ggml-org/llama.cpp:server + ports: + - "${LLAMA_EXPOSE_PORT:-8081}:8081" + volumes: + - ./models:/models:ro + command: > + --model /models/${LLAMA_MODEL:?Set LLAMA_MODEL in .env} + --host 0.0.0.0 --port 8081 + --ctx-size ${LLAMA_CTX_SIZE:-4096} + --n-gpu-layers ${LLAMA_N_GPU_LAYERS:-0} + --embeddings + --logprobs + ${LLAMA_RPC_ENDPOINTS:+--rpc ${LLAMA_RPC_ENDPOINTS}} + healthcheck: + test: ["CMD", "curl", "-fs", "http://localhost:8081/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + restart: unless-stopped + + # ── Ollama (embeddings) ───────────────────────────────── + ollama: + image: ollama/ollama:latest + ports: + - "${OLLAMA_EXPOSE_PORT:-11434}:11434" + volumes: + - ollama_models:/root/.ollama + healthcheck: + test: ["CMD", "ollama", "list"] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + +volumes: + caic_data: + searxng_config: + qdrant_storage: + rabbitmq_data: + ollama_models: + +secrets: + rabbitmq_password: + file: ./secrets/rabbitmq_password.txt diff --git a/hardware.py b/hardware.py index 9dac24b..5db5f83 100644 --- a/hardware.py +++ b/hardware.py @@ -12,11 +12,11 @@ from pathlib import Path import httpx import psutil -from config import LLAMA_SERVER_BASE, SEARXNG_BASE, QDRANT_URL +from config import LLAMA_SERVER_BASE, SEARXNG_BASE, QDRANT_URL, HW_STATE_PATH log = logging.getLogger("caic") -HARDWARE_STATE_PATH = Path("hardware_state.json") +HARDWARE_STATE_PATH = Path(HW_STATE_PATH) _TIMEOUT_EXPIRED = subprocess.TimeoutExpired diff --git a/models/README.txt b/models/README.txt new file mode 100644 index 0000000..f0fb172 --- /dev/null +++ b/models/README.txt @@ -0,0 +1,9 @@ +# Models +Place .gguf model files in this directory. + +The filename must match the LLAMA_MODEL value in .env. +For example, if LLAMA_MODEL=llama3.1-8b-instruct.Q4_K_M.gguf, +place that file here. + +Download from HuggingFace: + https://huggingface.co/models?search=gguf diff --git a/requirements.txt b/requirements.txt index 732d9ba..0b23e37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ pypdf>=5.0.0 python-multipart>=0.0.9 aio-pika>=9.0.0 cryptography>=44.0.0 +psutil>=5.9.0 +jinja2>=3.1.0 diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..8f62766 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# cAIc — First-run scaffolding +# Creates secrets, config, and directories needed by docker compose. +# Idempotent: safe to re-run. +set -euo pipefail + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' + +ok() { echo -e " ${GREEN}✓${NC} $1"; } +warn() { echo -e " ${YELLOW}!${NC} $1"; } + +echo "cAIc — scaffolding" +echo "===================" + +# ── .env ──────────────────────────────────────────────────── +if [ ! -f .env ]; then + cp .env.example .env + + # Auto-generate secrets + ADMIN_PIN=$(shuf -i 1000-9999 -n 1) + API_KEY="caic-sk-$(openssl rand -hex 24)" + RMQ_PASS=$(openssl rand -hex 20) + SEARX_KEY=$(openssl rand -hex 32) + + sed -i "s/^CAIC_ADMIN_PIN=$/CAIC_ADMIN_PIN=$ADMIN_PIN/" .env + sed -i "s/^CAIC_COMPLETIONS_API_KEY=$/CAIC_COMPLETIONS_API_KEY=$API_KEY/" .env + sed -i "s/^RABBITMQ_PASSWORD=$/RABBITMQ_PASSWORD=$RMQ_PASS/" .env + sed -i "s/^SEARXNG_SECRET_KEY=$/SEARXNG_SECRET_KEY=$SEARX_KEY/" .env + + ok ".env created" + warn " Admin PIN: $ADMIN_PIN" + warn " API key: $API_KEY" +else + warn ".env already exists — skipped" +fi + +# ── secrets/ ──────────────────────────────────────────────── +mkdir -p secrets +if [ ! -f secrets/rabbitmq_password.txt ]; then + RMQ_PASS=$(grep '^RABBITMQ_PASSWORD=' .env | cut -d= -f2) + echo -n "$RMQ_PASS" > secrets/rabbitmq_password.txt + ok "secrets/rabbitmq_password.txt created" +else + warn "secrets/rabbitmq_password.txt exists — skipped" +fi + +# ── searxng/settings.yml ──────────────────────────────────── +mkdir -p searxng +if [ ! -f searxng/settings.yml ]; then + SEARX_KEY=$(grep '^SEARXNG_SECRET_KEY=' .env | cut -d= -f2) + # Substitute the secret key into the template + sed "s/\${SEARXNG_SECRET_KEY}/$SEARX_KEY/" searxng-settings.yml.dist > searxng/settings.yml + ok "searxng/settings.yml created" +else + warn "searxng/settings.yml exists — skipped" +fi + +# ── models/ ───────────────────────────────────────────────── +mkdir -p models +if [ ! "$(ls -A models/*.gguf 2>/dev/null)" ]; then + warn "No .gguf models found in ./models/" + warn " Place your model file(s) there before running docker compose up." +else + ok "models/ has $(ls models/*.gguf 2>/dev/null | wc -l) model(s)" +fi + +# ── data/ ─────────────────────────────────────────────────── +mkdir -p data +ok "data/ directory ready" + +# ── Verify Docker ─────────────────────────────────────────── +echo "" +if command -v docker &>/dev/null && docker compose version &>/dev/null; then + ok "Docker Compose available: $(docker compose version --short)" + echo "" + echo -e "${GREEN}Ready!${NC} Run: docker compose up -d" +else + warn "Docker Compose not found — install Docker Engine + Compose plugin first." + echo " https://docs.docker.com/engine/install/" +fi diff --git a/searxng-settings.yml.dist b/searxng-settings.yml.dist new file mode 100644 index 0000000..800545b --- /dev/null +++ b/searxng-settings.yml.dist @@ -0,0 +1,26 @@ +# SearXNG configuration — cAIc stack +# Secret key is injected via environment variable. + +search: + safe_search: 0 + autocomplete: "" + default_lang: en + +server: + secret_key: ${SEARXNG_SECRET_KEY} + limiter: false + image_proxy: false + method: GET + port: 8080 + bind_address: "0.0.0.0" + +engines: + - name: google + engine: google + shortcut: g + - name: duckduckgo + engine: duckduckgo + shortcut: ddg + - name: wikipedia + engine: wikipedia + shortcut: wp