#!/usr/bin/env bash set -euo pipefail # deploy-docker.sh — Deploy Stonks Oracle to a Docker host via SSH # # Usage: bash deploy-docker.sh [OPTIONS] # # Options: # --host USER@HOST SSH target (default: celes@192.168.42.254) # --ollama-url URL Ollama API URL (default: auto-detect or install) # --ollama-model MODEL Ollama model name (default: qwen3.5:9b-fast) # --dir PATH Remote install directory (default: ~/stonks-oracle) # # Examples: # bash deploy-docker.sh # bash deploy-docker.sh --ollama-url http://10.1.1.12:2701 --ollama-model qwen3.6 # bash deploy-docker.sh --host user@myserver --dir /opt/stonks # ------------------------------------------------------- # Configuration (override via flags or environment) # ------------------------------------------------------- REMOTE_HOST="${DEPLOY_HOST:-celes@192.168.42.254}" REMOTE_DIR="${DEPLOY_DIR:-/home/celes/stonks-oracle}" OLLAMA_URL="${DEPLOY_OLLAMA_URL:-}" OLLAMA_MODEL="${DEPLOY_OLLAMA_MODEL:-qwen3.5:9b-fast}" REPO_URL="http://admin:St0nks0racl3!@10.1.1.12:30300/admin/stonks-oracle.git" # Parse command-line flags while [[ $# -gt 0 ]]; do case $1 in --host) REMOTE_HOST="$2"; shift 2 ;; --ollama-url) OLLAMA_URL="$2"; shift 2 ;; --ollama-model) OLLAMA_MODEL="$2"; shift 2 ;; --dir) REMOTE_DIR="$2"; shift 2 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done echo "=== Stonks Oracle Docker Deployment ===" echo " Target: ${REMOTE_HOST}:${REMOTE_DIR}" echo " Model: ${OLLAMA_MODEL}" echo " Ollama: Docker container (GPU-accelerated)" echo "" # ------------------------------------------------------- # Step 0: Ensure prerequisites (multi-distro support) # ------------------------------------------------------- echo "--- Step 0: Checking prerequisites ---" ssh "$REMOTE_HOST" bash -s <<'REMOTE_SCRIPT' set -euo pipefail # --- Detect OS and package manager --- detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release OS_ID="${ID:-unknown}" OS_LIKE="${ID_LIKE:-$OS_ID}" elif [ -f /etc/redhat-release ]; then OS_ID="rhel" OS_LIKE="rhel" else OS_ID="unknown" OS_LIKE="unknown" fi # Detect WSL IS_WSL=false if grep -qi microsoft /proc/version 2>/dev/null; then IS_WSL=true fi # Determine package manager if command -v apt-get &>/dev/null; then PKG_MGR="apt" elif command -v dnf &>/dev/null; then PKG_MGR="dnf" elif command -v yum &>/dev/null; then PKG_MGR="yum" elif command -v pacman &>/dev/null; then PKG_MGR="pacman" elif command -v zypper &>/dev/null; then PKG_MGR="zypper" else PKG_MGR="unknown" fi echo " Detected: OS=$OS_ID (like=$OS_LIKE), pkg=$PKG_MGR, WSL=$IS_WSL" } install_pkg() { local pkg="$1" case "$PKG_MGR" in apt) sudo apt-get install -y "$pkg" ;; dnf) sudo dnf -y install "$pkg" ;; yum) sudo yum -y install "$pkg" ;; pacman) sudo pacman -S --noconfirm "$pkg" ;; zypper) sudo zypper install -y "$pkg" ;; *) echo " ERROR: Unknown package manager"; exit 1 ;; esac } update_pkg_cache() { case "$PKG_MGR" in apt) sudo apt-get update -qq ;; dnf|yum) ;; # dnf/yum auto-refresh pacman) sudo pacman -Sy ;; zypper) sudo zypper refresh -q ;; esac } detect_os # --- Git --- if ! command -v git &>/dev/null; then echo " Installing git..." update_pkg_cache install_pkg git echo " ✓ Git installed" else echo " ✓ Git present" fi # --- Docker Engine --- if command -v docker &>/dev/null && docker info &>/dev/null; then echo " ✓ Docker already installed ($(docker --version | cut -d' ' -f3 | tr -d ','))" else echo " Installing Docker CE..." case "$PKG_MGR" in apt) # Debian/Ubuntu/WSL sudo apt-get update -qq sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/${OS_ID}/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null sudo chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${OS_ID} $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -qq sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ;; dnf|yum) # RHEL/Rocky/Fedora/CentOS sudo "$PKG_MGR" -y install dnf-plugins-core 2>/dev/null || true local repo_distro="rhel" if [[ "$OS_ID" == "fedora" ]]; then repo_distro="fedora"; fi sudo dnf config-manager --add-repo "https://download.docker.com/linux/${repo_distro}/docker-ce.repo" 2>/dev/null || \ sudo yum-config-manager --add-repo "https://download.docker.com/linux/${repo_distro}/docker-ce.repo" 2>/dev/null sudo "$PKG_MGR" -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ;; pacman) # Arch Linux sudo pacman -S --noconfirm docker docker-compose docker-buildx ;; zypper) # openSUSE sudo zypper install -y docker docker-compose docker-buildx ;; esac sudo systemctl enable --now docker 2>/dev/null || true sudo usermod -aG docker "$(whoami)" 2>/dev/null || true echo " ✓ Docker installed and started" fi # --- Docker Compose plugin --- if docker compose version &>/dev/null; then echo " ✓ Docker Compose plugin available ($(docker compose version --short))" else echo " ERROR: docker compose plugin not found after Docker install" exit 1 fi # --- NVIDIA Driver (skip on WSL — uses host driver) --- if [ "$IS_WSL" = "true" ]; then echo " ✓ WSL detected — using host Windows NVIDIA driver" elif ! command -v nvidia-smi &>/dev/null; then echo " Installing NVIDIA drivers..." case "$PKG_MGR" in apt) sudo apt-get install -y nvidia-driver-560 2>/dev/null || \ sudo apt-get install -y nvidia-driver 2>/dev/null || \ echo " ⚠ NVIDIA driver install failed — install manually" ;; dnf|yum) sudo dnf -y install epel-release 2>/dev/null || true sudo dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel9/x86_64/cuda-rhel9.repo 2>/dev/null || true sudo dnf -y module install nvidia-driver:latest-dkms 2>/dev/null || \ echo " ⚠ NVIDIA driver install failed — install manually" ;; pacman) sudo pacman -S --noconfirm nvidia nvidia-utils 2>/dev/null || \ echo " ⚠ NVIDIA driver install failed — install manually" ;; zypper) echo " ⚠ NVIDIA driver: install manually for openSUSE" ;; esac else echo " ✓ NVIDIA driver present ($(nvidia-smi --query-gpu=driver_version --format=csv,noheader | head -1))" fi # --- NVIDIA Container Toolkit --- if command -v nvidia-ctk &>/dev/null; then echo " ✓ NVIDIA Container Toolkit already installed" elif [ "$IS_WSL" = "true" ] && docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi &>/dev/null 2>&1; then echo " ✓ WSL GPU passthrough working (no nvidia-ctk needed)" else echo " Installing NVIDIA Container Toolkit..." case "$PKG_MGR" in apt) curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg 2>/dev/null curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list > /dev/null sudo apt-get update -qq sudo apt-get install -y nvidia-container-toolkit ;; dnf|yum) curl -fsSL https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | \ sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo > /dev/null sudo "$PKG_MGR" -y install nvidia-container-toolkit ;; pacman) sudo pacman -S --noconfirm nvidia-container-toolkit 2>/dev/null || \ echo " ⚠ Install nvidia-container-toolkit from AUR" ;; zypper) echo " ⚠ NVIDIA Container Toolkit: install manually for openSUSE" ;; esac sudo nvidia-ctk runtime configure --runtime=docker 2>/dev/null || true sudo systemctl restart docker 2>/dev/null || true echo " ✓ NVIDIA Container Toolkit installed and Docker configured" fi # --- Verify GPU is accessible from Docker --- if docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi &>/dev/null 2>&1; then echo " ✓ GPU passthrough verified" else echo " ⚠ GPU passthrough test failed — may need a reboot or manual NVIDIA setup" fi # --- Firewall (open required ports if firewall is active) --- if command -v firewall-cmd &>/dev/null && systemctl is-active firewalld &>/dev/null; then echo " Configuring firewalld..." for port in 3000 8001 8002 8003 8004 9000 9001 11434; do sudo firewall-cmd --permanent --add-port="${port}/tcp" 2>/dev/null || true done sudo firewall-cmd --reload 2>/dev/null || true echo " ✓ Firewall ports opened" elif command -v ufw &>/dev/null && sudo ufw status 2>/dev/null | grep -q "active"; then echo " Configuring ufw..." for port in 3000 8001 8002 8003 8004 9000 9001 11434; do sudo ufw allow "${port}/tcp" 2>/dev/null || true done echo " ✓ UFW ports opened" fi REMOTE_SCRIPT echo "" # ------------------------------------------------------- # Step 1: Clone or update the repo on the remote host # ------------------------------------------------------- echo "--- Step 1: Syncing repository ---" ssh "$REMOTE_HOST" bash -s -- "$REMOTE_DIR" "$REPO_URL" <<'REMOTE_SCRIPT' set -euo pipefail REMOTE_DIR="$1" REPO_URL="$2" if [ -d "$REMOTE_DIR/.git" ]; then echo " Updating existing repo..." cd "$REMOTE_DIR" git fetch origin git reset --hard origin/main else echo " Cloning fresh..." git clone "$REPO_URL" "$REMOTE_DIR" cd "$REMOTE_DIR" fi echo " ✓ Repo synced at $(git log --oneline -1)" REMOTE_SCRIPT echo "" # ------------------------------------------------------- # Step 2: Detect or configure Ollama # ------------------------------------------------------- echo "--- Step 2: Configuring Ollama ---" # Always use the Docker Ollama container with GPU passthrough # The ollama/ollama image ships with CUDA runtime built-in USE_DOCKER_OLLAMA=true OLLAMA_URL="http://ollama:11434" echo " Using Docker Ollama container (GPU-accelerated via NVIDIA passthrough)" echo " Host-accessible at localhost:11434" echo "" # ------------------------------------------------------- # Step 3: Create .env and compose override # ------------------------------------------------------- echo "--- Step 3: Configuring environment ---" ssh "$REMOTE_HOST" bash -s -- "$REMOTE_DIR" "$OLLAMA_URL" "$OLLAMA_MODEL" "$USE_DOCKER_OLLAMA" <<'REMOTE_SCRIPT' set -euo pipefail REMOTE_DIR="$1" OLLAMA_URL="$2" OLLAMA_MODEL="$3" USE_DOCKER_OLLAMA="$4" cd "$REMOTE_DIR" # Read API keys from local files if they exist POLYGON_KEY="" ALPACA_KEY="" ALPACA_SECRET="" ALPACA_URL="https://paper-api.alpaca.markets" [ -f polygon.io.key ] && POLYGON_KEY=$(cat polygon.io.key) [ -f alpaca.key ] && ALPACA_KEY=$(cat alpaca.key) [ -f alpaca.secret ] && ALPACA_SECRET=$(cat alpaca.secret) [ -f alpaca.url ] && ALPACA_URL=$(cat alpaca.url) cat > .env < docker-compose.override.yml < docker-compose.override.yml </dev/null || true # Build all images echo " Building images (this may take a few minutes)..." docker compose build --quiet 2>&1 | tail -5 # Start infrastructure echo " Starting infrastructure..." if [ "$USE_DOCKER_OLLAMA" = "true" ]; then docker compose up -d postgres redis minio minio-init ollama else docker compose up -d postgres redis minio minio-init fi # Wait for infrastructure to be healthy echo " Waiting for infrastructure health checks..." for svc in postgres redis minio; do for i in $(seq 1 30); do if docker compose ps "$svc" 2>/dev/null | grep -q healthy; then break fi sleep 2 done done echo " ✓ Infrastructure healthy" # Start all application services echo " Starting application services..." docker compose up -d echo " Waiting for services to stabilize..." sleep 20 # Show status echo "" echo " Service Status:" docker compose ps --format "table {{.Name}}\t{{.Status}}" 2>/dev/null | head -25 || docker compose ps REMOTE_SCRIPT echo "" # ------------------------------------------------------- # Step 5: Seed the database # ------------------------------------------------------- echo "--- Step 5: Seeding database ---" ssh "$REMOTE_HOST" bash -s -- "$REMOTE_DIR" <<'REMOTE_SCRIPT' set -euo pipefail cd "$1" # Wait for query-api to be healthy for i in $(seq 1 30); do if docker compose ps query-api 2>/dev/null | grep -q healthy; then break fi sleep 3 done # Run the symbol registry seed echo " Seeding symbol registry..." docker compose exec -T scheduler python -m services.symbol_registry.seed 2>/dev/null && echo " ✓ Database seeded" || echo " ⚠ Seed skipped (may already be seeded or service not ready)" REMOTE_SCRIPT echo "" # ------------------------------------------------------- # Step 6: Ensure Ollama model is available # ------------------------------------------------------- echo "--- Step 6: Checking Ollama model ---" ssh "$REMOTE_HOST" bash -s -- "$OLLAMA_URL" "$OLLAMA_MODEL" "$USE_DOCKER_OLLAMA" "$REMOTE_DIR" <<'REMOTE_SCRIPT' set -euo pipefail OLLAMA_URL="$1" OLLAMA_MODEL="$2" USE_DOCKER_OLLAMA="$3" REMOTE_DIR="$4" if [ "$USE_DOCKER_OLLAMA" = "true" ]; then # Pull via Docker container cd "$REMOTE_DIR" if docker compose exec -T ollama ollama list 2>/dev/null | grep -q "$OLLAMA_MODEL"; then echo " ✓ Model $OLLAMA_MODEL already available" else echo " Pulling $OLLAMA_MODEL via Docker Ollama..." docker compose exec -T ollama ollama pull "$OLLAMA_MODEL" echo " ✓ Model pulled" fi else # Check via API if curl -sf "$OLLAMA_URL/api/tags" 2>/dev/null | grep -q "$OLLAMA_MODEL"; then echo " ✓ Model $OLLAMA_MODEL already available at $OLLAMA_URL" else echo " Pulling $OLLAMA_MODEL via $OLLAMA_URL..." curl -sf "$OLLAMA_URL/api/pull" -d "{\"name\":\"$OLLAMA_MODEL\"}" | tail -1 echo " ✓ Model pulled" fi fi REMOTE_SCRIPT echo "" # ------------------------------------------------------- # Done # ------------------------------------------------------- REMOTE_IP=$(echo "$REMOTE_HOST" | cut -d@ -f2) echo "=== Deployment Complete ===" echo "" echo "Endpoints:" echo " Dashboard: http://${REMOTE_IP}:3000" echo " Query API: http://${REMOTE_IP}:8004" echo " Symbol Registry: http://${REMOTE_IP}:8001" echo " Trading Engine: http://${REMOTE_IP}:8002" echo " Risk Engine: http://${REMOTE_IP}:8003" echo " MinIO Console: http://${REMOTE_IP}:9001" echo " Superset: http://${REMOTE_IP}:8088" echo " Ollama: http://${REMOTE_IP}:11434" echo "" echo "Commands:" echo " ssh $REMOTE_HOST 'cd $REMOTE_DIR && docker compose logs -f'" echo " ssh $REMOTE_HOST 'cd $REMOTE_DIR && docker compose ps'" echo " ssh $REMOTE_HOST 'cd $REMOTE_DIR && docker compose down'"