1{ lib, pkgs, config, ... }:
2
3let
4 cfg = config.services.peertube;
5
6 settingsFormat = pkgs.formats.json {};
7 configFile = settingsFormat.generate "production.json" cfg.settings;
8
9 env = {
10 NODE_CONFIG_DIR = "/var/lib/peertube/config";
11 NODE_ENV = "production";
12 NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt";
13 NPM_CONFIG_PREFIX = cfg.package;
14 HOME = cfg.package;
15 };
16
17 systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@memlock" "@mount" "@obsolete" "@privileged" "@setuid" ];
18
19 cfgService = {
20 # Proc filesystem
21 ProcSubset = "pid";
22 ProtectProc = "invisible";
23 # Access write directories
24 UMask = "0027";
25 # Capabilities
26 CapabilityBoundingSet = "";
27 # Security
28 NoNewPrivileges = true;
29 # Sandboxing
30 ProtectSystem = "strict";
31 ProtectHome = true;
32 PrivateTmp = true;
33 PrivateDevices = true;
34 PrivateUsers = true;
35 ProtectClock = true;
36 ProtectHostname = true;
37 ProtectKernelLogs = true;
38 ProtectKernelModules = true;
39 ProtectKernelTunables = true;
40 ProtectControlGroups = true;
41 RestrictNamespaces = true;
42 LockPersonality = true;
43 RestrictRealtime = true;
44 RestrictSUIDSGID = true;
45 RemoveIPC = true;
46 PrivateMounts = true;
47 # System Call Filtering
48 SystemCallArchitectures = "native";
49 };
50
51 envFile = pkgs.writeText "peertube.env" (lib.concatMapStrings (s: s + "\n") (
52 (lib.concatLists (lib.mapAttrsToList (name: value:
53 if value != null then [
54 "${name}=\"${toString value}\""
55 ] else []
56 ) env))));
57
58 peertubeEnv = pkgs.writeShellScriptBin "peertube-env" ''
59 set -a
60 source "${envFile}"
61 eval -- "\$@"
62 '';
63
64 peertubeCli = pkgs.writeShellScriptBin "peertube" ''
65 node ~/dist/server/tools/peertube.js $@
66 '';
67
68in {
69 options.services.peertube = {
70 enable = lib.mkEnableOption "Enable Peertube’s service";
71
72 user = lib.mkOption {
73 type = lib.types.str;
74 default = "peertube";
75 description = "User account under which Peertube runs.";
76 };
77
78 group = lib.mkOption {
79 type = lib.types.str;
80 default = "peertube";
81 description = "Group under which Peertube runs.";
82 };
83
84 localDomain = lib.mkOption {
85 type = lib.types.str;
86 example = "peertube.example.com";
87 description = "The domain serving your PeerTube instance.";
88 };
89
90 listenHttp = lib.mkOption {
91 type = lib.types.int;
92 default = 9000;
93 description = "listen port for HTTP server.";
94 };
95
96 listenWeb = lib.mkOption {
97 type = lib.types.int;
98 default = 9000;
99 description = "listen port for WEB server.";
100 };
101
102 enableWebHttps = lib.mkOption {
103 type = lib.types.bool;
104 default = false;
105 description = "Enable or disable HTTPS protocol.";
106 };
107
108 dataDirs = lib.mkOption {
109 type = lib.types.listOf lib.types.path;
110 default = [ ];
111 example = [ "/opt/peertube/storage" "/var/cache/peertube" ];
112 description = "Allow access to custom data locations.";
113 };
114
115 serviceEnvironmentFile = lib.mkOption {
116 type = lib.types.nullOr lib.types.path;
117 default = null;
118 example = "/run/keys/peertube/password-init-root";
119 description = ''
120 Set environment variables for the service. Mainly useful for setting the initial root password.
121 For example write to file:
122 PT_INITIAL_ROOT_PASSWORD=changeme
123 '';
124 };
125
126 settings = lib.mkOption {
127 type = settingsFormat.type;
128 example = lib.literalExpression ''
129 {
130 listen = {
131 hostname = "0.0.0.0";
132 };
133 log = {
134 level = "debug";
135 };
136 storage = {
137 tmp = "/opt/data/peertube/storage/tmp/";
138 logs = "/opt/data/peertube/storage/logs/";
139 cache = "/opt/data/peertube/storage/cache/";
140 };
141 }
142 '';
143 description = "Configuration for peertube.";
144 };
145
146 database = {
147 createLocally = lib.mkOption {
148 type = lib.types.bool;
149 default = false;
150 description = "Configure local PostgreSQL database server for PeerTube.";
151 };
152
153 host = lib.mkOption {
154 type = lib.types.str;
155 default = if cfg.database.createLocally then "/run/postgresql" else null;
156 example = "192.168.15.47";
157 description = "Database host address or unix socket.";
158 };
159
160 port = lib.mkOption {
161 type = lib.types.int;
162 default = 5432;
163 description = "Database host port.";
164 };
165
166 name = lib.mkOption {
167 type = lib.types.str;
168 default = "peertube";
169 description = "Database name.";
170 };
171
172 user = lib.mkOption {
173 type = lib.types.str;
174 default = "peertube";
175 description = "Database user.";
176 };
177
178 passwordFile = lib.mkOption {
179 type = lib.types.nullOr lib.types.path;
180 default = null;
181 example = "/run/keys/peertube/password-posgressql-db";
182 description = "Password for PostgreSQL database.";
183 };
184 };
185
186 redis = {
187 createLocally = lib.mkOption {
188 type = lib.types.bool;
189 default = false;
190 description = "Configure local Redis server for PeerTube.";
191 };
192
193 host = lib.mkOption {
194 type = lib.types.nullOr lib.types.str;
195 default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then "127.0.0.1" else null;
196 description = "Redis host.";
197 };
198
199 port = lib.mkOption {
200 type = lib.types.nullOr lib.types.port;
201 default = if cfg.redis.createLocally && cfg.redis.enableUnixSocket then null else 6379;
202 description = "Redis port.";
203 };
204
205 passwordFile = lib.mkOption {
206 type = lib.types.nullOr lib.types.path;
207 default = null;
208 example = "/run/keys/peertube/password-redis-db";
209 description = "Password for redis database.";
210 };
211
212 enableUnixSocket = lib.mkOption {
213 type = lib.types.bool;
214 default = cfg.redis.createLocally;
215 description = "Use Unix socket.";
216 };
217 };
218
219 smtp = {
220 createLocally = lib.mkOption {
221 type = lib.types.bool;
222 default = false;
223 description = "Configure local Postfix SMTP server for PeerTube.";
224 };
225
226 passwordFile = lib.mkOption {
227 type = lib.types.nullOr lib.types.path;
228 default = null;
229 example = "/run/keys/peertube/password-smtp";
230 description = "Password for smtp server.";
231 };
232 };
233
234 package = lib.mkOption {
235 type = lib.types.package;
236 default = pkgs.peertube;
237 description = "Peertube package to use.";
238 };
239 };
240
241 config = lib.mkIf cfg.enable {
242 assertions = [
243 { assertion = cfg.serviceEnvironmentFile == null || !lib.hasPrefix builtins.storeDir cfg.serviceEnvironmentFile;
244 message = ''
245 <option>services.peertube.serviceEnvironmentFile</option> points to
246 a file in the Nix store. You should use a quoted absolute path to
247 prevent this.
248 '';
249 }
250 { assertion = !(cfg.redis.enableUnixSocket && (cfg.redis.host != null || cfg.redis.port != null));
251 message = ''
252 <option>services.peertube.redis.createLocally</option> and redis network connection (<option>services.peertube.redis.host</option> or <option>services.peertube.redis.port</option>) enabled. Disable either of them.
253 '';
254 }
255 { assertion = cfg.redis.enableUnixSocket || (cfg.redis.host != null && cfg.redis.port != null);
256 message = ''
257 <option>services.peertube.redis.host</option> and <option>services.peertube.redis.port</option> needs to be set if <option>services.peertube.redis.enableUnixSocket</option> is not enabled.
258 '';
259 }
260 { assertion = cfg.redis.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.redis.passwordFile;
261 message = ''
262 <option>services.peertube.redis.passwordFile</option> points to
263 a file in the Nix store. You should use a quoted absolute path to
264 prevent this.
265 '';
266 }
267 { assertion = cfg.database.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.database.passwordFile;
268 message = ''
269 <option>services.peertube.database.passwordFile</option> points to
270 a file in the Nix store. You should use a quoted absolute path to
271 prevent this.
272 '';
273 }
274 { assertion = cfg.smtp.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.smtp.passwordFile;
275 message = ''
276 <option>services.peertube.smtp.passwordFile</option> points to
277 a file in the Nix store. You should use a quoted absolute path to
278 prevent this.
279 '';
280 }
281 ];
282
283 services.peertube.settings = lib.mkMerge [
284 {
285 listen = {
286 port = cfg.listenHttp;
287 };
288 webserver = {
289 https = (if cfg.enableWebHttps then true else false);
290 hostname = "${cfg.localDomain}";
291 port = cfg.listenWeb;
292 };
293 database = {
294 hostname = "${cfg.database.host}";
295 port = cfg.database.port;
296 name = "${cfg.database.name}";
297 username = "${cfg.database.user}";
298 };
299 redis = {
300 hostname = "${toString cfg.redis.host}";
301 port = (if cfg.redis.port == null then "" else cfg.redis.port);
302 };
303 storage = {
304 tmp = lib.mkDefault "/var/lib/peertube/storage/tmp/";
305 avatars = lib.mkDefault "/var/lib/peertube/storage/avatars/";
306 videos = lib.mkDefault "/var/lib/peertube/storage/videos/";
307 streaming_playlists = lib.mkDefault "/var/lib/peertube/storage/streaming-playlists/";
308 redundancy = lib.mkDefault "/var/lib/peertube/storage/redundancy/";
309 logs = lib.mkDefault "/var/lib/peertube/storage/logs/";
310 previews = lib.mkDefault "/var/lib/peertube/storage/previews/";
311 thumbnails = lib.mkDefault "/var/lib/peertube/storage/thumbnails/";
312 torrents = lib.mkDefault "/var/lib/peertube/storage/torrents/";
313 captions = lib.mkDefault "/var/lib/peertube/storage/captions/";
314 cache = lib.mkDefault "/var/lib/peertube/storage/cache/";
315 plugins = lib.mkDefault "/var/lib/peertube/storage/plugins/";
316 client_overrides = lib.mkDefault "/var/lib/peertube/storage/client-overrides/";
317 };
318 }
319 (lib.mkIf cfg.redis.enableUnixSocket { redis = { socket = "/run/redis/redis.sock"; }; })
320 ];
321
322 systemd.tmpfiles.rules = [
323 "d '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
324 "z '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
325 ];
326
327 systemd.services.peertube-init-db = lib.mkIf cfg.database.createLocally {
328 description = "Initialization database for PeerTube daemon";
329 after = [ "network.target" "postgresql.service" ];
330 wantedBy = [ "multi-user.target" ];
331
332 script = let
333 psqlSetupCommands = pkgs.writeText "peertube-init.sql" ''
334 SELECT 'CREATE USER "${cfg.database.user}"' WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${cfg.database.user}')\gexec
335 SELECT 'CREATE DATABASE "${cfg.database.name}" OWNER "${cfg.database.user}" TEMPLATE template0 ENCODING UTF8' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${cfg.database.name}')\gexec
336 \c '${cfg.database.name}'
337 CREATE EXTENSION IF NOT EXISTS pg_trgm;
338 CREATE EXTENSION IF NOT EXISTS unaccent;
339 '';
340 in "${config.services.postgresql.package}/bin/psql -f ${psqlSetupCommands}";
341
342 serviceConfig = {
343 Type = "oneshot";
344 WorkingDirectory = cfg.package;
345 # User and group
346 User = "postgres";
347 Group = "postgres";
348 # Sandboxing
349 RestrictAddressFamilies = [ "AF_UNIX" ];
350 MemoryDenyWriteExecute = true;
351 # System Call Filtering
352 SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
353 } // cfgService;
354 };
355
356 systemd.services.peertube = {
357 description = "PeerTube daemon";
358 after = [ "network.target" ]
359 ++ lib.optionals cfg.redis.createLocally [ "redis.service" ]
360 ++ lib.optionals cfg.database.createLocally [ "postgresql.service" "peertube-init-db.service" ];
361 wantedBy = [ "multi-user.target" ];
362
363 environment = env;
364
365 path = with pkgs; [ bashInteractive ffmpeg nodejs-16_x openssl yarn youtube-dl ];
366
367 script = ''
368 #!/bin/sh
369 umask 077
370 cat > /var/lib/peertube/config/local.yaml <<EOF
371 ${lib.optionalString ((!cfg.database.createLocally) && (cfg.database.passwordFile != null)) ''
372 database:
373 password: '$(cat ${cfg.database.passwordFile})'
374 ''}
375 ${lib.optionalString (cfg.redis.passwordFile != null) ''
376 redis:
377 auth: '$(cat ${cfg.redis.passwordFile})'
378 ''}
379 ${lib.optionalString (cfg.smtp.passwordFile != null) ''
380 smtp:
381 password: '$(cat ${cfg.smtp.passwordFile})'
382 ''}
383 EOF
384 ln -sf ${cfg.package}/config/default.yaml /var/lib/peertube/config/default.yaml
385 ln -sf ${configFile} /var/lib/peertube/config/production.json
386 npm start
387 '';
388 serviceConfig = {
389 Type = "simple";
390 Restart = "always";
391 RestartSec = 20;
392 TimeoutSec = 60;
393 WorkingDirectory = cfg.package;
394 # User and group
395 User = cfg.user;
396 Group = cfg.group;
397 # State directory and mode
398 StateDirectory = "peertube";
399 StateDirectoryMode = "0750";
400 # Access write directories
401 ReadWritePaths = cfg.dataDirs;
402 # Environment
403 EnvironmentFile = cfg.serviceEnvironmentFile;
404 # Sandboxing
405 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
406 MemoryDenyWriteExecute = false;
407 # System Call Filtering
408 SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "pipe" "pipe2" ];
409 } // cfgService;
410 };
411
412 services.postgresql = lib.mkIf cfg.database.createLocally {
413 enable = true;
414 };
415
416 services.redis = lib.mkMerge [
417 (lib.mkIf cfg.redis.createLocally {
418 enable = true;
419 })
420 (lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {
421 unixSocket = "/run/redis/redis.sock";
422 unixSocketPerm = 770;
423 })
424 ];
425
426 services.postfix = lib.mkIf cfg.smtp.createLocally {
427 enable = true;
428 hostname = lib.mkDefault "${cfg.localDomain}";
429 };
430
431 users.users = lib.mkMerge [
432 (lib.mkIf (cfg.user == "peertube") {
433 peertube = {
434 isSystemUser = true;
435 group = cfg.group;
436 home = cfg.package;
437 };
438 })
439 (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs-16_x pkgs.yarn ])
440 (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis" ];})
441 ];
442
443 users.groups = lib.optionalAttrs (cfg.group == "peertube") {
444 peertube = { };
445 };
446 };
447}