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 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
|
||||
|
||||
@@ -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 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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
aio-pika>=9.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