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 "d /srv/bob/home-automations 0755 root root -" 21 "d /srv/bob/grafana 0755 472 472 -" 22 "d /srv/bob/ollama 0755 root root -" 23 "d /srv/bob/firefly-iii 0755 root root -" 24 "d /srv/bob/firefly-iii/db 0755 root root -" 25 "d /srv/bob/firefly-iii/upload 0755 root root -" 26 "d /srv/backup/syncthing 0755 1000 100 -" 27 ]; 28 29 virtualisation.oci-containers.backend = "docker"; 30 31 virtualisation.oci-containers.containers = { 32 33 # ── LLM Inference ───────────────────────────────────────────────── 34 vllm = { 35 image = "vllm/vllm-openai:latest"; 36 ports = [ "8000:8000" ]; 37 volumes = [ "/srv/bob/vllm:/root/.cache/huggingface" ]; 38 cmd = [ 39 "--model" "Qwen/Qwen3-32B-AWQ" 40 "--tensor-parallel-size" "2" 41 "--max-model-len" "16384" 42 "--gpu-memory-utilization" "0.85" 43 "--dtype" "float16" 44 "--enforce-eager" 45 "--enable-auto-tool-choice" 46 "--tool-call-parser" "hermes" 47 ]; 48 extraOptions = [ "--device=nvidia.com/gpu=0" "--device=nvidia.com/gpu=1" ]; 49 }; 50 51 # ── LLM Classifier (Qwen3-8B for coordinator tiering) ───────────── 52 vllm-classifier = { 53 image = "vllm/vllm-openai:latest"; 54 ports = [ "8001:8000" ]; 55 volumes = [ "/srv/bob/vllm:/root/.cache/huggingface" ]; 56 cmd = [ 57 "--model" "Qwen/Qwen3-8B-AWQ" 58 "--max-model-len" "8192" 59 "--gpu-memory-utilization" "0.45" 60 "--dtype" "float16" 61 "--enforce-eager" 62 ]; 63 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 64 }; 65 66 # ── Embeddings ──────────────────────────────────────────────────── 67 embeddings = { 68 image = "ghcr.io/huggingface/text-embeddings-inference:86-1.7"; 69 ports = [ "8080:80" ]; 70 volumes = [ "/srv/bob/vllm:/data" ]; 71 environment = { 72 HUGGINGFACE_HUB_CACHE = "/data"; 73 USE_FLASH_ATTENTION = "True"; 74 }; 75 cmd = [ "--model-id" "BAAI/bge-m3" "--port" "80" ]; 76 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 77 }; 78 79 # ── STT ─────────────────────────────────────────────────────────── 80 faster-whisper = { 81 image = "fedirz/faster-whisper-server:latest-cuda"; 82 ports = [ "10300:8000" ]; 83 volumes = [ "/srv/bob/vllm:/root/.cache/huggingface" ]; 84 environment = { 85 WHISPER__MODEL = "Systran/faster-whisper-large-v3"; 86 WHISPER__COMPUTE_TYPE = "int8"; 87 WHISPER__INFERENCE_DEVICE = "auto"; 88 }; 89 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 90 }; 91 92 # ── TTS (Kokoro — fallback) ─────────────────────────────────────── 93 kokoro-tts = { 94 image = "ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.4"; 95 ports = [ "10400:8880" ]; 96 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 97 }; 98 99 # ── TTS (Fish Speech — Ray Porter voice clone) ──────────────────── 100 fish-speech = { 101 image = "fishaudio/fish-speech:v1.5.1"; 102 ports = [ "10600:8080" ]; 103 volumes = [ "/srv/bob/fish-speech/references:/opt/fish-speech/references" ]; 104 environment = { 105 NVIDIA_VISIBLE_DEVICES = "2"; 106 CUDA_VISIBLE_DEVICES = "0"; 107 }; 108 cmd = [ 109 "python3" "tools/api_server.py" 110 "--mode" "tts" "--half" 111 "--listen" "0.0.0.0:8080" 112 "--llama-checkpoint-path" "checkpoints/fish-speech-1.5" 113 "--decoder-checkpoint-path" "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth" 114 ]; 115 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 116 }; 117 118 # ── Wake Word ───────────────────────────────────────────────────── 119 openwakeword = { 120 image = "rhasspy/wyoming-openwakeword:latest"; 121 ports = [ "10500:10400" ]; 122 volumes = [ "/srv/bob/openwakeword/custom:/custom" ]; 123 cmd = [ 124 "--preload-model" "hey_bob" 125 "--custom-model-dir" "/custom" 126 "--threshold" "0.3" 127 "--debug-probability" 128 ]; 129 }; 130 131 # ── Knowledge Store ─────────────────────────────────────────────── 132 oxigraph = { 133 image = "ghcr.io/oxigraph/oxigraph:latest"; 134 ports = [ "7878:7878" ]; 135 volumes = [ "/srv/bob/oxigraph:/data" ]; 136 cmd = [ "serve" "--location" "/data" "--bind" "0.0.0.0:7878" ]; 137 }; 138 139 # ── Knowledge Graph (Neo4j) ─────────────────────────────────────── 140 neo4j = { 141 image = "neo4j:5-community"; 142 ports = [ "7474:7474" "7687:7687" ]; 143 volumes = [ 144 "/srv/bob/neo4j/data:/data" 145 "/srv/bob/neo4j/logs:/logs" 146 ]; 147 environment = { 148 NEO4J_PLUGINS = ''["apoc"]''; 149 }; 150 environmentFiles = [ "/run/bob-secrets/neo4j-auth.env" ]; 151 }; 152 153 # ── Home Automation ─────────────────────────────────────────────── 154 homeassistant = { 155 image = "ghcr.io/home-assistant/home-assistant:stable"; 156 volumes = [ 157 "/srv/bob/hass:/config" 158 "/etc/localtime:/etc/localtime:ro" 159 ]; 160 extraOptions = [ "--network=host" ]; 161 }; 162 163 # ── Monitoring ──────────────────────────────────────────────────── 164 grafana = { 165 image = "grafana/grafana:latest"; 166 volumes = [ "/srv/bob/grafana:/var/lib/grafana" ]; 167 environmentFiles = [ "/run/bob-secrets/grafana.env" ]; 168 extraOptions = [ "--network=host" ]; 169 }; 170 171 # ── HA → NATS Bridge ───────────────────────────────────────────── 172 ha-nats-bridge = { 173 image = "bob-ha-nats-bridge"; 174 environment = { 175 HA_URL = "ws://localhost:8123/api/websocket"; 176 NATS_URL = "nats://localhost:4222"; 177 PYTHONUNBUFFERED = "1"; 178 }; 179 environmentFiles = [ "/run/bob-secrets/ha.env" ]; 180 extraOptions = [ "--network=host" ]; 181 }; 182 183 # ── Voice Agent (Pipecat) ───────────────────────────────────────── 184 pipecat-agent = { 185 image = "bob-pipecat-agent"; 186 volumes = [ 187 "/srv/bob/fish-speech/references:/references" 188 "/home/rig/bob/secrets:/secrets:ro" 189 "/srv/bob/calendar:/srv/bob/calendar:ro" 190 ]; 191 environment = { 192 LLM_BASE_URL = "http://127.0.0.1:8000/v1"; 193 LLM_MODEL = "Qwen/Qwen3-32B-AWQ"; 194 STT_BASE_URL = "http://127.0.0.1:10300/v1"; 195 TTS_BASE_URL = "http://127.0.0.1:10400/v1"; 196 TTS_ENGINE = "fish"; 197 FISH_SPEECH_URL = "http://127.0.0.1:10600"; 198 FISH_REFERENCE_AUDIO = "/references/ray_porter.wav"; 199 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."; 200 HA_URL = "http://127.0.0.1:8123"; 201 OXIGRAPH_URL = "http://127.0.0.1:7878"; 202 NATS_URL = "nats://127.0.0.1:4222"; 203 FIREFLY_URL = "http://127.0.0.1:8181"; 204 BOT_HOST = "0.0.0.0"; 205 BOT_PORT = "10700"; 206 REPL_URL = "http://127.0.0.1:10900"; 207 DIARIZATION_ENABLED = "true"; 208 DIARIZATION_URL = "ws://127.0.0.1:7007"; 209 SPEAKER_ID_ENABLED = "false"; 210 WAKE_WORD_ENABLED = "true"; 211 WAKE_WORD_HOST = "127.0.0.1"; 212 WAKE_WORD_PORT = "10500"; 213 WAKE_WORD_NAME = "hey_bob"; 214 WAKE_WORD_IDLE_TIMEOUT = "15.0"; 215 COORDINATOR_ENABLED = "true"; 216 COORDINATOR_NATS_URL = "nats://127.0.0.1:4222"; 217 FAST_PATH_ENABLED = "true"; 218 TIMEZONE = "America/New_York"; 219 PYTHONUNBUFFERED = "1"; 220 }; 221 environmentFiles = [ "/run/bob-secrets/ha.env" "/run/bob-secrets/neo4j-password.env" "/run/bob-secrets/proxy.env" ]; 222 extraOptions = [ "--network=host" ]; 223 }; 224 225 # ── Agent Scheduler ─────────────────────────────────────────────── 226 bob-agent-scheduler = { 227 image = "bob-agent-scheduler"; 228 environment = { 229 NATS_URL = "nats://127.0.0.1:4222"; 230 TZ = "America/New_York"; 231 PYTHONUNBUFFERED = "1"; 232 }; 233 extraOptions = [ "--network=host" ]; 234 }; 235 236 # ── Home Keeper ─────────────────────────────────────────────────── 237 bob-home-keeper = { 238 image = "bob-home-keeper"; 239 volumes = [ 240 "/var/run/docker.sock:/var/run/docker.sock" 241 "/home/rig/.ssh:/root/.ssh:ro" 242 ]; 243 environment = { 244 NATS_URL = "nats://127.0.0.1:4222"; 245 AUTO_REMEDIATE = "true"; 246 TZ = "America/New_York"; 247 PYTHONUNBUFFERED = "1"; 248 }; 249 environmentFiles = [ "/run/bob-secrets/proxy.env" ]; 250 extraOptions = [ "--network=host" "--device=nvidia.com/gpu=all" ]; 251 }; 252 253 # ── Morning Coordinator ─────────────────────────────────────────── 254 bob-morning-coordinator = { 255 image = "bob-morning-coordinator"; 256 environment = { 257 NATS_URL = "nats://127.0.0.1:4222"; 258 LATITUDE = "27.9506"; 259 LONGITUDE = "-82.4572"; 260 LOCATION_NAME = "Tampa, FL"; 261 TZ = "America/New_York"; 262 TIMEZONE = "America/New_York"; 263 PYTHONUNBUFFERED = "1"; 264 }; 265 extraOptions = [ "--network=host" ]; 266 }; 267 268 # ── Evening Coordinator ─────────────────────────────────────────── 269 bob-evening-coordinator = { 270 image = "bob-evening-coordinator"; 271 environment = { 272 NATS_URL = "nats://127.0.0.1:4222"; 273 LATITUDE = "27.9506"; 274 LONGITUDE = "-82.4572"; 275 LOCATION_NAME = "Tampa, FL"; 276 TZ = "America/New_York"; 277 TIMEZONE = "America/New_York"; 278 PYTHONUNBUFFERED = "1"; 279 }; 280 extraOptions = [ "--network=host" ]; 281 }; 282 283 # ── Knowledge Gardener ──────────────────────────────────────────── 284 bob-knowledge-gardener = { 285 image = "bob-knowledge-gardener"; 286 volumes = [ "/srv/bob/digests:/srv/bob/digests" ]; 287 environment = { 288 NATS_URL = "nats://127.0.0.1:4222"; 289 OXIGRAPH_URL = "http://127.0.0.1:7878"; 290 LLM_BASE_URL = "http://127.0.0.1:8000/v1"; 291 LLM_MODEL = "Qwen/Qwen3-32B-AWQ"; 292 EMBEDDER_BASE_URL = "http://127.0.0.1:8080/v1"; 293 EMBEDDER_MODEL = "BAAI/bge-m3"; 294 EMBEDDER_DIM = "1024"; 295 NEO4J_URI = "neo4j://127.0.0.1:7687"; 296 NEO4J_USER = "neo4j"; 297 GRAPHITI_ENABLED = "true"; 298 OPENAI_API_KEY = "not-needed"; 299 TZ = "America/New_York"; 300 TIMEZONE = "America/New_York"; 301 PYTHONUNBUFFERED = "1"; 302 }; 303 environmentFiles = [ "/run/bob-secrets/neo4j-password.env" ]; 304 extraOptions = [ "--network=host" ]; 305 }; 306 307 # ── System Sentinel ─────────────────────────────────────────────── 308 bob-system-sentinel = { 309 image = "bob-system-sentinel"; 310 volumes = [ 311 "/var/run/docker.sock:/var/run/docker.sock" 312 "/home/rig/.ssh:/root/.ssh:ro" 313 ]; 314 environment = { 315 NATS_URL = "nats://127.0.0.1:4222"; 316 PROMETHEUS_URL = "http://127.0.0.1:9090"; 317 TZ = "America/New_York"; 318 PYTHONUNBUFFERED = "1"; 319 }; 320 extraOptions = [ "--network=host" ]; 321 }; 322 323 # ── REPL Sandbox ─────────────────────────────────────────────────── 324 bob-repl-sandbox = { 325 image = "bob-repl-sandbox"; 326 volumes = [ "/var/run/docker.sock:/var/run/docker.sock:ro" ]; 327 environment = { 328 LISTEN_PORT = "10900"; 329 EXEC_TIMEOUT = "30"; 330 PROMETHEUS_URL = "http://127.0.0.1:9090"; 331 HA_URL = "http://127.0.0.1:8123"; 332 OXIGRAPH_URL = "http://127.0.0.1:7878"; 333 NATS_URL = "nats://127.0.0.1:4222"; 334 VLLM_URL = "http://127.0.0.1:8000"; 335 PYTHONUNBUFFERED = "1"; 336 }; 337 extraOptions = [ "--network=host" ]; 338 }; 339 340 # ── Speaker Diarization (diart + CAM++) ───────────────────────────── 341 bob-diarization = { 342 image = "bob-diarization"; 343 volumes = [ 344 "/srv/bob/voice-enrollment:/srv/bob/voice-enrollment:ro" 345 "/srv/bob/diarization-cache:/root/.cache" 346 ]; 347 environment = { 348 LISTEN_HOST = "0.0.0.0"; 349 LISTEN_PORT = "7007"; 350 ENROLLMENT_DB = "/srv/bob/voice-enrollment/speakers.db"; 351 DEVICE = "cuda"; 352 PYTHONUNBUFFERED = "1"; 353 }; 354 environmentFiles = [ "/run/bob-secrets/hf.env" ]; 355 extraOptions = [ "--network=host" "--device=nvidia.com/gpu=2" ]; 356 }; 357 358 # ── Reticulum Transport + Propagation Node ───────────────────────── 359 reticulum = { 360 image = "ghcr.io/markqvist/nomadnet:master"; 361 volumes = [ 362 "/srv/bob/reticulum/config:/root/.reticulum" 363 "/srv/bob/reticulum/nomadnet:/root/.nomadnetwork" 364 ]; 365 cmd = [ "--daemon" ]; 366 extraOptions = [ "--network=host" ]; 367 }; 368 369 # ── LXMF Bridge (Reticulum text messaging for reMarkable) ───────── 370 bob-lxmf-bridge = { 371 image = "bob-lxmf-bridge"; 372 volumes = [ 373 "/srv/bob/reticulum/config:/root/.reticulum:ro" 374 "/srv/bob/lxmf-bridge:/data" 375 ]; 376 environment = { 377 NATS_URL = "nats://127.0.0.1:4222"; 378 RETICULUM_CONFIG = "/root/.reticulum"; 379 IDENTITY_PATH = "/data/bob_lxmf_identity"; 380 BOB_DISPLAY_NAME = "Bob"; 381 TZ = "America/New_York"; 382 PYTHONUNBUFFERED = "1"; 383 }; 384 extraOptions = [ "--network=host" ]; 385 }; 386 387 # ── Voice Enrollment ─────────────────────────────────────────────── 388 bob-voice-enrollment = { 389 image = "bob-voice-enrollment"; 390 volumes = [ 391 "/srv/bob/voice-enrollment:/srv/bob/voice-enrollment" 392 "/var/run/docker.sock:/var/run/docker.sock" 393 "/tmp/openwakeword-training:/tmp/openwakeword-training" 394 "/srv/bob/openwakeword/custom:/srv/bob/openwakeword/custom" 395 ]; 396 environment = { 397 DATA_DIR = "/srv/bob/voice-enrollment"; 398 LISTEN_PORT = "10800"; 399 TRAIN_OUTPUT_DIR = "/srv/bob/openwakeword/custom"; 400 PYTHONUNBUFFERED = "1"; 401 }; 402 extraOptions = [ "--network=host" ]; 403 }; 404 405 # ── Alert Bridge (Prometheus → NATS) ──────────────────────────────── 406 bob-alert-bridge = { 407 image = "bob-alert-bridge"; 408 environment = { 409 NATS_URL = "nats://127.0.0.1:4222"; 410 LISTEN_PORT = "9095"; 411 PYTHONUNBUFFERED = "1"; 412 }; 413 extraOptions = [ "--network=host" ]; 414 }; 415 416 # ── Calendar Bridge ─────────────────────────────────────────────── 417 bob-calendar-bridge = { 418 image = "bob-calendar-bridge"; 419 volumes = [ "/srv/bob/calendar:/srv/bob/calendar" ]; 420 environment = { 421 NATS_URL = "nats://127.0.0.1:4222"; 422 CALENDAR_BACKEND = "ics"; 423 POLL_INTERVAL = "1800"; 424 TZ = "America/New_York"; 425 TIMEZONE = "America/New_York"; 426 PYTHONUNBUFFERED = "1"; 427 }; 428 environmentFiles = [ "/run/bob-secrets/calendar.env" ]; 429 extraOptions = [ "--network=host" ]; 430 }; 431 432 # ── Coordinator (request classifier + model router) ─────────────── 433 bob-coordinator = { 434 image = "bob-coordinator"; 435 volumes = [ "/home/rig/.ssh:/root/.ssh:ro" ]; 436 environment = { 437 NATS_URL = "nats://127.0.0.1:4222"; 438 CLASSIFIER_URL = "http://127.0.0.1:8001/v1"; 439 CLASSIFIER_MODEL = "Qwen/Qwen3-8B-AWQ"; 440 PRIMARY_LLM_URL = "http://127.0.0.1:8000/v1"; 441 PRIMARY_LLM_MODEL = "Qwen/Qwen3-32B-AWQ"; 442 HA_URL = "http://127.0.0.1:8123"; 443 OXIGRAPH_URL = "http://127.0.0.1:7878"; 444 REPL_URL = "http://127.0.0.1:10900"; 445 METRICS_PORT = "8002"; 446 PYTHONUNBUFFERED = "1"; 447 }; 448 environmentFiles = [ "/run/bob-secrets/ha.env" "/run/bob-secrets/proxy.env" ]; 449 extraOptions = [ "--network=host" ]; 450 }; 451 452 # ── News Aggregator (RSS + NWS weather alerts) ──────────────────── 453 bob-news-aggregator = { 454 image = "bob-news-aggregator"; 455 environment = { 456 NATS_URL = "nats://127.0.0.1:4222"; 457 LATITUDE = "27.9506"; 458 LONGITUDE = "-82.4572"; 459 NWS_ZONE = "FLZ151"; 460 PYTHONUNBUFFERED = "1"; 461 }; 462 extraOptions = [ "--network=host" ]; 463 }; 464 465 # ── Home Automations (rule engine) ──────────────────────────────── 466 bob-home-automations = { 467 image = "bob-home-automations"; 468 volumes = [ "/srv/bob/home-automations:/data" ]; 469 environment = { 470 NATS_URL = "nats://127.0.0.1:4222"; 471 HA_URL = "http://127.0.0.1:8123"; 472 LLM_URL = "http://127.0.0.1:8000/v1"; 473 LLM_MODEL = "Qwen/Qwen3-32B-AWQ"; 474 TZ = "America/New_York"; 475 TIMEZONE = "America/New_York"; 476 PYTHONUNBUFFERED = "1"; 477 }; 478 environmentFiles = [ "/run/bob-secrets/ha.env" ]; 479 extraOptions = [ "--network=host" ]; 480 }; 481 482 # ── Device Health (SSH checks across family devices) ────────────── 483 bob-device-health = { 484 image = "bob-device-health"; 485 volumes = [ "/home/rig/.ssh:/root/.ssh:ro" ]; 486 environment = { 487 NATS_URL = "nats://127.0.0.1:4222"; 488 PYTHONUNBUFFERED = "1"; 489 }; 490 extraOptions = [ "--network=host" ]; 491 }; 492 493 # ── Network Discovery (subnet scanner) ──────────────────────────── 494 bob-network-discovery = { 495 image = "bob-network-discovery"; 496 environment = { 497 NATS_URL = "nats://127.0.0.1:4222"; 498 SUBNET = "192.168.1.0/24"; 499 PYTHONUNBUFFERED = "1"; 500 }; 501 extraOptions = [ "--network=host" ]; 502 }; 503 504 # ── Syncthing (file replication) ───────────────────────────────── 505 syncthing = { 506 image = "syncthing/syncthing:latest"; 507 ports = [ "8384:8384" "22000:22000/tcp" "22000:22000/udp" ]; 508 volumes = [ 509 "/srv/backup/syncthing:/var/syncthing/config" 510 "/srv/bob:/data/srv-bob" 511 "/srv/backup:/data/backup" 512 "/home/rig/bob:/data/bob" 513 ]; 514 environment = { 515 PUID = "1000"; 516 PGID = "100"; 517 }; 518 }; 519 520 # ── Ollama (vision model, GPU 2 time-shared) ───────────────────── 521 ollama = { 522 image = "ollama/ollama:latest"; 523 ports = [ "11434:11434" ]; 524 volumes = [ "/srv/bob/ollama:/root/.ollama" ]; 525 extraOptions = [ "--device=nvidia.com/gpu=2" ]; 526 }; 527 528 # ── Firefly III (financial management) ──────────────────────────── 529 firefly-db = { 530 image = "mariadb:lts"; 531 volumes = [ "/srv/bob/firefly-iii/db:/var/lib/mysql" ]; 532 environmentFiles = [ "/run/bob-secrets/firefly-db.env" ]; 533 extraOptions = [ "--network=host" ]; 534 }; 535 536 firefly-iii = { 537 image = "fireflyiii/core:latest"; 538 volumes = [ "/srv/bob/firefly-iii/upload:/var/www/html/storage/upload" ]; 539 environment = { 540 DB_HOST = "127.0.0.1"; 541 DB_PORT = "3306"; 542 DB_CONNECTION = "mysql"; 543 APP_URL = "http://rig.lan:8181"; 544 NGINX_HTTP_PORT = "8181"; 545 TRUSTED_PROXIES = "**"; 546 TZ = "America/New_York"; 547 }; 548 environmentFiles = [ "/run/bob-secrets/firefly-app.env" ]; 549 dependsOn = [ "firefly-db" ]; 550 extraOptions = [ 551 "--network=host" 552 "--health-cmd=curl -sf http://localhost:8181/login || exit 1" 553 "--health-interval=30s" 554 "--health-timeout=5s" 555 "--health-retries=3" 556 ]; 557 }; 558 559 # ── Announce Player (TTS announcements on rig speakers) ─────────── 560 bob-announce-player = { 561 image = "bob-announce-player"; 562 volumes = [ "/run/user/1000/pulse:/run/user/1000/pulse:ro" ]; 563 environment = { 564 NATS_URL = "nats://127.0.0.1:4222"; 565 TTS_URL = "http://127.0.0.1:10400"; 566 TTS_VOICE = "am_michael"; 567 DEVICE_NAME = "greatroom"; 568 PULSE_SERVER = "unix:/run/user/1000/pulse/native"; 569 PYTHONUNBUFFERED = "1"; 570 }; 571 extraOptions = [ "--network=host" ]; 572 }; 573 }; 574 575 systemd.services = let 576 # Containers that need secrets from env files 577 mkEnvDep = name: lib.nameValuePair "docker-${name}" { 578 after = [ "bob-generate-env-files.service" ]; 579 requires = [ "bob-generate-env-files.service" ]; 580 }; 581 containers-needing-secrets = [ 582 "neo4j" "grafana" "ha-nats-bridge" "pipecat-agent" 583 "bob-knowledge-gardener" "bob-calendar-bridge" "bob-diarization" 584 "bob-coordinator" "bob-home-automations" 585 "firefly-db" "firefly-iii" "bob-home-keeper" 586 ]; 587 588 # All containers should restart on failure 589 mkRestart = name: lib.nameValuePair "docker-${name}" { 590 serviceConfig = { 591 Restart = lib.mkForce "always"; 592 RestartSec = "10"; 593 }; 594 }; 595 all-containers = [ 596 "vllm" "vllm-classifier" "embeddings" "faster-whisper" "kokoro-tts" "fish-speech" 597 "openwakeword" "oxigraph" "neo4j" "homeassistant" "grafana" 598 "ha-nats-bridge" "pipecat-agent" 599 "bob-agent-scheduler" "bob-home-keeper" "bob-morning-coordinator" 600 "bob-evening-coordinator" "bob-knowledge-gardener" "bob-system-sentinel" 601 "bob-calendar-bridge" "bob-alert-bridge" "bob-voice-enrollment" "reticulum" 602 "bob-diarization" "bob-repl-sandbox" "bob-lxmf-bridge" 603 "bob-coordinator" "bob-news-aggregator" "bob-home-automations" 604 "bob-device-health" "bob-network-discovery" "bob-announce-player" 605 "syncthing" "ollama" "firefly-db" "firefly-iii" 606 ]; 607 in lib.mkMerge [ 608 (builtins.listToAttrs (map mkEnvDep containers-needing-secrets)) 609 (builtins.listToAttrs (map mkRestart all-containers)) 610 ]; 611 }