/ nix / modules / containers.nix
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  }