containers.nix
1 # Bob Docker containers — NixOS-managed via oci-containers 2 # All secrets injected via /run/bob-secrets/*.env files (from sops-nix) 3 # Replaces ad-hoc docker run commands 4 5 { config, pkgs, lib, ... }: 6 7 { 8 # Additional data directories 9 systemd.tmpfiles.rules = [ 10 "d /srv/bob/fish-speech 0755 root root -" 11 "d /srv/bob/fish-speech/references 0755 root root -" 12 "d /srv/bob/neo4j 0755 root root -" 13 "d /srv/bob/neo4j/data 0755 root root -" 14 "d /srv/bob/neo4j/logs 0755 root root -" 15 "d /srv/bob/openwakeword 0755 root root -" 16 "d /srv/bob/openwakeword/custom 0755 root root -" 17 "d /srv/bob/calendar 0755 root root -" 18 "d /srv/bob/digests 0755 root root -" 19 "d /srv/bob/web 0755 root root -" 20 ]; 21 22 virtualisation.oci-containers.backend = "docker"; 23 24 virtualisation.oci-containers.containers = { 25 26 # ── LLM Inference ───────────────────────────────────────────────── 27 vllm = { 28 image = "vllm/vllm-openai:latest"; 29 ports = [ "8000:8000" ]; 30 volumes = [ "/srv/bob/vllm:/root/.cache/huggingface" ]; 31 cmd = [ 32 "--model" "Qwen/Qwen3-32B-AWQ" 33 "--tensor-parallel-size" "2" 34 "--max-model-len" "16384" 35 "--gpu-memory-utilization" "0.85" 36 "--dtype" "float16" 37 "--enforce-eager" 38 "--enable-auto-tool-choice" 39 "--tool-call-parser" "hermes" 40 ]; 41 extraOptions = [ "--device=nvidia.com/gpu=0" "--device=nvidia.com/gpu=1" ]; 42 }; 43 44 # ── Embeddings ──────────────────────────────────────────────────── 45 embeddings = { 46 image = "ghcr.io/huggingface/text-embeddings-inference:86-1.7"; 47 ports = [ "8080:80" ]; 48 volumes = [ "/srv/bob/vllm:/data" ]; 49 environment = { 50 HUGGINGFACE_HUB_CACHE = "/data"; 51 USE_FLASH_ATTENTION = "True"; 52 }; 53 cmd = [ "--model-id" "BAAI/bge-m3" "--port" "80" ]; 54 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 55 }; 56 57 # ── STT ─────────────────────────────────────────────────────────── 58 faster-whisper = { 59 image = "fedirz/faster-whisper-server:latest-cuda"; 60 ports = [ "10300:8000" ]; 61 volumes = [ "/srv/bob/vllm:/root/.cache/huggingface" ]; 62 environment = { 63 WHISPER__MODEL = "Systran/faster-whisper-large-v3"; 64 WHISPER__COMPUTE_TYPE = "int8"; 65 WHISPER__INFERENCE_DEVICE = "auto"; 66 }; 67 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 68 }; 69 70 # ── TTS (Kokoro — fallback) ─────────────────────────────────────── 71 kokoro-tts = { 72 image = "ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.4"; 73 ports = [ "10400:8880" ]; 74 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 75 }; 76 77 # ── TTS (Fish Speech — Ray Porter voice clone) ──────────────────── 78 fish-speech = { 79 image = "fishaudio/fish-speech:v1.5.1"; 80 ports = [ "10600:8080" ]; 81 volumes = [ "/srv/bob/fish-speech/references:/opt/fish-speech/references" ]; 82 environment = { 83 NVIDIA_VISIBLE_DEVICES = "2"; 84 CUDA_VISIBLE_DEVICES = "0"; 85 }; 86 cmd = [ 87 "python3" "tools/api_server.py" 88 "--mode" "tts" "--half" 89 "--listen" "0.0.0.0:8080" 90 "--llama-checkpoint-path" "checkpoints/fish-speech-1.5" 91 "--decoder-checkpoint-path" "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth" 92 ]; 93 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 94 }; 95 96 # ── Wake Word ───────────────────────────────────────────────────── 97 openwakeword = { 98 image = "rhasspy/wyoming-openwakeword:latest"; 99 ports = [ "10500:10400" ]; 100 volumes = [ "/srv/bob/openwakeword/custom:/custom" ]; 101 cmd = [ 102 "--preload-model" "hey_bob" 103 "--custom-model-dir" "/custom" 104 "--threshold" "0.3" 105 "--debug-probability" 106 ]; 107 }; 108 109 # ── Knowledge Store ─────────────────────────────────────────────── 110 oxigraph = { 111 image = "ghcr.io/oxigraph/oxigraph:latest"; 112 ports = [ "7878:7878" ]; 113 volumes = [ "/srv/bob/oxigraph:/data" ]; 114 cmd = [ "serve" "--location" "/data" "--bind" "0.0.0.0:7878" ]; 115 }; 116 117 # ── Knowledge Graph (Neo4j) ─────────────────────────────────────── 118 neo4j = { 119 image = "neo4j:5-community"; 120 ports = [ "7474:7474" "7687:7687" ]; 121 volumes = [ 122 "/srv/bob/neo4j/data:/data" 123 "/srv/bob/neo4j/logs:/logs" 124 ]; 125 environment = { 126 NEO4J_PLUGINS = ''["apoc"]''; 127 }; 128 environmentFiles = [ "/run/bob-secrets/neo4j-auth.env" ]; 129 }; 130 131 # ── Home Automation ─────────────────────────────────────────────── 132 homeassistant = { 133 image = "ghcr.io/home-assistant/home-assistant:stable"; 134 volumes = [ 135 "/srv/bob/hass:/config" 136 "/etc/localtime:/etc/localtime:ro" 137 ]; 138 extraOptions = [ "--network=host" ]; 139 }; 140 141 # ── Monitoring ──────────────────────────────────────────────────── 142 grafana = { 143 image = "grafana/grafana:latest"; 144 volumes = [ "/srv/bob/grafana:/var/lib/grafana" ]; 145 environmentFiles = [ "/run/bob-secrets/grafana.env" ]; 146 extraOptions = [ "--network=host" ]; 147 }; 148 149 # ── HA → NATS Bridge ───────────────────────────────────────────── 150 ha-nats-bridge = { 151 image = "bob-ha-nats-bridge"; 152 environment = { 153 HA_URL = "ws://localhost:8123/api/websocket"; 154 NATS_URL = "nats://localhost:4222"; 155 PYTHONUNBUFFERED = "1"; 156 }; 157 environmentFiles = [ "/run/bob-secrets/ha.env" ]; 158 extraOptions = [ "--network=host" ]; 159 }; 160 161 # ── Voice Agent (Pipecat) ───────────────────────────────────────── 162 pipecat-agent = { 163 image = "bob-pipecat-agent"; 164 volumes = [ "/srv/bob/fish-speech/references:/references" ]; 165 environment = { 166 LLM_BASE_URL = "http://127.0.0.1:8000/v1"; 167 LLM_MODEL = "Qwen/Qwen3-32B-AWQ"; 168 STT_BASE_URL = "http://127.0.0.1:10300/v1"; 169 TTS_BASE_URL = "http://127.0.0.1:10400/v1"; 170 TTS_ENGINE = "fish"; 171 FISH_SPEECH_URL = "http://127.0.0.1:10600"; 172 FISH_REFERENCE_AUDIO = "/references/ray_porter.wav"; 173 FISH_REFERENCE_TEXT = "it's Bob please you're not talking to my father. The cryo eterna sales rep, the name tag identified him as Kevin, nodded and gestured toward the big placard which displayed the cryonics process in ghoulish detail."; 174 HA_URL = "http://127.0.0.1:8123"; 175 OXIGRAPH_URL = "http://127.0.0.1:7878"; 176 BOT_HOST = "0.0.0.0"; 177 BOT_PORT = "10700"; 178 REPL_URL = "http://127.0.0.1:10900"; 179 DIARIZATION_ENABLED = "true"; 180 DIARIZATION_URL = "ws://127.0.0.1:7007"; 181 SPEAKER_ID_ENABLED = "false"; 182 WAKE_WORD_ENABLED = "true"; 183 WAKE_WORD_HOST = "127.0.0.1"; 184 WAKE_WORD_PORT = "10500"; 185 WAKE_WORD_NAME = "hey_bob"; 186 WAKE_WORD_IDLE_TIMEOUT = "15.0"; 187 PYTHONUNBUFFERED = "1"; 188 }; 189 environmentFiles = [ "/run/bob-secrets/ha.env" ]; 190 extraOptions = [ "--network=host" ]; 191 }; 192 193 # ── Agent Scheduler ─────────────────────────────────────────────── 194 bob-agent-scheduler = { 195 image = "bob-agent-scheduler"; 196 environment = { 197 NATS_URL = "nats://127.0.0.1:4222"; 198 PYTHONUNBUFFERED = "1"; 199 }; 200 extraOptions = [ "--network=host" ]; 201 }; 202 203 # ── Home Keeper ─────────────────────────────────────────────────── 204 bob-home-keeper = { 205 image = "bob-home-keeper"; 206 volumes = [ "/var/run/docker.sock:/var/run/docker.sock" ]; 207 environment = { 208 NATS_URL = "nats://127.0.0.1:4222"; 209 AUTO_REMEDIATE = "true"; 210 PYTHONUNBUFFERED = "1"; 211 }; 212 extraOptions = [ "--network=host" "--device=nvidia.com/gpu=all" ]; 213 }; 214 215 # ── Morning Coordinator ─────────────────────────────────────────── 216 bob-morning-coordinator = { 217 image = "bob-morning-coordinator"; 218 environment = { 219 NATS_URL = "nats://127.0.0.1:4222"; 220 LATITUDE = "27.9506"; 221 LONGITUDE = "-82.4572"; 222 LOCATION_NAME = "Tampa, FL"; 223 PYTHONUNBUFFERED = "1"; 224 }; 225 extraOptions = [ "--network=host" ]; 226 }; 227 228 # ── Evening Coordinator ─────────────────────────────────────────── 229 bob-evening-coordinator = { 230 image = "bob-evening-coordinator"; 231 environment = { 232 NATS_URL = "nats://127.0.0.1:4222"; 233 LATITUDE = "27.9506"; 234 LONGITUDE = "-82.4572"; 235 LOCATION_NAME = "Tampa, FL"; 236 PYTHONUNBUFFERED = "1"; 237 }; 238 extraOptions = [ "--network=host" ]; 239 }; 240 241 # ── Knowledge Gardener ──────────────────────────────────────────── 242 bob-knowledge-gardener = { 243 image = "bob-knowledge-gardener"; 244 volumes = [ "/srv/bob/digests:/srv/bob/digests" ]; 245 environment = { 246 NATS_URL = "nats://127.0.0.1:4222"; 247 OXIGRAPH_URL = "http://127.0.0.1:7878"; 248 LLM_BASE_URL = "http://127.0.0.1:8000/v1"; 249 LLM_MODEL = "Qwen/Qwen3-32B-AWQ"; 250 EMBEDDER_BASE_URL = "http://127.0.0.1:8080/v1"; 251 EMBEDDER_MODEL = "BAAI/bge-m3"; 252 EMBEDDER_DIM = "1024"; 253 NEO4J_URI = "neo4j://127.0.0.1:7687"; 254 NEO4J_USER = "neo4j"; 255 GRAPHITI_ENABLED = "true"; 256 OPENAI_API_KEY = "not-needed"; 257 PYTHONUNBUFFERED = "1"; 258 }; 259 environmentFiles = [ "/run/bob-secrets/neo4j-password.env" ]; 260 extraOptions = [ "--network=host" ]; 261 }; 262 263 # ── System Sentinel ─────────────────────────────────────────────── 264 bob-system-sentinel = { 265 image = "bob-system-sentinel"; 266 volumes = [ 267 "/var/run/docker.sock:/var/run/docker.sock" 268 "/home/rig/.ssh:/root/.ssh:ro" 269 ]; 270 environment = { 271 NATS_URL = "nats://127.0.0.1:4222"; 272 PROMETHEUS_URL = "http://127.0.0.1:9090"; 273 PYTHONUNBUFFERED = "1"; 274 }; 275 extraOptions = [ "--network=host" ]; 276 }; 277 278 # ── REPL Sandbox ─────────────────────────────────────────────────── 279 bob-repl-sandbox = { 280 image = "bob-repl-sandbox"; 281 volumes = [ "/var/run/docker.sock:/var/run/docker.sock:ro" ]; 282 environment = { 283 LISTEN_PORT = "10900"; 284 EXEC_TIMEOUT = "30"; 285 PROMETHEUS_URL = "http://127.0.0.1:9090"; 286 HA_URL = "http://127.0.0.1:8123"; 287 OXIGRAPH_URL = "http://127.0.0.1:7878"; 288 NATS_URL = "nats://127.0.0.1:4222"; 289 VLLM_URL = "http://127.0.0.1:8000"; 290 PYTHONUNBUFFERED = "1"; 291 }; 292 extraOptions = [ "--network=host" ]; 293 }; 294 295 # ── Speaker Diarization (diart + CAM++) ───────────────────────────── 296 bob-diarization = { 297 image = "bob-diarization"; 298 volumes = [ 299 "/srv/bob/voice-enrollment:/srv/bob/voice-enrollment:ro" 300 "/srv/bob/diarization-cache:/root/.cache" 301 ]; 302 environment = { 303 LISTEN_HOST = "0.0.0.0"; 304 LISTEN_PORT = "7007"; 305 ENROLLMENT_DB = "/srv/bob/voice-enrollment/speakers.db"; 306 DEVICE = "cuda"; 307 PYTHONUNBUFFERED = "1"; 308 }; 309 environmentFiles = [ "/run/bob-secrets/hf.env" ]; 310 extraOptions = [ "--network=host" "--device=nvidia.com/gpu=2" ]; 311 }; 312 313 # ── Reticulum Transport + Propagation Node ───────────────────────── 314 reticulum = { 315 image = "ghcr.io/markqvist/nomadnet:master"; 316 volumes = [ 317 "/srv/bob/reticulum/config:/root/.reticulum" 318 "/srv/bob/reticulum/nomadnet:/root/.nomadnetwork" 319 ]; 320 cmd = [ "--daemon" ]; 321 extraOptions = [ "--network=host" ]; 322 }; 323 324 # ── Voice Enrollment ─────────────────────────────────────────────── 325 bob-voice-enrollment = { 326 image = "bob-voice-enrollment"; 327 ports = [ "10800:10800" ]; 328 volumes = [ "/srv/bob/voice-enrollment:/srv/bob/voice-enrollment" ]; 329 environment = { 330 DATA_DIR = "/srv/bob/voice-enrollment"; 331 LISTEN_PORT = "10800"; 332 PYTHONUNBUFFERED = "1"; 333 }; 334 }; 335 336 # ── Alert Bridge (Prometheus → NATS) ──────────────────────────────── 337 bob-alert-bridge = { 338 image = "bob-alert-bridge"; 339 environment = { 340 NATS_URL = "nats://127.0.0.1:4222"; 341 LISTEN_PORT = "9095"; 342 PYTHONUNBUFFERED = "1"; 343 }; 344 extraOptions = [ "--network=host" ]; 345 }; 346 347 # ── Calendar Bridge ─────────────────────────────────────────────── 348 bob-calendar-bridge = { 349 image = "bob-calendar-bridge"; 350 volumes = [ "/srv/bob/calendar:/srv/bob/calendar" ]; 351 environment = { 352 NATS_URL = "nats://127.0.0.1:4222"; 353 CALENDAR_BACKEND = "ics"; 354 POLL_INTERVAL = "1800"; 355 PYTHONUNBUFFERED = "1"; 356 }; 357 environmentFiles = [ "/run/bob-secrets/calendar.env" ]; 358 extraOptions = [ "--network=host" ]; 359 }; 360 }; 361 362 systemd.services = let 363 # Containers that need secrets from env files 364 mkEnvDep = name: lib.nameValuePair "docker-${name}" { 365 after = [ "bob-generate-env-files.service" ]; 366 requires = [ "bob-generate-env-files.service" ]; 367 }; 368 containers-needing-secrets = [ 369 "neo4j" "grafana" "ha-nats-bridge" "pipecat-agent" 370 "bob-knowledge-gardener" "bob-calendar-bridge" "bob-diarization" 371 ]; 372 373 # All containers should restart on failure 374 mkRestart = name: lib.nameValuePair "docker-${name}" { 375 serviceConfig = { 376 Restart = lib.mkForce "always"; 377 RestartSec = "10"; 378 }; 379 }; 380 all-containers = [ 381 "vllm" "embeddings" "faster-whisper" "kokoro-tts" "fish-speech" 382 "openwakeword" "oxigraph" "neo4j" "homeassistant" "grafana" 383 "ha-nats-bridge" "pipecat-agent" 384 "bob-agent-scheduler" "bob-home-keeper" "bob-morning-coordinator" 385 "bob-evening-coordinator" "bob-knowledge-gardener" "bob-system-sentinel" 386 "bob-calendar-bridge" "bob-alert-bridge" "bob-voice-enrollment" "reticulum" 387 "bob-diarization" "bob-repl-sandbox" 388 ]; 389 in lib.mkMerge [ 390 (builtins.listToAttrs (map mkEnvDep containers-needing-secrets)) 391 (builtins.listToAttrs (map mkRestart all-containers)) 392 ]; 393 }