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
This commit is contained in:
gramps
2026-07-19 16:25:57 -07:00
parent 54cca366a4
commit 8fd6c99ccc
10 changed files with 370 additions and 2 deletions
+44
View File
@@ -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/
+53
View File
@@ -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
+36
View File
@@ -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"]
+2
View File
@@ -6,6 +6,7 @@ import os
import re import re
import ipaddress import ipaddress
import logging import logging
from pathlib import Path
log = logging.getLogger("caic") log = logging.getLogger("caic")
@@ -76,6 +77,7 @@ BODY_LIMIT_UPLOAD_BYTES = MAX_UPLOAD_BYTES
# --- RAG eviction --- # --- RAG eviction ---
RAG_MAX_VECTORS = int(os.environ.get("CAIC_RAG_MAX_VECTORS", "50000")) 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_HIGH_WATER = 0.80
RAG_EVICTION_LOW_WATER = 0.20 RAG_EVICTION_LOW_WATER = 0.20
RAG_EVICTION_BATCH = 1000 RAG_EVICTION_BATCH = 1000
+116
View File
@@ -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
+2 -2
View File
@@ -12,11 +12,11 @@ from pathlib import Path
import httpx import httpx
import psutil 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") log = logging.getLogger("caic")
HARDWARE_STATE_PATH = Path("hardware_state.json") HARDWARE_STATE_PATH = Path(HW_STATE_PATH)
_TIMEOUT_EXPIRED = subprocess.TimeoutExpired _TIMEOUT_EXPIRED = subprocess.TimeoutExpired
+9
View File
@@ -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
+2
View File
@@ -5,3 +5,5 @@ pypdf>=5.0.0
python-multipart>=0.0.9 python-multipart>=0.0.9
aio-pika>=9.0.0 aio-pika>=9.0.0
cryptography>=44.0.0 cryptography>=44.0.0
psutil>=5.9.0
jinja2>=3.1.0
+80
View File
@@ -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
+26
View File
@@ -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