From 748f05be46822748f2d11d48c27a2020098224c4 Mon Sep 17 00:00:00 2001 From: Santhosh Janardhanan Date: Mon, 13 Apr 2026 15:53:08 -0400 Subject: [PATCH] feat: Phase 6 - Docker, systemd, and installation scripts --- Dockerfile | 55 ++++++++++++++ Dockerfile.indexer | 32 +++++++++ docker-compose.yml | 76 ++++++++++++++++++++ scripts/install.ps1 | 110 ++++++++++++++++++++++++++++ scripts/install.sh | 116 ++++++++++++++++++++++++++++++ systemd/companion-api.service | 35 +++++++++ systemd/companion-index.timer | 11 +++ systemd/companion-index@.service | 22 ++++++ systemd/companion-indexer.service | 38 ++++++++++ 9 files changed, 495 insertions(+) create mode 100644 Dockerfile create mode 100644 Dockerfile.indexer create mode 100644 docker-compose.yml create mode 100644 scripts/install.ps1 create mode 100644 scripts/install.sh create mode 100644 systemd/companion-api.service create mode 100644 systemd/companion-index.timer create mode 100644 systemd/companion-index@.service create mode 100644 systemd/companion-indexer.service diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0a7b336 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# Build stage +FROM python:3.11-slim AS builder + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY pyproject.toml . +RUN pip install --no-cache-dir -e ".[dev]" \ + && pip wheel --no-cache-dir --wheel-dir /app/wheels \ + pydantic lancedb pyarrow requests watchdog typer rich numpy httpx sse-starlette fastapi uvicorn + +# Production stage +FROM python:3.11-slim AS production + +WORKDIR /app + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + && rm -rf /var/lib/apt/lists/* + +# Copy wheels and install +COPY --from=builder /app/wheels /wheels +RUN pip install --no-cache-dir /wheels/* + +# Copy application code +COPY companion/ ./companion/ +COPY companion/forge/ ./companion/forge/ +COPY companion/indexer_daemon/ ./companion/indexer_daemon/ +COPY companion/rag/ ./companion/rag/ + +# Create directories for data +RUN mkdir -p /data/vectors /data/memory /models + +# Copy default config +COPY config.json /app/config.json + +# Environment variables +ENV PYTHONPATH=/app +ENV COMPANION_CONFIG=/app/config.json +ENV COMPANION_DATA_DIR=/data + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:7373/health')" || exit 1 + +# API port +EXPOSE 7373 + +# Default command +CMD ["python", "-m", "uvicorn", "companion.api:app", "--host", "0.0.0.0", "--port", "7373"] diff --git a/Dockerfile.indexer b/Dockerfile.indexer new file mode 100644 index 0000000..6c81024 --- /dev/null +++ b/Dockerfile.indexer @@ -0,0 +1,32 @@ +# Indexer-only Dockerfile (lightweight, no API dependencies) +FROM python:3.11-slim + +WORKDIR /app + +# Install dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +RUN pip install --no-cache-dir \ + pydantic lancedb pyarrow requests watchdog typer rich numpy httpx + +# Copy application code +COPY companion/ ./companion/ +COPY companion/indexer_daemon/ ./companion/indexer_daemon/ +COPY companion/rag/ ./companion/rag/ + +# Create directories for data +RUN mkdir -p /data/vectors + +# Copy default config +COPY config.json /app/config.json + +# Environment variables +ENV PYTHONPATH=/app +ENV COMPANION_CONFIG=/app/config.json +ENV COMPANION_DATA_DIR=/data + +# Default command (can be overridden) +CMD ["python", "-m", "companion.indexer_daemon.cli", "index"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6e1f18d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,76 @@ +version: '3.8' + +services: + companion-api: + build: + context: . + dockerfile: Dockerfile + target: production + container_name: companion-api + ports: + - "7373:7373" + volumes: + - ./config.json:/app/config.json:ro + - companion-data:/data + - ./models:/models:ro + environment: + - COMPANION_CONFIG=/app/config.json + - COMPANION_DATA_DIR=/data + networks: + - companion-network + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:7373/health')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + + companion-indexer: + build: + context: . + dockerfile: Dockerfile.indexer + container_name: companion-indexer + volumes: + - ./config.json:/app/config.json:ro + - companion-data:/data + - /home/san/KnowledgeVault:/vault:ro # Mount Obsidian vault as read-only + environment: + - COMPANION_CONFIG=/app/config.json + - COMPANION_DATA_DIR=/data + - VAULT_PATH=/vault + networks: + - companion-network + restart: unless-stopped + command: ["python", "-m", "companion.indexer_daemon.watcher"] + # Or use CLI mode for manual sync: + # command: ["python", "-m", "companion.indexer_daemon.cli", "index"] + + # Optional: Ollama for local embeddings and LLM + ollama: + image: ollama/ollama:latest + container_name: companion-ollama + ports: + - "11434:11434" + volumes: + - ollama-data:/root/.ollama + networks: + - companion-network + restart: unless-stopped + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + +volumes: + companion-data: + driver: local + ollama-data: + driver: local + +networks: + companion-network: + driver: bridge diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..4960211 --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,110 @@ +#!/bin/bash +# Companion AI - Windows Installation Script + +Write-Host "Companion AI Installation Script for Windows" +Write-Host "=============================================" +Write-Host "" + +# Check if Python is installed +$python = Get-Command python -ErrorAction SilentlyContinue +if (-not $python) { + Write-Error "Python 3 is required but not found. Please install Python 3.11 or later." + exit 1 +} + +$pythonVersion = python --version 2>&1 +Write-Host "Found: $pythonVersion" + +# Create directories +$installDir = "$env:LOCALAPPDATA\Companion" +$dataDir = "$env:LOCALAPPDATA\Companion\Data" + +Write-Host "Creating directories..." +New-Item -ItemType Directory -Force -Path $installDir | Out-Null +New-Item -ItemType Directory -Force -Path $dataDir\vectors | Out-Null +New-Item -ItemType Directory -Force -Path $dataDir\memory | Out-Null +New-Item -ItemType Directory -Force -Path $dataDir\models | Out-Null + +# Copy application files +Write-Host "Installing Companion AI..." +$sourceDir = $PSScriptRoot +if (Test-Path "$sourceDir\src") { + Copy-Item -Recurse -Force "$sourceDir\*" $installDir +} else { + Write-Error "Cannot find source files. Please run from the Companion AI directory." + exit 1 +} + +# Create virtual environment +Write-Host "Creating Python virtual environment..." +Set-Location $installDir +python -m venv venv + +# Activate and install +Write-Host "Installing dependencies..." +& .\venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" + +# Copy config +if (-not (Test-Path "$dataDir\config.json")) { + Copy-Item "$installDir\config.json" "$dataDir\config.json" + # Update paths + $config = Get-Content "$dataDir\config.json" -Raw + $config = $config -replace '~/.companion', $dataDir.Replace('\', '/') + Set-Content "$dataDir\config.json" $config +} + +# Create startup scripts +Write-Host "Creating startup scripts..." + +# API server startup script +$apiScript = @" +@echo off +set PYTHONPATH=$($installDir) +set COMPANION_CONFIG=$($dataDir)\config.json +set COMPANION_DATA_DIR=$($dataDir) +cd "$($installDir)" +.\venv\Scripts\python -m uvicorn companion.api:app --host 0.0.0.0 --port 7373 +"@ +Set-Content "$installDir\start-api.bat" $apiScript + +# Indexer watcher startup script +$indexerScript = @" +@echo off +set PYTHONPATH=$($installDir) +set COMPANION_CONFIG=$($dataDir)\config.json +set COMPANION_DATA_DIR=$($dataDir) +cd "$($installDir)" +.\venv\Scripts\python -m companion.indexer_daemon.watcher +"@ +Set-Content "$installDir\start-indexer.bat" $indexerScript + +# CLI shortcut +$cliScript = @" +@echo off +set PYTHONPATH=$($installDir) +set COMPANION_CONFIG=$($dataDir)\config.json +set COMPANION_DATA_DIR=$($dataDir) +cd "$($installDir)" +.\venv\Scripts\python -m companion.indexer_daemon.cli %* +"@ +Set-Content "$installDir\companion.bat" $cliScript + +Write-Host "" +Write-Host "Installation complete!" +Write-Host "=====================" +Write-Host "" +Write-Host "To start the API server:" +Write-Host " $installDir\start-api.bat" +Write-Host "" +Write-Host "To start the file watcher (auto-indexing):" +Write-Host " $installDir\start-indexer.bat" +Write-Host "" +Write-Host "To run a manual index:" +Write-Host " $installDir\companion.bat index" +Write-Host "" +Write-Host "Config location: $dataDir\config.json" +Write-Host "Data location: $dataDir" +Write-Host "" +Write-Host "Edit the config to set your vault path, then start the services." diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..00618d8 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# Companion AI Installation Script for Linux + +set -e + +# Configuration +INSTALL_DIR="/opt/companion" +DATA_DIR="/var/lib/companion" +USER="companion" +SERVICE_USER="${USER}" +REPO_URL="https://github.com/santhoshjan/companion.git" + +echo "Companion AI Installation Script" +echo "================================" +echo "" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "Please run as root (use sudo)" + exit 1 +fi + +# Check dependencies +echo "Checking dependencies..." +if ! command -v python3 &> /dev/null; then + echo "Python 3 is required but not installed. Aborting." + exit 1 +fi + +if ! command -v pip3 &> /dev/null; then + echo "pip3 is required but not installed. Installing..." + apt-get update && apt-get install -y python3-pip +fi + +# Create user +echo "Creating companion user..." +if ! id "$USER" &>/dev/null; then + useradd -r -s /bin/false -d "$INSTALL_DIR" "$USER" +fi + +# Create directories +echo "Creating directories..." +mkdir -p "$INSTALL_DIR" +mkdir -p "$DATA_DIR"/{vectors,memory,models} +mkdir -p /etc/companion + +# Install application +echo "Installing Companion AI..." +if [ -d ".git" ]; then + # Running from git repo + cp -r . "$INSTALL_DIR/" +else + # Clone from remote + git clone "$REPO_URL" "$INSTALL_DIR" +fi + +# Create virtual environment +echo "Creating Python virtual environment..." +cd "$INSTALL_DIR" +python3 -m venv venv +source venv/bin/activate +pip install --upgrade pip +pip install -e ".[dev]" + +# Set permissions +echo "Setting permissions..." +chown -R "$USER:$USER" "$INSTALL_DIR" +chown -R "$USER:$USER" "$DATA_DIR" + +# Install systemd services +echo "Installing systemd services..." +if command -v systemctl &> /dev/null; then + cp systemd/companion-api.service /etc/systemd/system/ + cp systemd/companion-indexer.service /etc/systemd/system/ + cp systemd/companion-index@.service /etc/systemd/system/ + cp systemd/companion-index.timer /etc/systemd/system/ 2>/dev/null || true + + systemctl daemon-reload + + echo "" + echo "Services installed. To start:" + echo " sudo systemctl enable companion-api" + echo " sudo systemctl start companion-api" + echo "" + echo "For auto-indexing:" + echo " sudo systemctl enable companion-indexer" + echo " sudo systemctl start companion-indexer" +else + echo "systemd not found. Manual setup required." +fi + +# Create config if doesn't exist +if [ ! -f /etc/companion/config.json ]; then + echo "Creating default configuration..." + cp "$INSTALL_DIR/config.json" /etc/companion/config.json + # Update paths in config + sed -i "s|~/.companion|$DATA_DIR|g" /etc/companion/config.json +fi + +# Create symlink for CLI +echo "Installing CLI..." +ln -sf "$INSTALL_DIR/venv/bin/companion" /usr/local/bin/companion 2>/dev/null || true + +echo "" +echo "Installation complete!" +echo "======================" +echo "" +echo "Next steps:" +echo "1. Edit configuration: sudo nano /etc/companion/config.json" +echo "2. Update vault path in the config" +echo "3. Start services: sudo systemctl start companion-api companion-indexer" +echo "4. Check status: sudo systemctl status companion-api" +echo "" +echo "Data directory: $DATA_DIR" +echo "Config directory: /etc/companion" +echo "Install directory: $INSTALL_DIR" diff --git a/systemd/companion-api.service b/systemd/companion-api.service new file mode 100644 index 0000000..3b98fc8 --- /dev/null +++ b/systemd/companion-api.service @@ -0,0 +1,35 @@ +[Unit] +Description=Companion AI API Service +Documentation=https://github.com/santhoshjan/companion +After=network.target ollama.service +Wants=ollama.service + +[Service] +Type=simple +User=companion +Group=companion +WorkingDirectory=/opt/companion +Environment=PYTHONPATH=/opt/companion +Environment=COMPANION_CONFIG=/opt/companion/config.json +Environment=COMPANION_DATA_DIR=/var/lib/companion + +# Start the API server +ExecStart=/opt/companion/venv/bin/python -m uvicorn companion.api:app --host 0.0.0.0 --port 7373 + +# Restart on failure +Restart=on-failure +RestartSec=5 + +# Resource limits +MemoryMax=2G +CPUQuota=200% + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/lib/companion + +[Install] +WantedBy=multi-user.target diff --git a/systemd/companion-index.timer b/systemd/companion-index.timer new file mode 100644 index 0000000..f9e3fdb --- /dev/null +++ b/systemd/companion-index.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Companion AI Daily Full Index +Documentation=https://github.com/santhoshjan/companion + +[Timer] +OnCalendar=daily +OnCalendar=*-*-* 03:00:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/systemd/companion-index@.service b/systemd/companion-index@.service new file mode 100644 index 0000000..a26c2f1 --- /dev/null +++ b/systemd/companion-index@.service @@ -0,0 +1,22 @@ +[Unit] +Description=Companion AI Full Index (One-shot) +Documentation=https://github.com/santhoshjan/companion + +[Service] +Type=oneshot +User=companion +Group=companion +WorkingDirectory=/opt/companion +Environment=PYTHONPATH=/opt/companion +Environment=COMPANION_CONFIG=/opt/companion/config.json +Environment=COMPANION_DATA_DIR=/var/lib/companion + +# Run full index +ExecStart=/opt/companion/venv/bin/python -m companion.indexer_daemon.cli index + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/lib/companion diff --git a/systemd/companion-indexer.service b/systemd/companion-indexer.service new file mode 100644 index 0000000..2493499 --- /dev/null +++ b/systemd/companion-indexer.service @@ -0,0 +1,38 @@ +[Unit] +Description=Companion AI Vault Indexer +Documentation=https://github.com/santhoshjan/companion +After=network.target companion-api.service +Wants=companion-api.service + +[Service] +Type=simple +User=companion +Group=companion +WorkingDirectory=/opt/companion +Environment=PYTHONPATH=/opt/companion +Environment=COMPANION_CONFIG=/opt/companion/config.json +Environment=COMPANION_DATA_DIR=/var/lib/companion + +# Start the file watcher for auto-sync +ExecStart=/opt/companion/venv/bin/python -m companion.indexer_daemon.watcher + +# Or use the CLI for scheduled sync (uncomment to use): +# ExecStart=/opt/companion/venv/bin/python -m companion.indexer_daemon.cli sync + +# Restart on failure +Restart=on-failure +RestartSec=10 + +# Resource limits +MemoryMax=1G +CPUQuota=100% + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/lib/companion + +[Install] +WantedBy=multi-user.target