31 filterRecursiveNull = o:
33 mapAttrs (_: v: filterRecursiveNull v) (filterAttrs (_: v: v != null) o)
35 map filterRecursiveNull (filter (v: v != null) o)
39 cfg = config.services.pretix;
40 format = pkgs.formats.ini { };
42 configFile = format.generate "pretix.cfg" (filterRecursiveNull cfg.settings);
44 finalPackage = cfg.package.override {
45 inherit (cfg) plugins;
48 pythonEnv = cfg.package.python.buildEnv.override {
49 extraLibs = with cfg.package.python.pkgs; [
50 (toPythonModule finalPackage)
53 ++ lib.optionals (cfg.settings.memcached.location != null)
54 cfg.package.optional-dependencies.memcached
58 withRedis = cfg.settings.redis.location != null;
62 maintainers = with maintainers; [ hexa ];
65 options.services.pretix = {
66 enable = mkEnableOption "Pretix, a ticket shop application for conferences, festivals, concerts, etc";
68 package = mkPackageOption pkgs "pretix" { };
74 Group under which pretix should run.
82 User under which pretix should run.
86 environmentFile = mkOption {
87 type = types.nullOr types.path;
89 example = "/run/keys/pretix-secrets.env";
91 Environment file to pass secret configuration values.
93 Each line must follow the `PRETIX_SECTION_KEY=value` pattern.
98 type = types.listOf types.package;
100 example = literalExpression ''
101 with config.services.pretix.package.plugins; [
107 Pretix plugins to install into the Python environment.
111 gunicorn.extraArgs = mkOption {
112 type = with types; listOf str;
119 "--max-requests=1200"
120 "--max-requests-jitter=50"
124 Extra arguments to pass to gunicorn.
125 See <https://docs.pretix.eu/en/latest/admin/installation/manual_smallscale.html#start-pretix-as-a-service> for details.
127 apply = escapeShellArgs;
131 extraArgs = mkOption {
132 type = with types; listOf str;
135 Extra arguments to pass to celery.
137 See <https://docs.celeryq.dev/en/stable/reference/cli.html#celery-worker> for more info.
139 apply = utils.escapeSystemdExecArgs;
149 Whether to set up an nginx virtual host.
155 example = "talks.example.com";
157 The domain name under which to set up the virtual host.
162 database.createLocally = mkOption {
167 Whether to automatically set up the database on the local DBMS instance.
169 Only supported for PostgreSQL. Not required for sqlite.
173 settings = mkOption {
174 type = types.submodule {
175 freeformType = format.type;
178 instance_name = mkOption {
180 example = "tickets.example.com";
182 The name of this installation.
188 example = "https://tickets.example.com";
190 The installation’s full URL, without a trailing slash.
194 cachedir = mkOption {
196 default = "/var/cache/pretix";
198 Directory for storing temporary files.
204 default = "/var/lib/pretix";
206 Directory for storing user uploads and similar data.
212 default = "/var/log/pretix";
214 Directory for storing log files.
218 currency = mkOption {
223 Default currency for events in its ISO 4217 three-letter code.
227 registration = mkOption {
232 Whether to allow registration of new admin users.
243 default = "postgresql";
245 Database backend to use.
247 Only postgresql is recommended for production setups.
252 type = with types; nullOr str;
253 default = if cfg.settings.database.backend == "postgresql" then "/run/postgresql" else null;
254 defaultText = literalExpression ''
255 if config.services.pretix.settings..database.backend == "postgresql" then "/run/postgresql"
259 Database host or socket path.
283 example = "tickets@example.com";
285 E-Mail address used in the `FROM` header of outgoing mails.
291 default = "localhost";
292 example = "mail.example.com";
294 Hostname of the SMTP server use for mail delivery.
303 Port of the SMTP server to use for mail delivery.
311 default = "redis+socket://${config.services.redis.servers.pretix.unixSocket}?virtual_host=1";
312 defaultText = literalExpression ''
313 redis+socket://''${config.services.redis.servers.pretix.unixSocket}?virtual_host=1
316 URI to the celery backend used for the asynchronous job queue.
322 default = "redis+socket://${config.services.redis.servers.pretix.unixSocket}?virtual_host=2";
323 defaultText = literalExpression ''
324 redis+socket://''${config.services.redis.servers.pretix.unixSocket}?virtual_host=2
327 URI to the celery broker used for the asynchronous job queue.
333 location = mkOption {
334 type = with types; nullOr str;
335 default = "unix://${config.services.redis.servers.pretix.unixSocket}?db=0";
336 defaultText = literalExpression ''
337 "unix://''${config.services.redis.servers.pretix.unixSocket}?db=0"
340 URI to the redis server, used to speed up locking, caching and session storage.
344 sessions = mkOption {
349 Whether to use redis as the session storage.
355 location = mkOption {
356 type = with types; nullOr str;
358 example = "127.0.0.1:11211";
360 The `host:port` combination or the path to the UNIX socket of a memcached instance.
362 Can be used instead of Redis for caching.
370 default = getExe pkgs.pdftk;
371 defaultText = literalExpression ''
372 lib.getExe pkgs.pdftk
375 Path to the pdftk executable.
383 pretix configuration as a Nix attribute set. All settings can also be passed
384 from the environment.
386 See <https://docs.pretix.eu/en/latest/admin/config.html> for possible options.
391 config = mkIf cfg.enable {
392 # https://docs.pretix.eu/en/latest/admin/installation/index.html
394 environment.systemPackages = [
395 (pkgs.writeScriptBin "pretix-manage" ''
396 cd ${cfg.settings.pretix.datadir}
398 if [[ "$USER" != ${cfg.user} ]]; then
399 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} ${optionalString withRedis "-g redis-pretix"} --preserve-env=PRETIX_CONFIG_FILE'
401 export PRETIX_CONFIG_FILE=${configFile}
402 $sudo ${getExe' pythonEnv "pretix-manage"} "$@"
406 services.logrotate.settings.pretix = {
407 files = "${cfg.settings.pretix.logdir}/*.log";
408 su = "${cfg.user} ${cfg.group}";
409 frequency = "weekly";
416 nginx = mkIf cfg.nginx.enable {
418 recommendedGzipSettings = mkDefault true;
419 recommendedOptimisation = mkDefault true;
420 recommendedProxySettings = mkDefault true;
421 recommendedTlsSettings = mkDefault true;
422 upstreams.pretix.servers."unix:/run/pretix/pretix.sock" = { };
423 virtualHosts.${cfg.nginx.domain} = {
424 # https://docs.pretix.eu/en/latest/admin/installation/manual_smallscale.html#ssl
426 more_set_headers Referrer-Policy same-origin;
427 more_set_headers X-Content-Type-Options nosniff;
430 "/".proxyPass = "http://pretix";
432 alias = "${cfg.settings.pretix.datadir}/media/";
438 "^~ /media/(cachedfiles|invoices)" = {
445 alias = "${finalPackage}/${cfg.package.python.sitePackages}/pretix/static.dist/";
448 more_set_headers Cache-Control "public";
456 postgresql = mkIf (cfg.database.createLocally && cfg.settings.database.backend == "postgresql") {
459 name = cfg.settings.database.user;
460 ensureDBOwnership = true;
462 ensureDatabases = [ cfg.settings.database.name ];
465 redis.servers.pretix.enable = withRedis;
468 systemd.services = let
470 environment.PRETIX_CONFIG_FILE = configFile;
474 EnvironmentFile = optionals (cfg.environmentFile != null) [
480 StateDirectoryMode = "0750";
481 CacheDirectory = "pretix";
482 LogsDirectory = "pretix";
483 WorkingDirectory = cfg.settings.pretix.datadir;
484 SupplementaryGroups = optionals withRedis [
487 AmbientCapabilities = "";
488 CapabilityBoundingSet = [ "" ];
489 DevicePolicy = "closed";
490 LockPersonality = true;
491 MemoryDenyWriteExecute = false; # required by pdftk
492 NoNewPrivileges = true;
493 PrivateDevices = true;
496 ProtectControlGroups = true;
498 ProtectHostname = true;
499 ProtectKernelLogs = true;
500 ProtectKernelModules = true;
501 ProtectKernelTunables = true;
502 ProtectProc = "invisible";
503 ProtectSystem = "strict";
505 RestrictAddressFamilies = [
510 RestrictNamespaces = true;
511 RestrictRealtime = true;
512 RestrictSUIDSGID = true;
513 SystemCallArchitectures = "native";
523 pretix-web = recursiveUpdate commonUnitConfig {
524 description = "pretix web service";
527 "redis-pretix.service"
530 wantedBy = [ "multi-user.target" ];
532 versionFile="${cfg.settings.pretix.datadir}/.version"
533 version=$(cat "$versionFile" 2>/dev/null || echo 0)
535 pluginsFile="${cfg.settings.pretix.datadir}/.plugins"
536 plugins=$(cat "$pluginsFile" 2>/dev/null || echo "")
537 configuredPlugins="${concatMapStringsSep "|" (package: package.name) cfg.plugins}"
539 if [[ $version != ${cfg.package.version} || $plugins != $configuredPlugins ]]; then
540 ${getExe' pythonEnv "pretix-manage"} migrate
542 echo "${cfg.package.version}" > "$versionFile"
543 echo "$configuredPlugins" > "$pluginsFile"
547 TimeoutStartSec = "15min";
548 ExecStart = "${getExe' pythonEnv "gunicorn"} --bind unix:/run/pretix/pretix.sock ${cfg.gunicorn.extraArgs} pretix.wsgi";
549 RuntimeDirectory = "pretix";
550 Restart = "on-failure";
554 pretix-periodic = recursiveUpdate commonUnitConfig {
555 description = "pretix periodic task runner";
557 startAt = [ "*:3,18,33,48" ];
560 ExecStart = "${getExe' pythonEnv "pretix-manage"} runperiodic";
564 pretix-worker = recursiveUpdate commonUnitConfig {
565 description = "pretix asynchronous job runner";
568 "redis-pretix.service"
571 wantedBy = [ "multi-user.target" ];
573 ExecStart = "${getExe' pythonEnv "celery"} -A pretix.celery_app worker ${cfg.celery.extraArgs}";
574 Restart = "on-failure";
578 nginx.serviceConfig.SupplementaryGroups = mkIf cfg.nginx.enable [ "pretix" ];
581 systemd.sockets.pretix-web.socketConfig = {
582 ListenStream = "/run/pretix/pretix.sock";
583 SocketUser = "nginx";
587 groups.${cfg.group} = {};
588 users.${cfg.user} = {