1 { config, lib, pkgs, ... }:
6 cfg = config.services.jitsi-meet;
8 # The configuration files are JS of format "var <<string>> = <<JSON>>;". In order to
9 # override only some settings, we need to extract the JSON, use jq to merge it with
10 # the config provided by user, and then reconstruct the file.
12 source: varName: userCfg: appendExtra:
14 extractor = pkgs.writeText "extractor.js" ''
15 var fs = require("fs");
16 eval(fs.readFileSync(process.argv[2], 'utf8'));
17 process.stdout.write(JSON.stringify(eval(process.argv[3])));
19 userJson = pkgs.writeText "user.json" (builtins.toJSON userCfg);
20 in (pkgs.runCommand "${varName}.js" { } ''
21 ${pkgs.nodejs}/bin/node ${extractor} ${source} ${varName} > default.json
23 echo "var ${varName} = "
24 ${pkgs.jq}/bin/jq -s '.[0] * .[1]' default.json ${userJson}
26 echo ${escapeShellArg appendExtra}
30 # Essential config - it's probably not good to have these as option default because
31 # types.attrs doesn't do merging. Let's merge explicitly, can still be overridden if
35 domain = cfg.hostName;
36 muc = "conference.${cfg.hostName}";
37 focus = "focus.${cfg.hostName}";
38 jigasi = "jigasi.${cfg.hostName}";
40 bosh = "//${cfg.hostName}/http-bind";
41 websocket = "wss://${cfg.hostName}/xmpp-websocket";
43 fileRecordingsEnabled = true;
44 liveStreamingEnabled = true;
45 hiddenDomain = "recorder.${cfg.hostName}";
49 options.services.jitsi-meet = with types; {
50 enable = mkEnableOption "Jitsi Meet - Secure, Simple and Scalable Video Conferences";
54 example = "meet.example.org";
56 FQDN of the Jitsi Meet instance.
63 example = literalExpression ''
65 enableWelcomePage = false;
70 Client-side web application settings that override the defaults in {file}`config.js`.
72 See <https://github.com/jitsi/jitsi-meet/blob/master/config.js> for default
73 configuration with comments.
77 extraConfig = mkOption {
81 Text to append to {file}`config.js` web application config file.
83 Can be used to insert JavaScript logic to determine user's region in cascading bridges setup.
87 interfaceConfig = mkOption {
90 example = literalExpression ''
92 SHOW_JITSI_WATERMARK = false;
93 SHOW_WATERMARK_FOR_GUESTS = false;
97 Client-side web-app interface settings that override the defaults in {file}`interface_config.js`.
99 See <https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js> for
100 default configuration with comments.
109 Jitsi Videobridge instance and configure it to connect to Prosody.
111 Additional configuration is possible with {option}`services.jitsi-videobridge`
115 passwordFile = mkOption {
118 example = "/run/keys/videobridge";
120 File containing password to the Prosody account for videobridge.
122 If `null`, a file with password will be generated automatically. Setting
123 this option is useful if you plan to connect additional videobridges to the XMPP server.
128 jicofo.enable = mkOption {
132 Whether to enable JiCoFo instance and configure it to connect to Prosody.
134 Additional configuration is possible with {option}`services.jicofo`.
138 jibri.enable = mkOption {
142 Whether to enable a Jibri instance and configure it to connect to Prosody.
144 Additional configuration is possible with {option}`services.jibri`, and
145 {option}`services.jibri.finalizeScript` is especially useful.
149 jigasi.enable = mkOption {
153 Whether to enable jigasi instance and configure it to connect to Prosody.
155 Additional configuration is possible with <option>services.jigasi</option>.
159 nginx.enable = mkOption {
163 Whether to enable nginx virtual host that will serve the javascript application and act as
164 a proxy for the XMPP server. Further nginx configuration can be done by adapting
165 {option}`services.nginx.virtualHosts.<hostName>`.
166 When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable
167 this, set the {option}`services.nginx.virtualHosts.<hostName>.enableACME` to
168 `false` and if appropriate do the same for
169 {option}`services.nginx.virtualHosts.<hostName>.forceSSL`.
173 caddy.enable = mkEnableOption "caddy reverse proxy to expose jitsi-meet";
175 prosody.enable = mkOption {
179 Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this
180 off if you want to configure it manually.
184 excalidraw.enable = mkEnableOption "Excalidraw collaboration backend for Jitsi";
185 excalidraw.port = mkOption {
188 description = ''The port which the Excalidraw backend for Jitsi should listen to.'';
192 enable = mkEnableOption "Authenticated room creation";
193 authentication = mkOption {
195 default = "internal_hashed";
196 description = ''The authentication type to be used by jitsi'';
201 config = mkIf cfg.enable {
202 services.prosody = mkIf cfg.prosody.enable {
203 enable = mkDefault true;
204 xmppComplianceSuite = mkDefault false;
206 admin_adhoc = mkDefault false;
207 bosh = mkDefault true;
208 ping = mkDefault true;
209 roster = mkDefault true;
210 saslauth = mkDefault true;
211 smacks = mkDefault true;
212 tls = mkDefault true;
213 websocket = mkDefault true;
217 domain = "conference.${cfg.hostName}";
218 name = "Jitsi Meet MUC";
220 roomDefaultPublicJids = true;
222 restrict_room_creation = true
224 admins = { "focus@auth.${cfg.hostName}" }
228 domain = "breakout.${cfg.hostName}";
229 name = "Jitsi Meet Breakout MUC";
231 roomDefaultPublicJids = true;
233 restrict_room_creation = true
235 admins = { "focus@auth.${cfg.hostName}" }
239 domain = "internal.auth.${cfg.hostName}";
240 name = "Jitsi Meet Videobridge MUC";
242 roomDefaultPublicJids = true;
245 admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}", "jigasi@auth.${cfg.hostName}" }
247 #-- muc_room_cache_size = 1000
250 domain = "lobby.${cfg.hostName}";
251 name = "Jitsi Meet Lobby MUC";
253 roomDefaultPublicJids = true;
255 restrict_room_creation = true
265 "conference_duration"
278 extraPluginPaths = [ "${pkgs.jitsi-meet-prosody}/share/prosody-plugins" ];
279 extraConfig = lib.mkMerge [
281 Component "focus.${cfg.hostName}" "client_proxy"
282 target_address = "focus@auth.${cfg.hostName}"
284 Component "jigasi.${cfg.hostName}" "client_proxy"
285 target_address = "jigasi@auth.${cfg.hostName}"
287 Component "speakerstats.${cfg.hostName}" "speakerstats_component"
288 muc_component = "conference.${cfg.hostName}"
290 Component "conferenceduration.${cfg.hostName}" "conference_duration_component"
291 muc_component = "conference.${cfg.hostName}"
293 Component "endconference.${cfg.hostName}" "end_conference"
294 muc_component = "conference.${cfg.hostName}"
296 Component "avmoderation.${cfg.hostName}" "av_moderation_component"
297 muc_component = "conference.${cfg.hostName}"
299 Component "metadata.${cfg.hostName}" "room_metadata_component"
300 muc_component = "conference.${cfg.hostName}"
301 breakout_rooms_component = "breakout.${cfg.hostName}"
304 muc_mapper_domain_base = "${cfg.hostName}"
306 cross_domain_websocket = true;
307 consider_websocket_secure = true;
310 "focus@auth.${cfg.hostName}",
311 "jvb@auth.${cfg.hostName}"
315 virtualHosts.${cfg.hostName} = {
317 domain = cfg.hostName;
319 authentication = ${if cfg.secureDomain.enable then "\"${cfg.secureDomain.authentication}\"" else "\"jitsi-anonymous\""}
320 c2s_require_encryption = false
321 admins = { "focus@auth.${cfg.hostName}" }
322 smacks_max_unacked_stanzas = 5
323 smacks_hibernation_time = 60
324 smacks_max_hibernated_sessions = 1
325 smacks_max_old_sessions = 1
327 av_moderation_component = "avmoderation.${cfg.hostName}"
328 speakerstats_component = "speakerstats.${cfg.hostName}"
329 conference_duration_component = "conferenceduration.${cfg.hostName}"
330 end_conference_component = "endconference.${cfg.hostName}"
332 c2s_require_encryption = false
333 lobby_muc = "lobby.${cfg.hostName}"
334 breakout_rooms_muc = "breakout.${cfg.hostName}"
335 room_metadata_component = "metadata.${cfg.hostName}"
336 main_muc = "conference.${cfg.hostName}"
339 cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
340 key = "/var/lib/jitsi-meet/jitsi-meet.key";
343 virtualHosts."auth.${cfg.hostName}" = {
345 domain = "auth.${cfg.hostName}";
347 authentication = "internal_hashed"
350 cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
351 key = "/var/lib/jitsi-meet/jitsi-meet.key";
354 virtualHosts."recorder.${cfg.hostName}" = {
356 domain = "recorder.${cfg.hostName}";
358 authentication = "internal_plain"
359 c2s_require_encryption = false
362 virtualHosts."guest.${cfg.hostName}" = {
364 domain = "guest.${cfg.hostName}";
366 authentication = "anonymous"
367 c2s_require_encryption = false
371 systemd.services.prosody = mkIf cfg.prosody.enable {
373 videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
375 ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
376 ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
377 ${config.services.prosody.package}/bin/prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
378 ${config.services.prosody.package}/bin/prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
379 ${config.services.prosody.package}/bin/prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
380 '' + optionalString cfg.jigasi.enable ''
381 ${config.services.prosody.package}/bin/prosodyctl register jigasi auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jigasi-user-secret)"
385 EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
386 SupplementaryGroups = [ "jitsi-meet" ];
388 reloadIfChanged = true;
391 users.groups.jitsi-meet = { };
392 systemd.tmpfiles.rules = [
393 "d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -"
396 systemd.services.jitsi-meet-init-secrets = {
397 wantedBy = [ "multi-user.target" ];
398 before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service") ++ (optional cfg.jigasi.enable "jigasi.service");
403 Group = "jitsi-meet";
404 WorkingDirectory = "/var/lib/jitsi-meet";
408 secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optionals cfg.jigasi.enable [ "jigasi-user-secret" "jigasi-component-secret" ]) ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
411 ${concatMapStringsSep "\n" (s: ''
412 if [ ! -f ${s} ]; then
413 tr -dc a-zA-Z0-9 </dev/urandom | head -c 64 > ${s}
417 # for easy access in prosody
418 echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env
419 echo "JIGASI_COMPONENT_SECRET=$(cat jigasi-component-secret)" >> secrets-env
421 + optionalString cfg.prosody.enable ''
422 # generate self-signed certificates
423 if [ ! -f /var/lib/jitsi-meet/jitsi-meet.crt ]; then
424 ${getBin pkgs.openssl}/bin/openssl req \
427 -keyout /var/lib/jitsi-meet/jitsi-meet.key \
428 -out /var/lib/jitsi-meet/jitsi-meet.crt \
431 -subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}'
432 chmod 640 /var/lib/jitsi-meet/jitsi-meet.key
437 systemd.services.jitsi-excalidraw = mkIf cfg.excalidraw.enable {
438 description = "Excalidraw collaboration backend for Jitsi";
439 after = [ "network.target" ];
440 wantedBy = [ "multi-user.target" ];
441 environment.PORT = toString cfg.excalidraw.port;
445 ExecStart = "${pkgs.jitsi-excalidraw}/bin/jitsi-excalidraw-backend";
446 Restart = "on-failure";
447 Group = "jitsi-meet";
451 services.nginx = mkIf cfg.nginx.enable {
452 enable = mkDefault true;
453 virtualHosts.${cfg.hostName} = {
454 enableACME = mkDefault true;
455 forceSSL = mkDefault true;
456 root = pkgs.jitsi-meet;
460 locations."@root_path".extraConfig = ''
461 rewrite ^/(.*)$ / break;
463 locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path";
464 locations."^~ /xmpp-websocket" = {
466 proxyPass = "http://localhost:5280/xmpp-websocket";
467 proxyWebsockets = true;
469 locations."=/http-bind" = {
470 proxyPass = "http://localhost:5280/http-bind";
472 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
473 proxy_set_header Host $host;
476 locations."=/external_api.js" = mkDefault {
477 alias = "${pkgs.jitsi-meet}/libs/external_api.min.js";
479 locations."=/_api/room-info" = {
480 proxyPass = "http://localhost:5280/room-info";
482 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
483 proxy_set_header Host $host;
486 locations."=/config.js" = mkDefault {
487 alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig;
489 locations."=/interface_config.js" = mkDefault {
490 alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig "";
492 locations."/socket.io/" = mkIf cfg.excalidraw.enable {
493 proxyPass = "http://127.0.0.1:${toString cfg.excalidraw.port}";
494 proxyWebsockets = true;
499 services.caddy = mkIf cfg.caddy.enable {
500 enable = mkDefault true;
501 virtualHosts.${cfg.hostName} = {
504 templatedJitsiMeet = pkgs.runCommand "templated-jitsi-meet" { } ''
505 cp -R --no-preserve=all ${pkgs.jitsi-meet}/* .
506 for file in *.html **/*.html ; do
507 ${pkgs.sd}/bin/sd '<!--#include virtual="(.*)" -->' '{{ include "$1" }}' $file
510 rm interface_config.js
512 cp ${overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig} $out/config.js
513 cp ${overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""} $out/interface_config.js
514 cp ./libs/external_api.min.js $out/external_api.js
518 header Host ${cfg.hostName}
519 reverse_proxy 127.0.0.1:5280
521 handle /xmpp-websocket {
522 reverse_proxy 127.0.0.1:5280
526 root * ${templatedJitsiMeet}
527 try_files {path} {path}
528 try_files {path} /index.html
535 services.jitsi-meet.config = recursiveUpdate
536 (mkIf cfg.excalidraw.enable {
539 collabServerBaseUrl = "https://${cfg.hostName}";
542 (mkIf cfg.secureDomain.enable {
543 hosts.anonymousdomain = "guest.${cfg.hostName}";
546 services.jitsi-videobridge = mkIf cfg.videobridge.enable {
548 xmppConfigs."localhost" = {
550 domain = "auth.${cfg.hostName}";
551 passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
552 mucJids = "jvbbrewery@internal.auth.${cfg.hostName}";
553 disableCertificateVerification = true;
557 services.jicofo = mkIf cfg.jicofo.enable {
559 xmppHost = "localhost";
560 xmppDomain = cfg.hostName;
561 userDomain = "auth.${cfg.hostName}";
563 userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret";
564 componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
565 bridgeMuc = "jvbbrewery@internal.auth.${cfg.hostName}";
567 jicofo.xmpp.service.disable-certificate-verification = true;
568 jicofo.xmpp.client.disable-certificate-verification = true;
570 (lib.mkIf (config.services.jibri.enable || cfg.jibri.enable) {
572 brewery-jid = "JibriBrewery@internal.auth.${cfg.hostName}";
573 pending-timeout = "90";
576 (lib.mkIf cfg.secureDomain.enable {
581 login-url = cfg.hostName;
583 xmpp.client.client-proxy = "focus.${cfg.hostName}";
588 services.jibri = mkIf cfg.jibri.enable {
591 xmppEnvironments."jitsi-meet" = {
592 xmppServerHosts = [ "localhost" ];
593 xmppDomain = cfg.hostName;
596 domain = "internal.auth.${cfg.hostName}";
597 roomName = "JibriBrewery";
602 domain = "auth.${cfg.hostName}";
604 passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret";
608 domain = "recorder.${cfg.hostName}";
609 username = "recorder";
610 passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret";
614 disableCertificateVerification = true;
615 stripFromRoomDomain = "conference.";
619 services.jigasi = mkIf cfg.jigasi.enable {
621 xmppHost = "localhost";
622 xmppDomain = cfg.hostName;
623 userDomain = "auth.${cfg.hostName}";
625 userPasswordFile = "/var/lib/jitsi-meet/jigasi-user-secret";
626 componentPasswordFile = "/var/lib/jitsi-meet/jigasi-component-secret";
627 bridgeMuc = "jigasibrewery@internal.${cfg.hostName}";
629 "org.jitsi.jigasi.ALWAYS_TRUST_MODE_ENABLED" = "true";
634 meta.doc = ./jitsi-meet.md;
635 meta.maintainers = lib.teams.jitsi.members;