auralynq-rag / scripts /stack_up.sh
asdfasdfqrqwer's picture
sync: bring the Space up to the current GitHub tree
656439d
Raw
History Blame Contribute Delete
7.02 kB
#!/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://<SERVER_IP>:${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)"