1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 inherit (lib)
10 any
11 attrValues
12 converge
13 elem
14 filterAttrsRecursive
15 hasPrefix
16 literalExpression
17 makeLibraryPath
18 mkDefault
19 mkEnableOption
20 mkPackageOption
21 mkIf
22 mkOption
23 optionalAttrs
24 optionals
25 types
26 ;
27
28 cfg = config.services.frigate;
29
30 format = pkgs.formats.yaml { };
31
32 filteredConfig = converge (filterAttrsRecursive (_: v: !elem v [ null ])) cfg.settings;
33
34 configFileUnchecked = format.generate "frigate.yaml" filteredConfig;
35 configFileChecked =
36 pkgs.runCommand "frigate-config"
37 {
38 preferLocalBuilds = true;
39 }
40 ''
41 function error() {
42 cat << 'HEREDOC'
43
44 Note that not all configurations can be reliably checked in the
45 build sandbox.
46
47 This check can be disabled using `services.frigate.checkConfig`.
48 HEREDOC
49
50 exit 1
51 }
52
53 cp ${configFileUnchecked} $out
54 export CONFIG_FILE=$out
55 export PYTHONPATH=${cfg.package.pythonPath}
56 ${cfg.package.python.interpreter} -m frigate --validate-config || error
57 '';
58 configFile = if cfg.checkConfig then configFileChecked else configFileUnchecked;
59
60 cameraFormat =
61 with types;
62 submodule {
63 freeformType = format.type;
64 options = {
65 ffmpeg = {
66 inputs = mkOption {
67 description = ''
68 List of inputs for this camera.
69 '';
70 type = listOf (submodule {
71 freeformType = format.type;
72 options = {
73 path = mkOption {
74 type = str;
75 example = "rtsp://192.0.2.1:554/rtsp";
76 description = ''
77 Stream URL
78 '';
79 };
80 roles = mkOption {
81 type = listOf (enum [
82 "audio"
83 "detect"
84 "record"
85 ]);
86 example = [
87 "detect"
88 "record"
89 ];
90 description = ''
91 List of roles for this stream
92 '';
93 };
94 };
95 });
96 };
97 };
98 };
99 };
100
101 # auth_request.conf
102 nginxAuthRequest = ''
103 # Send a subrequest to verify if the user is authenticated and has permission to access the resource.
104 auth_request /auth;
105
106 # Save the upstream metadata response headers from the auth request to variables
107 auth_request_set $user $upstream_http_remote_user;
108 auth_request_set $role $upstream_http_remote_role;
109 auth_request_set $groups $upstream_http_remote_groups;
110 auth_request_set $name $upstream_http_remote_name;
111 auth_request_set $email $upstream_http_remote_email;
112
113 # Inject the metadata response headers from the variables into the request made to the backend.
114 proxy_set_header Remote-User $user;
115 proxy_set_header Remote-Role $role;
116 proxy_set_header Remote-Groups $groups;
117 proxy_set_header Remote-Email $email;
118 proxy_set_header Remote-Name $name;
119
120 # Refresh the cookie as needed
121 auth_request_set $auth_cookie $upstream_http_set_cookie;
122 add_header Set-Cookie $auth_cookie;
123
124 # Pass the location header back up if it exists
125 auth_request_set $redirection_url $upstream_http_location;
126 add_header Location $redirection_url;
127 '';
128
129 nginxProxySettings = ''
130 # Basic Proxy Configuration
131 client_body_buffer_size 128k;
132 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; ## Timeout if the real server is dead.
133 proxy_redirect http:// $scheme://;
134 proxy_cache_bypass $cookie_session;
135 proxy_no_cache $cookie_session;
136 proxy_buffers 64 256k;
137
138 # Advanced Proxy Configuration
139 send_timeout 5m;
140 proxy_read_timeout 360;
141 proxy_send_timeout 360;
142 proxy_connect_timeout 360;
143 '';
144
145 # Discover configured detectors for acceleration support
146 detectors = attrValues cfg.settings.detectors or { };
147 withCoralUSB = any (d: d.type == "edgetpu" && hasPrefix "usb" d.device or "") detectors;
148 withCoralPCI = any (d: d.type == "edgetpu" && hasPrefix "pci" d.device or "") detectors;
149 withCoral = withCoralPCI || withCoralUSB;
150in
151
152{
153 meta.buildDocsInSandbox = false;
154
155 options.services.frigate = with types; {
156 enable = mkEnableOption "Frigate NVR";
157
158 package = mkPackageOption pkgs "frigate" { };
159
160 hostname = mkOption {
161 type = str;
162 example = "frigate.exampe.com";
163 description = ''
164 Hostname of the nginx vhost to configure.
165
166 Only nginx is supported by upstream for direct reverse proxying.
167 '';
168 };
169
170 vaapiDriver = mkOption {
171 type = nullOr (enum [
172 "i965"
173 "iHD"
174 "nouveau"
175 "vdpau"
176 "nvidia"
177 "radeonsi"
178 ]);
179 default = null;
180 example = "radeonsi";
181 description = ''
182 Force usage of a particular VA-API driver for video acceleration. Use together with `settings.ffmpeg.hwaccel_args`.
183
184 Setting this *is not required* for VA-API to work, but it can help steer VA-API towards the correct card if you have multiple.
185
186 :::{.note}
187 For VA-API to work you must enable {option}`hardware.graphics.enable` (sufficient for AMDGPU) and pass for example
188 `pkgs.intel-media-driver` (required for Intel 5th Gen. and newer) into {option}`hardware.graphics.extraPackages`.
189 :::
190
191 See also:
192
193 - <https://docs.frigate.video/configuration/hardware_acceleration>
194 - <https://docs.frigate.video/configuration/ffmpeg_presets#hwaccel-presets>
195 '';
196 };
197
198 checkConfig = mkOption {
199 type = bool;
200 default =
201 pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform
202 && (!pkgs.stdenv.hostPlatform.isAarch64);
203 defaultText = literalExpression ''
204 pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform && !(pkgs.stdenv.hostPlaform.isAarch64)
205 '';
206 description = ''
207 Whether to check the configuration at build time.
208 '';
209 };
210
211 settings = mkOption {
212 type = submodule {
213 freeformType = format.type;
214 options = {
215 cameras = mkOption {
216 type = attrsOf cameraFormat;
217 description = ''
218 Attribute set of cameras configurations.
219
220 <https://docs.frigate.video/configuration/cameras>
221 '';
222 };
223
224 database = {
225 path = mkOption {
226 type = path;
227 default = "/var/lib/frigate/frigate.db";
228 description = ''
229 Path to the SQLite database used
230 '';
231 };
232 };
233
234 ffmpeg = {
235 path = mkOption {
236 type = coercedTo package toString str;
237 default = pkgs.ffmpeg-headless;
238 example = literalExpression "pkgs.ffmpeg-full";
239 description = ''
240 Package providing the ffmpeg and ffprobe executables below the bin/ directory.
241 '';
242 };
243 };
244
245 mqtt = {
246 enabled = mkEnableOption "MQTT support";
247
248 host = mkOption {
249 type = nullOr str;
250 default = null;
251 example = "mqtt.example.com";
252 description = ''
253 MQTT server hostname
254 '';
255 };
256 };
257 };
258 };
259 default = { };
260 description = ''
261 Frigate configuration as a nix attribute set.
262
263 See the project documentation for how to configure frigate.
264 - [Creating a config file](https://docs.frigate.video/guides/getting_started)
265 - [Configuration reference](https://docs.frigate.video/configuration/index)
266 '';
267 };
268 };
269
270 config = mkIf cfg.enable {
271 services.nginx = {
272 enable = true;
273 additionalModules = with pkgs.nginxModules; [
274 develkit
275 rtmp
276 secure-token
277 set-misc
278 vod
279 ];
280 recommendedGzipSettings = mkDefault true;
281 mapHashBucketSize = mkDefault 128;
282 upstreams = {
283 frigate-api.servers = {
284 "127.0.0.1:5001" = { };
285 };
286 frigate-mqtt-ws.servers = {
287 "127.0.0.1:5002" = { };
288 };
289 frigate-jsmpeg.servers = {
290 "127.0.0.1:8082" = { };
291 };
292 frigate-go2rtc.servers = {
293 "127.0.0.1:1984" = { };
294 };
295 };
296 proxyCachePath."frigate" = {
297 enable = true;
298 keysZoneSize = "10m";
299 keysZoneName = "frigate_api_cache";
300 maxSize = "10m";
301 inactive = "1m";
302 levels = "1:2";
303 };
304 # Based on https://github.com/blakeblackshear/frigate/blob/v0.13.1/docker/main/rootfs/usr/local/nginx/conf/nginx.conf
305 virtualHosts."${cfg.hostname}" = {
306 locations = {
307 # auth_location.conf
308 "/auth" = {
309 proxyPass = "http://frigate-api/auth";
310 recommendedProxySettings = true;
311 extraConfig = ''
312 internal;
313
314 # Strip all request headers
315 proxy_pass_request_headers off;
316
317 # Pass info about the request
318 proxy_set_header X-Original-Method $request_method;
319 proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
320 proxy_set_header X-Server-Port $server_port;
321 proxy_set_header Content-Length "";
322
323 # Pass along auth related info
324 proxy_set_header Authorization $http_authorization;
325 proxy_set_header Cookie $http_cookie;
326 proxy_set_header X-CSRF-TOKEN "1";
327
328 # Header used to validate reverse proxy trust
329 proxy_set_header X-Proxy-Secret $http_x_proxy_secret;
330
331 # Pass headers for common auth proxies
332 proxy_set_header Remote-User $http_remote_user;
333 proxy_set_header Remote-Groups $http_remote_groups;
334 proxy_set_header Remote-Email $http_remote_email;
335 proxy_set_header Remote-Name $http_remote_name;
336 proxy_set_header X-Forwarded-User $http_x_forwarded_user;
337 proxy_set_header X-Forwarded-Groups $http_x_forwarded_groups;
338 proxy_set_header X-Forwarded-Email $http_x_forwarded_email;
339 proxy_set_header X-Forwarded-Preferred-Username $http_x_forwarded_preferred_username;
340 proxy_set_header X-authentik-username $http_x_authentik_username;
341 proxy_set_header X-authentik-groups $http_x_authentik_groups;
342 proxy_set_header X-authentik-email $http_x_authentik_email;
343 proxy_set_header X-authentik-name $http_x_authentik_name;
344 proxy_set_header X-authentik-uid $http_x_authentik_uid;
345
346 ${nginxProxySettings}
347 '';
348 };
349 "/vod/" = {
350 extraConfig = nginxAuthRequest + ''
351 aio threads;
352 vod hls;
353
354 secure_token $args;
355 secure_token_types application/vnd.apple.mpegurl;
356
357 add_header Cache-Control "no-store";
358 expires off;
359
360 keepalive_disable safari;
361
362 # vod module returns 502 for non-existent media
363 # https://github.com/kaltura/nginx-vod-module/issues/468
364 error_page 502 =404 /vod-not-found;
365 '';
366 };
367 "/vod-not-found" = {
368 return = 404;
369 };
370 "/stream/" = {
371 alias = "/var/cache/frigate/stream/";
372 extraConfig = nginxAuthRequest + ''
373 add_header Cache-Control "no-store";
374 expires off;
375
376 types {
377 application/dash+xml mpd;
378 application/vnd.apple.mpegurl m3u8;
379 video/mp2t ts;
380 image/jpeg jpg;
381 }
382 '';
383 };
384 "/clips/" = {
385 root = "/var/lib/frigate";
386 extraConfig = nginxAuthRequest + ''
387 types {
388 video/mp4 mp4;
389 image/jpeg jpg;
390 }
391
392 expires 7d;
393 add_header Cache-Control "public";
394 autoindex on;
395 '';
396 };
397 "/cache/" = {
398 alias = "/var/cache/frigate/";
399 extraConfig = ''
400 internal;
401 '';
402 };
403 "/recordings/" = {
404 root = "/var/lib/frigate";
405 extraConfig = nginxAuthRequest + ''
406 types {
407 video/mp4 mp4;
408 }
409
410 autoindex on;
411 autoindex_format json;
412 '';
413 };
414 "/exports/" = {
415 root = "/var/lib/frigate";
416 extraConfig = nginxAuthRequest + ''
417 types {
418 video/mp4 mp4;
419 }
420
421 autoindex on;
422 autoindex_format json;
423 '';
424 };
425 "/ws" = {
426 proxyPass = "http://frigate-mqtt-ws/";
427 recommendedProxySettings = true;
428 proxyWebsockets = true;
429 extraConfig = nginxAuthRequest + nginxProxySettings;
430 };
431 "/live/jsmpeg" = {
432 proxyPass = "http://frigate-jsmpeg/";
433 recommendedProxySettings = true;
434 proxyWebsockets = true;
435 extraConfig = nginxAuthRequest + nginxProxySettings;
436 };
437 # frigate lovelace card uses this path
438 "/live/mse/api/ws" = {
439 proxyPass = "http://frigate-go2rtc/api/ws";
440 proxyWebsockets = true;
441 recommendedProxySettings = true;
442 extraConfig =
443 nginxAuthRequest
444 + nginxProxySettings
445 + ''
446 limit_except GET {
447 deny all;
448 }
449 '';
450 };
451 "/live/webrtc/api/ws" = {
452 proxyPass = "http://frigate-go2rtc/api/ws";
453 proxyWebsockets = true;
454 recommendedProxySettings = true;
455 extraConfig =
456 nginxAuthRequest
457 + nginxProxySettings
458 + ''
459 limit_except GET {
460 deny all;
461 }
462 '';
463 };
464 # pass through go2rtc player
465 "/live/webrtc/webrtc.html" = {
466 proxyPass = "http://frigate-go2rtc/webrtc.html";
467 recommendedProxySettings = true;
468 extraConfig =
469 nginxAuthRequest
470 + nginxProxySettings
471 + ''
472 limit_except GET {
473 deny all;
474 }
475 '';
476 };
477 # frontend uses this to fetch the version
478 "/api/go2rtc/api" = {
479 proxyPass = "http://frigate-go2rtc/api";
480 recommendedProxySettings = true;
481 extraConfig =
482 nginxAuthRequest
483 + nginxProxySettings
484 + ''
485 limit_except GET {
486 deny all;
487 }
488 '';
489 };
490 # integrationn uses this to add webrtc candidate
491 "/api/go2rtc/webrtc" = {
492 proxyPass = "http://frigate-go2rtc/api/webrtc";
493 proxyWebsockets = true;
494 recommendedProxySettings = true;
495 extraConfig =
496 nginxAuthRequest
497 + nginxProxySettings
498 + ''
499 limit_except GET {
500 deny all;
501 }
502 '';
503 };
504 "~* /api/.*\\.(jpg|jpeg|png|webp|gif)$" = {
505 proxyPass = "http://frigate-api";
506 recommendedProxySettings = true;
507 extraConfig =
508 nginxAuthRequest
509 + nginxProxySettings
510 + ''
511 rewrite ^/api/(.*)$ /$1 break;
512 '';
513 };
514 "/api/" = {
515 proxyPass = "http://frigate-api/";
516 recommendedProxySettings = true;
517 extraConfig =
518 nginxAuthRequest
519 + nginxProxySettings
520 + ''
521 add_header Cache-Control "no-store";
522 expires off;
523
524 proxy_cache frigate_api_cache;
525 proxy_cache_lock on;
526 proxy_cache_use_stale updating;
527 proxy_cache_valid 200 5s;
528 proxy_cache_bypass $http_x_cache_bypass;
529 proxy_no_cache $should_not_cache;
530 add_header X-Cache-Status $upstream_cache_status;
531
532 location /api/vod/ {
533 ${nginxAuthRequest}
534 proxy_pass http://frigate-api/vod/;
535 proxy_cache off;
536 add_header Cache-Control "no-store";
537 ${nginxProxySettings}
538 }
539
540 location /api/login {
541 auth_request off;
542 rewrite ^/api(/.*)$ $1 break;
543 proxy_pass http://frigate-api;
544 ${nginxProxySettings}
545 }
546
547 location /api/stats {
548 ${nginxAuthRequest}
549 access_log off;
550 rewrite ^/api(/.*)$ $1 break;
551 add_header Cache-Control "no-store";
552 proxy_pass http://frigate-api;
553 ${nginxProxySettings}
554 }
555
556 location /api/version {
557 ${nginxAuthRequest}
558 access_log off;
559 rewrite ^/api(/.*)$ $1 break;
560 add_header Cache-Control "no-store";
561 proxy_pass http://frigate-api;
562 ${nginxProxySettings}
563 }
564 '';
565 };
566 "/assets/" = {
567 root = cfg.package.web;
568 extraConfig = ''
569 access_log off;
570 expires 1y;
571 add_header Cache-Control "public";
572 '';
573 };
574 "/locales/" = {
575 root = cfg.package.web;
576 extraConfig = ''
577 access_log off;
578 add_header Cache-Control "public";
579 '';
580 };
581 "~ ^/.*-([A-Za-z0-9]+)\.webmanifest$" = {
582 root = cfg.package.web;
583 extraConfig = ''
584 access_log off;
585 expires 1y;
586 add_header Cache-Control "public";
587 default_type application/json;
588 proxy_set_header Accept-Encoding "";
589 '';
590 };
591 "/" = {
592 root = cfg.package.web;
593 tryFiles = "$uri $uri.html $uri/ /index.html";
594 extraConfig = ''
595 add_header Cache-Control "no-store";
596 expires off;
597 '';
598 };
599 };
600 extraConfig = ''
601 # Frigate wants to connect on 127.0.0.1:5000 for unauthenticated requests
602 # https://github.com/NixOS/nixpkgs/issues/370349
603 listen 127.0.0.1:5000;
604
605 # vod settings
606 vod_base_url "";
607 vod_segments_base_url "";
608 vod_mode mapped;
609 vod_max_mapping_response_size 1m;
610 vod_upstream_location /api;
611 vod_align_segments_to_key_frames on;
612 vod_manifest_segment_durations_mode accurate;
613 vod_ignore_edit_list on;
614 vod_segment_duration 10000;
615 vod_hls_mpegts_align_frames off;
616 vod_hls_mpegts_interleave_frames on;
617
618 # file handle caching / aio
619 open_file_cache max=1000 inactive=5m;
620 open_file_cache_valid 2m;
621 open_file_cache_min_uses 1;
622 open_file_cache_errors on;
623 aio on;
624
625 # file upload size
626 client_max_body_size 20M;
627
628 # https://github.com/kaltura/nginx-vod-module#vod_open_file_thread_pool
629 vod_open_file_thread_pool default;
630
631 # vod caches
632 vod_metadata_cache metadata_cache 512m;
633 vod_mapping_cache mapping_cache 5m 10m;
634
635 # gzip manifest
636 gzip_types application/vnd.apple.mpegurl;
637 '';
638 };
639 appendConfig = ''
640 rtmp {
641 server {
642 listen 1935;
643 chunk_size 4096;
644 allow publish 127.0.0.1;
645 deny publish all;
646 allow play all;
647 application live {
648 live on;
649 record off;
650 meta copy;
651 }
652 }
653 }
654 '';
655 appendHttpConfig = ''
656 map $sent_http_content_type $should_not_cache {
657 'application/json' 0;
658 default 1;
659 }
660 '';
661 };
662
663 systemd.services.nginx.serviceConfig.SupplementaryGroups = [
664 "frigate"
665 ];
666
667 hardware.coral = {
668 usb.enable = mkDefault withCoralUSB;
669 pcie.enable = mkDefault withCoralPCI;
670 };
671
672 users.users.frigate = {
673 isSystemUser = true;
674 group = "frigate";
675 };
676 users.groups.frigate = { };
677
678 systemd.services.frigate = {
679 after = [
680 "go2rtc.service"
681 "network.target"
682 ];
683 wantedBy = [
684 "multi-user.target"
685 ];
686 environment = {
687 CONFIG_FILE = "/run/frigate/frigate.yml";
688 HOME = "/var/lib/frigate";
689 PYTHONPATH = cfg.package.pythonPath;
690 }
691 // optionalAttrs (cfg.vaapiDriver != null) {
692 LIBVA_DRIVER_NAME = cfg.vaapiDriver;
693 }
694 // optionalAttrs withCoral {
695 LD_LIBRARY_PATH = makeLibraryPath (with pkgs; [ libedgetpu ]);
696 };
697 path =
698 with pkgs;
699 [
700 # unfree:
701 # config.boot.kernelPackages.nvidiaPackages.latest.bin
702 libva-utils
703 procps
704 radeontop
705 ]
706 ++ optionals (!stdenv.hostPlatform.isAarch64) [
707 # not available on aarch64-linux
708 intel-gpu-tools
709 rocmPackages.rocminfo
710 ];
711 serviceConfig = {
712 ExecStartPre = [
713 (pkgs.writeShellScript "frigate-clear-cache" ''
714 shopt -s extglob
715 rm --recursive --force /var/cache/frigate/!(model_cache)
716 '')
717 (pkgs.writeShellScript "frigate-create-writable-config" ''
718 cp --no-preserve=mode ${configFile} /run/frigate/frigate.yml
719 '')
720 ];
721 ExecStart = "${cfg.package.python.interpreter} -m frigate";
722 Restart = "on-failure";
723 SyslogIdentifier = "frigate";
724
725 User = "frigate";
726 Group = "frigate";
727 SupplementaryGroups = [ "render" ] ++ optionals withCoral [ "coral" ];
728
729 AmbientCapabilities = optionals (elem cfg.vaapiDriver [
730 "i965"
731 "iHD"
732 ]) [ "CAP_PERFMON" ]; # for intel_gpu_top
733
734 UMask = "0027";
735
736 StateDirectory = "frigate";
737 StateDirectoryMode = "0750";
738
739 # Caches
740 PrivateTmp = true;
741 CacheDirectory = [
742 "frigate"
743 # https://github.com/blakeblackshear/frigate/discussions/18129
744 "frigate/model_cache"
745 ];
746 CacheDirectoryMode = "0750";
747
748 # Sockets/IPC
749 RuntimeDirectory = "frigate";
750 };
751 };
752 };
753}