{ lib, pkgs, config, options, ... }:
let
cfg = config.services.peertube;
opt = options.services.peertube;
settingsFormat = pkgs.formats.json {};
configFile = settingsFormat.generate "production.json" cfg.settings;
env = {
NODE_CONFIG_DIR = "/var/lib/peertube/config";
NODE_ENV = "production";
NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt";
NPM_CONFIG_CACHE = "/var/cache/peertube/.npm";
NPM_CONFIG_PREFIX = cfg.package;
HOME = cfg.package;
};
systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@memlock" "@mount" "@obsolete" "@privileged" "@setuid" ];
cfgService = {
# Proc filesystem
ProcSubset = "pid";
ProtectProc = "invisible";
# Access write directories
UMask = "0027";
# Capabilities
CapabilityBoundingSet = "";
# Security
NoNewPrivileges = true;
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectClock = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
LockPersonality = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
PrivateMounts = true;
# System Call Filtering
SystemCallArchitectures = "native";
};
envFile = pkgs.writeText "peertube.env" (lib.concatMapStrings (s: s + "\n") (
(lib.concatLists (lib.mapAttrsToList (name: value:
lib.optional (value != null) ''${name}="${toString value}"''
) env))));
peertubeEnv = pkgs.writeShellScriptBin "peertube-env" ''
set -a
source "${envFile}"
eval -- "\$@"
'';
peertubeCli = pkgs.writeShellScriptBin "peertube" ''
node ~/dist/server/tools/peertube.js $@
'';
nginxCommonHeaders = lib.optionalString cfg.enableWebHttps ''
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains';
'' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
add_header Alt-Svc 'h3=":443"; ma=86400';
'' + ''
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'GET, OPTIONS';
add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
'';
in {
options.services.peertube = {
enable = lib.mkEnableOption (lib.mdDoc "Peertube");
user = lib.mkOption {
type = lib.types.str;
default = "peertube";
description = lib.mdDoc "User account under which Peertube runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "peertube";
description = lib.mdDoc "Group under which Peertube runs.";
};
localDomain = lib.mkOption {
type = lib.types.str;
example = "peertube.example.com";
description = lib.mdDoc "The domain serving your PeerTube instance.";
};
listenHttp = lib.mkOption {
type = lib.types.port;
default = 9000;
description = lib.mdDoc "The port that the local PeerTube web server will listen on.";
};
listenWeb = lib.mkOption {
type = lib.types.port;
default = 9000;
description = lib.mdDoc "The public-facing port that PeerTube will be accessible at (likely 80 or 443 if running behind a reverse proxy). Clients will try to access PeerTube at this port.";
};
enableWebHttps = lib.mkOption {
type = lib.types.bool;
default = false;
description = lib.mdDoc "Whether clients will access your PeerTube instance with HTTPS. Does NOT configure the PeerTube webserver itself to listen for incoming HTTPS connections.";
};
dataDirs = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
example = [ "/opt/peertube/storage" "/var/cache/peertube" ];
description = lib.mdDoc "Allow access to custom data locations.";
};
serviceEnvironmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/keys/peertube/password-init-root";
description = lib.mdDoc ''
Set environment variables for the service. Mainly useful for setting the initial root password.
For example write to file:
PT_INITIAL_ROOT_PASSWORD=changeme
'';
};
settings = lib.mkOption {
type = settingsFormat.type;
example = lib.literalExpression ''
{
listen = {
hostname = "0.0.0.0";
};
log = {
level = "debug";
};
storage = {
tmp = "/opt/data/peertube/storage/tmp/";
logs = "/opt/data/peertube/storage/logs/";
cache = "/opt/data/peertube/storage/cache/";
};
}
'';
description = lib.mdDoc "Configuration for peertube.";
};
configureNginx = lib.mkOption {
type = lib.types.bool;
default = false;
description = lib.mdDoc "Configure nginx as a reverse proxy for peertube.";
};
secrets = {
secretsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/peertube";
description = lib.mdDoc ''
Secrets to run PeerTube.
Generate one using `openssl rand -hex 32`
'';
};
};
database = {
createLocally = lib.mkOption {
type = lib.types.bool;
default = false;
description = lib.mdDoc "Configure local PostgreSQL database server for PeerTube.";
};
host = lib.mkOption {
type = lib.types.str;
default = if cfg.database.createLocally then "/run/postgresql" else null;
defaultText = lib.literalExpression ''
if config.${opt.database.createLocally}
then "/run/postgresql"
else null
'';
example = "192.168.15.47";
description = lib.mdDoc "Database host address or unix socket.";
};
port = lib.mkOption {
type = lib.types.port;
default = 5432;
description = lib.mdDoc "Database host port.";
};
name = lib.mkOption {
type = lib.types.str;
default = "peertube";
description = lib.mdDoc "Database name.";
};
user = lib.mkOption {
type = lib.types.str;
default = "peertube";
description = lib.mdDoc "Database user.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/keys/peertube/password-postgresql";
description = lib.mdDoc "Password for PostgreSQL database.";
};
};
redis = {
createLocally = lib.mkOption {
type = lib.types.bool;
default = false;
description = lib.mdDoc "Configure local Redis server for PeerTube.";
};
host = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then "127.0.0.1" else null;
defaultText = lib.literalExpression ''
if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket}
then "127.0.0.1"
else null
'';
description = lib.mdDoc "Redis host.";
};
port = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = if cfg.redis.createLocally && cfg.redis.enableUnixSocket then null else 31638;
defaultText = lib.literalExpression ''
if config.${opt.redis.createLocally} && config.${opt.redis.enableUnixSocket}
then null
else 6379
'';
description = lib.mdDoc "Redis port.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/keys/peertube/password-redis-db";
description = lib.mdDoc "Password for redis database.";
};
enableUnixSocket = lib.mkOption {
type = lib.types.bool;
default = cfg.redis.createLocally;
defaultText = lib.literalExpression "config.${opt.redis.createLocally}";
description = lib.mdDoc "Use Unix socket.";
};
};
smtp = {
createLocally = lib.mkOption {
type = lib.types.bool;
default = false;
description = lib.mdDoc "Configure local Postfix SMTP server for PeerTube.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/keys/peertube/password-smtp";
description = lib.mdDoc "Password for smtp server.";
};
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.peertube;
defaultText = lib.literalExpression "pkgs.peertube";
description = lib.mdDoc "PeerTube package to use.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{ assertion = cfg.serviceEnvironmentFile == null || !lib.hasPrefix builtins.storeDir cfg.serviceEnvironmentFile;
message = ''
points to
a file in the Nix store. You should use a quoted absolute path to
prevent this.
'';
}
{ assertion = cfg.secrets.secretsFile != null;
message = ''
needs to be set.
'';
}
{ assertion = !(cfg.redis.enableUnixSocket && (cfg.redis.host != null || cfg.redis.port != null));
message = ''
and redis network connection ( or ) enabled. Disable either of them.
'';
}
{ assertion = cfg.redis.enableUnixSocket || (cfg.redis.host != null && cfg.redis.port != null);
message = ''
and needs to be set if is not enabled.
'';
}
{ assertion = cfg.redis.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.redis.passwordFile;
message = ''
points to
a file in the Nix store. You should use a quoted absolute path to
prevent this.
'';
}
{ assertion = cfg.database.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.database.passwordFile;
message = ''
points to
a file in the Nix store. You should use a quoted absolute path to
prevent this.
'';
}
{ assertion = cfg.smtp.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.smtp.passwordFile;
message = ''
points to
a file in the Nix store. You should use a quoted absolute path to
prevent this.
'';
}
];
services.peertube.settings = lib.mkMerge [
{
listen = {
port = cfg.listenHttp;
};
webserver = {
https = (if cfg.enableWebHttps then true else false);
hostname = "${cfg.localDomain}";
port = cfg.listenWeb;
};
database = {
hostname = "${cfg.database.host}";
port = cfg.database.port;
name = "${cfg.database.name}";
username = "${cfg.database.user}";
};
redis = {
hostname = "${toString cfg.redis.host}";
port = (lib.optionalString (cfg.redis.port != null) cfg.redis.port);
};
storage = {
tmp = lib.mkDefault "/var/lib/peertube/storage/tmp/";
tmp_persistent = lib.mkDefault "/var/lib/peertube/storage/tmp_persistent/";
bin = lib.mkDefault "/var/lib/peertube/storage/bin/";
avatars = lib.mkDefault "/var/lib/peertube/storage/avatars/";
videos = lib.mkDefault "/var/lib/peertube/storage/videos/";
streaming_playlists = lib.mkDefault "/var/lib/peertube/storage/streaming-playlists/";
redundancy = lib.mkDefault "/var/lib/peertube/storage/redundancy/";
logs = lib.mkDefault "/var/lib/peertube/storage/logs/";
previews = lib.mkDefault "/var/lib/peertube/storage/previews/";
thumbnails = lib.mkDefault "/var/lib/peertube/storage/thumbnails/";
torrents = lib.mkDefault "/var/lib/peertube/storage/torrents/";
captions = lib.mkDefault "/var/lib/peertube/storage/captions/";
cache = lib.mkDefault "/var/lib/peertube/storage/cache/";
plugins = lib.mkDefault "/var/lib/peertube/storage/plugins/";
well_known = lib.mkDefault "/var/lib/peertube/storage/well_known/";
client_overrides = lib.mkDefault "/var/lib/peertube/storage/client-overrides/";
};
import = {
videos = {
http = {
youtube_dl_release = {
python_path = "${pkgs.python3}/bin/python";
};
};
};
};
}
(lib.mkIf cfg.redis.enableUnixSocket { redis = { socket = "/run/redis-peertube/redis.sock"; }; })
];
systemd.tmpfiles.rules = [
"d '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
"z '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
"d '/var/lib/peertube/www' 0750 ${cfg.user} ${cfg.group} - -"
"z '/var/lib/peertube/www' 0750 ${cfg.user} ${cfg.group} - -"
];
systemd.services.peertube-init-db = lib.mkIf cfg.database.createLocally {
description = "Initialization database for PeerTube daemon";
after = [ "network.target" "postgresql.service" ];
requires = [ "postgresql.service" ];
script = let
psqlSetupCommands = pkgs.writeText "peertube-init.sql" ''
SELECT 'CREATE USER "${cfg.database.user}"' WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${cfg.database.user}')\gexec
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
\c '${cfg.database.name}'
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;
'';
in "${config.services.postgresql.package}/bin/psql -f ${psqlSetupCommands}";
serviceConfig = {
Type = "oneshot";
WorkingDirectory = cfg.package;
# User and group
User = "postgres";
Group = "postgres";
# Sandboxing
RestrictAddressFamilies = [ "AF_UNIX" ];
MemoryDenyWriteExecute = true;
# System Call Filtering
SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
} // cfgService;
};
systemd.services.peertube = {
description = "PeerTube daemon";
after = [ "network.target" ]
++ lib.optional cfg.redis.createLocally "redis-peertube.service"
++ lib.optionals cfg.database.createLocally [ "postgresql.service" "peertube-init-db.service" ];
requires = lib.optional cfg.redis.createLocally "redis-peertube.service"
++ lib.optionals cfg.database.createLocally [ "postgresql.service" "peertube-init-db.service" ];
wantedBy = [ "multi-user.target" ];
environment = env;
path = with pkgs; [ bashInteractive ffmpeg nodejs_18 openssl yarn python3 ];
script = ''
#!/bin/sh
umask 077
cat > /var/lib/peertube/config/local.yaml <