/ 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    ];
 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  }