#!/usr/bin/env bash # Bring up the Auralynq stack (rootless Podman, no sudo). See ADR-0012/0013/0014. # # Networking: this host's CNI generates conflists with cniVersion 1.0.0, but the # installed `firewall` plugin only supports up to 0.4.0 — so the network silently # loses container DNS. The default `podman` network also ships without the # `dnsname` plugin. We fix BOTH without sudo by rewriting the relevant conflist(s) # under ~/.config/cni/net.d to cniVersion 0.4.0 + ensuring the dnsname plugin is # present. With DNS working, services reach peers by container_name # (auralynq-qdrant / auralynq-api / auralynq-web) — see ADR-0014. set -euo pipefail cd "$(dirname "$0")/.." COMPOSE="$(./scripts/check_container_runtime.sh)" CF="compose.yml" CNI_DIR="${HOME}/.config/cni/net.d" # Patch a CNI conflist in place: pin cniVersion 0.4.0 and append dnsname if absent. patch_conflist() { local f="$1" [ -f "$f" ] || return 0 python3 - "$f" <<'PY' import json, sys p = sys.argv[1] d = json.load(open(p)) changed = False if d.get("cniVersion") != "0.4.0": d["cniVersion"] = "0.4.0"; changed = True types = [pl.get("type") for pl in d.get("plugins", [])] if "dnsname" not in types: d.setdefault("plugins", []).append( {"type": "dnsname", "domainName": "dns.podman", "capabilities": {"aliases": True}}) changed = True if changed: json.dump(d, open(p, "w"), indent=2) print(f" patched {p} -> 0.4.0 + dnsname") PY } echo "→ ensuring rootless CNI networks have DNS (no sudo)…" # The default podman network (used by podman-compose) + any project network. shopt -s nullglob for f in "${CNI_DIR}"/87-podman.conflist "${CNI_DIR}"/*podman*.conflist "${CNI_DIR}"/auralynq*.conflist; do patch_conflist "$f" done shopt -u nullglob # Prefer an exported env var (e.g. from scripts/run_local.sh) over the .env file, # matching podman-compose's own precedence so the printed URL is accurate. bind_internal="${AURALYNQ_BIND_INTERNAL:-$(grep -E '^AURALYNQ_BIND_INTERNAL=' .env 2>/dev/null | cut -d= -f2)}"; bind_internal="${bind_internal:-127.0.0.1}" https_port="${AURALYNQ_HTTPS_PORT:-$(grep -E '^AURALYNQ_HTTPS_PORT=' .env 2>/dev/null | cut -d= -f2)}"; https_port="${https_port:-8443}" # ── Host Ollama reachability (rootless) ─────────────────────────────────────── # Containers on the rootless compose bridge cannot reach host loopback services, # and Podman auto-injects host.containers.internal pointing at the bridge gateway # (10.88.0.1) — which is NOT the host in rootless mode. If the user runs Ollama # on the host, route the containers to the host's real LAN IP instead (Ollama # binds 0.0.0.0, so the bridge can reach it there). Without this the API silently # degrades to the extractive LLM fallback even when a local GPU model is present. existing_base="${AURALYNQ_LLM__BASE_URL:-$(grep -E '^AURALYNQ_LLM__BASE_URL=' .env 2>/dev/null | cut -d= -f2- || true)}" if [ -z "${existing_base}" ]; then host_ip="$(ip -4 route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -1 || true)" if [ -n "${host_ip}" ] && curl -fsS -m 2 "http://${host_ip}:11434/api/tags" >/dev/null 2>&1; then export AURALYNQ_LLM__BASE_URL="http://${host_ip}:11434" echo "→ host Ollama reachable at ${host_ip}:11434 — routing containers there (local model, not extractive)." else echo "→ host Ollama not detected — API will use its configured/auto LLM (extractive fallback if none)." fi fi # Same reasoning for a host-side vLLM server: inside the container `localhost` # is the container, so a vLLM on the host is only reachable via the LAN IP. existing_vllm="${AURALYNQ_LLM__VLLM_BASE_URL:-$(grep -E '^AURALYNQ_LLM__VLLM_BASE_URL=' .env 2>/dev/null | cut -d= -f2- || true)}" if [ -z "${existing_vllm}" ]; then vllm_port="${AURALYNQ_VLLM_PORT:-8001}" vllm_host_ip="$(ip -4 route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -1 || true)" if [ -n "${vllm_host_ip}" ]; then export AURALYNQ_LLM__VLLM_BASE_URL="http://${vllm_host_ip}:${vllm_port}/v1" if curl -fsS -m 2 "http://${vllm_host_ip}:${vllm_port}/v1/models" >/dev/null 2>&1; then echo "→ host vLLM reachable at ${vllm_host_ip}:${vllm_port} — routing containers there." fi fi fi # ── Optional NVIDIA GPU visibility for hardware detection ───────────────────── # The API's ModelFit page reports the host's real GPUs. In a rootless container # without the nvidia-container-toolkit those GPUs are invisible, so the report # would wrongly say "CPU only". nvidia-smi only needs its binary + libnvidia-ml # + the /dev/nvidia* device nodes to enumerate GPUs (no CUDA runtime, no toolkit). # When they exist on the host we generate a gitignored compose override that # read-only-mounts them into the api container. This is detection-only; inference # still runs on host Ollama. compose.gpu.yml is regenerated each start. GPU_OVERRIDE="compose.gpu.yml" gpu_args=() rm -f "$GPU_OVERRIDE" smi_path="$(command -v nvidia-smi 2>/dev/null || true)" if [ -n "$smi_path" ] && [ -e /dev/nvidiactl ]; then libml="$(ldconfig -p 2>/dev/null | grep -m1 'libnvidia-ml.so.1' | awk '{print $NF}')" libml="${libml:-/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1}" if [ -e "$libml" ]; then { echo "# Auto-generated by scripts/stack_up.sh — host NVIDIA GPU visibility for" echo "# ModelFit hardware detection (nvidia-smi only). Do not edit; regenerated." echo "services:" echo " api:" echo " devices:" for d in /dev/nvidiactl /dev/nvidia-uvm /dev/nvidia-uvm-tools /dev/nvidia0 /dev/nvidia1 /dev/nvidia2 /dev/nvidia3 /dev/nvidia4 /dev/nvidia5 /dev/nvidia6 /dev/nvidia7; do [ -e "$d" ] && echo " - \"$d\"" done echo " volumes:" echo " - \"$smi_path:/usr/bin/nvidia-smi:ro\"" echo " - \"$libml:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:ro\"" } > "$GPU_OVERRIDE" gpu_args=(-f "$GPU_OVERRIDE") echo "→ NVIDIA GPU(s) detected — exposing nvidia-smi to the API for accurate hardware detection." fi fi echo "→ starting stack…" $COMPOSE -f "$CF" "${gpu_args[@]}" up -d # The compose `up` above creates the project network at cniVersion 1.0.0, which # the older `firewall` CNI plugin rejects (noisy validation warnings on every # later podman command). Patch it down to 0.4.0 + dnsname now that it exists so # subsequent `podman ps/logs/exec` and the next `up` run clean. shopt -s nullglob for f in "${CNI_DIR}"/*_default.conflist "${CNI_DIR}"/auralynq*.conflist; do patch_conflist "$f" done shopt -u nullglob echo "✓ stack up (services resolve peers by container_name via dnsname):" echo " HTTPS : https://:${https_port} (public — Caddy TLS proxy)" echo " web/api : internal (${bind_internal} loopback), fronted by Caddy" echo " Qdrant : internal (${bind_internal} loopback)" echo " Phoenix : internal (${bind_internal} loopback)"