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:
@@ -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/
|
||||||
@@ -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
@@ -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"]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user