1 { config, pkgs, lib, ... }:
3 cfg = config.services.paperless;
5 defaultUser = "paperless";
6 defaultFont = "${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf";
8 # Don't start a redis instance if the user sets a custom redis connection
9 enableRedis = !(cfg.settings ? PAPERLESS_REDIS);
10 redisServer = config.services.redis.servers.paperless;
13 PAPERLESS_DATA_DIR = cfg.dataDir;
14 PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
15 PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
16 PAPERLESS_THUMBNAIL_FONT_NAME = defaultFont;
17 GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
18 } // lib.optionalAttrs (config.time.timeZone != null) {
19 PAPERLESS_TIME_ZONE = config.time.timeZone;
20 } // lib.optionalAttrs enableRedis {
21 PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
22 } // lib.optionalAttrs (cfg.settings.PAPERLESS_ENABLE_NLTK or true) {
23 PAPERLESS_NLTK_DIR = pkgs.symlinkJoin {
24 name = "paperless_ngx_nltk_data";
25 paths = cfg.package.nltkData;
27 } // lib.optionalAttrs (cfg.openMPThreadingWorkaround) {
28 OMP_NUM_THREADS = "1";
29 } // (lib.mapAttrs (_: s:
30 if (lib.isAttrs s || lib.isList s) then builtins.toJSON s
31 else if lib.isBool s then lib.boolToString s
35 manage = pkgs.writeShellScript "manage" ''
36 set -o allexport # Export the following env vars
37 ${lib.toShellVars env}
38 ${lib.optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
39 exec ${cfg.package}/bin/paperless-ngx "$@"
42 defaultServiceConfig = {
43 Slice = "system-paperless.slice";
50 CacheDirectory = "paperless";
51 CapabilityBoundingSet = "";
52 # ProtectClock adds DeviceAllow=char-rtc r
54 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
55 LockPersonality = true;
56 MemoryDenyWriteExecute = true;
57 NoNewPrivileges = true;
58 PrivateDevices = true;
60 PrivateNetwork = true;
64 # Breaks if the home dir of the user is in /home
66 ProtectHostname = true;
67 ProtectSystem = "strict";
68 ProtectControlGroups = true;
69 ProtectKernelLogs = true;
70 ProtectKernelModules = true;
71 ProtectKernelTunables = true;
72 ProtectProc = "invisible";
73 # Don't restrict ProcSubset because django-q requires read access to /proc/stat
74 # to query CPU and memory information.
75 # Note that /proc only contains processes of user `paperless`, so this is safe.
77 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
78 RestrictNamespaces = true;
79 RestrictRealtime = true;
80 RestrictSUIDSGID = true;
81 SupplementaryGroups = lib.optional enableRedis redisServer.user;
82 SystemCallArchitectures = "native";
83 SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
88 meta.maintainers = with lib.maintainers; [ leona SuperSandro2000 erikarvstedt ];
91 (lib.mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
92 (lib.mkRenamedOptionModule [ "services" "paperless" "extraConfig" ] [ "services" "paperless" "settings" ])
95 options.services.paperless = {
96 enable = lib.mkOption {
97 type = lib.types.bool;
102 When started, the Paperless database is automatically created if it doesn't
103 exist and updated if the Paperless package has changed.
104 Both tasks are achieved by running a Django migration.
106 A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
107 `''${dataDir}/paperless-manage`.
111 dataDir = lib.mkOption {
112 type = lib.types.str;
113 default = "/var/lib/paperless";
114 description = "Directory to store the Paperless data.";
117 mediaDir = lib.mkOption {
118 type = lib.types.str;
119 default = "${cfg.dataDir}/media";
120 defaultText = lib.literalExpression ''"''${dataDir}/media"'';
121 description = "Directory to store the Paperless documents.";
124 consumptionDir = lib.mkOption {
125 type = lib.types.str;
126 default = "${cfg.dataDir}/consume";
127 defaultText = lib.literalExpression ''"''${dataDir}/consume"'';
128 description = "Directory from which new documents are imported.";
131 consumptionDirIsPublic = lib.mkOption {
132 type = lib.types.bool;
134 description = "Whether all users can write to the consumption dir.";
137 passwordFile = lib.mkOption {
138 type = lib.types.nullOr lib.types.path;
140 example = "/run/keys/paperless-password";
142 A file containing the superuser password.
144 A superuser is required to access the web interface.
145 If unset, you can create a superuser manually by running
146 `''${dataDir}/paperless-manage createsuperuser`.
148 The default superuser name is `admin`. To change it, set
149 option {option}`settings.PAPERLESS_ADMIN_USER`.
150 WARNING: When changing the superuser name after the initial setup, the old superuser
151 will continue to exist.
153 To disable login for the web interface, set the following:
154 `settings.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";`.
155 WARNING: Only use this on a trusted system without internet access to Paperless.
159 address = lib.mkOption {
160 type = lib.types.str;
161 default = "localhost";
162 description = "Web interface address.";
165 port = lib.mkOption {
166 type = lib.types.port;
168 description = "Web interface port.";
171 settings = lib.mkOption {
172 type = lib.types.submodule {
173 freeformType = with lib.types; attrsOf (let
174 typeList = [ bool float int str path package ];
175 in oneOf (typeList ++ [ (listOf (oneOf typeList)) (attrsOf (oneOf typeList)) ]));
179 Extra paperless config options.
181 See [the documentation](https://docs.paperless-ngx.com/configuration/) for available options.
183 Note that some settings such as `PAPERLESS_CONSUMER_IGNORE_PATTERN` expect JSON values.
184 Settings declared as lists or attrsets will automatically be serialised into JSON strings for your convenience.
187 PAPERLESS_OCR_LANGUAGE = "deu+eng";
188 PAPERLESS_CONSUMER_IGNORE_PATTERN = [ ".DS_STORE/*" "desktop.ini" ];
189 PAPERLESS_OCR_USER_ARGS = {
191 pdfa_image_compression = "lossless";
196 user = lib.mkOption {
197 type = lib.types.str;
198 default = defaultUser;
199 description = "User under which Paperless runs.";
202 package = lib.mkPackageOption pkgs "paperless-ngx" { } // {
203 apply = pkg: pkg.override {
204 tesseract5 = pkg.tesseract5.override {
205 # always enable detection modules
206 # tesseract fails to build when eng is not present
207 enableLanguages = if cfg.settings ? PAPERLESS_OCR_LANGUAGE then
209 [ "equ" "osd" "eng" ]
210 ++ lib.splitString "+" cfg.settings.PAPERLESS_OCR_LANGUAGE
217 openMPThreadingWorkaround = lib.mkEnableOption ''
218 a workaround for document classifier timeouts.
220 Paperless uses OpenBLAS via scikit-learn for document classification.
222 The default is to use threading for OpenMP but this would cause the
223 document classifier to spin on one core seemingly indefinitely if there
224 are large amounts of classes per classification; causing it to
225 effectively never complete due to running into timeouts.
227 This sets `OMP_NUM_THREADS` to `1` in order to mitigate the issue. See
228 https://github.com/NixOS/nixpkgs/issues/240591 for more information
229 '' // lib.mkOption { default = true; };
231 environmentFile = lib.mkOption {
232 type = lib.types.nullOr lib.types.path;
234 example = "/run/secrets/paperless";
236 Path to a file containing extra paperless config options in the systemd `EnvironmentFile`
237 format. Refer to the [documentation](https://docs.paperless-ngx.com/configuration/) for
240 This can be used to pass secrets to paperless without putting them in the Nix store.
242 To set a database password, point `environmentFile` at a file containing:
244 PAPERLESS_DBPASS=<pass>
250 createLocally = lib.mkOption {
251 type = lib.types.bool;
254 Configure local PostgreSQL database server for Paperless.
260 config = lib.mkIf cfg.enable {
261 services.redis.servers.paperless.enable = lib.mkIf enableRedis true;
263 services.postgresql = lib.mkIf cfg.database.createLocally {
265 ensureDatabases = [ "paperless" ];
267 name = config.services.paperless.user;
268 ensureDBOwnership = true;
272 services.paperless.settings = lib.mkIf cfg.database.createLocally {
273 PAPERLESS_DBENGINE = "postgresql";
274 PAPERLESS_DBHOST = "/run/postgresql";
275 PAPERLESS_DBNAME = "paperless";
276 PAPERLESS_DBUSER = "paperless";
279 systemd.slices.system-paperless = {
280 description = "Paperless Document Management System Slice";
281 documentation = [ "https://docs.paperless-ngx.com" ];
284 systemd.tmpfiles.settings."10-paperless" = let
287 inherit (config.users.users.${cfg.user}) group;
290 "${cfg.dataDir}".d = defaultRule;
291 "${cfg.mediaDir}".d = defaultRule;
292 "${cfg.consumptionDir}".d = if cfg.consumptionDirIsPublic then { mode = "777"; } else defaultRule;
295 systemd.services.paperless-scheduler = {
296 description = "Paperless Celery Beat";
297 wantedBy = [ "multi-user.target" ];
298 wants = [ "paperless-consumer.service" "paperless-web.service" "paperless-task-queue.service" ];
299 serviceConfig = defaultServiceConfig // {
301 ExecStart = "${cfg.package}/bin/celery --app paperless beat --loglevel INFO";
302 Restart = "on-failure";
303 LoadCredential = lib.optionalString (cfg.passwordFile != null) "PAPERLESS_ADMIN_PASSWORD:${cfg.passwordFile}";
308 ln -sf ${manage} ${cfg.dataDir}/paperless-manage
310 # Auto-migrate on first run or if the package has changed
311 versionFile="${cfg.dataDir}/src-version"
312 version=$(cat "$versionFile" 2>/dev/null || echo 0)
314 if [[ $version != ${cfg.package.version} ]]; then
315 ${cfg.package}/bin/paperless-ngx migrate
317 # Parse old version string format for backwards compatibility
318 version=$(echo "$version" | grep -ohP '[^-]+$')
322 [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]]
325 if versionLessThan 1.12.0; then
326 # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1
327 echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade."
328 ${cfg.package}/bin/paperless-ngx document_index reindex
331 echo ${cfg.package.version} > "$versionFile"
334 + lib.optionalString (cfg.passwordFile != null) ''
335 export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
336 PAPERLESS_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PAPERLESS_ADMIN_PASSWORD")
337 export PAPERLESS_ADMIN_PASSWORD
338 superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
339 superuserStateFile="${cfg.dataDir}/superuser-state"
341 if [[ $(cat "$superuserStateFile" 2>/dev/null) != "$superuserState" ]]; then
342 ${cfg.package}/bin/paperless-ngx manage_superuser
343 echo "$superuserState" > "$superuserStateFile"
346 requires = lib.optional cfg.database.createLocally "postgresql.service";
347 after = lib.optional enableRedis "redis-paperless.service"
348 ++ lib.optional cfg.database.createLocally "postgresql.service";
351 systemd.services.paperless-task-queue = {
352 description = "Paperless Celery Workers";
353 requires = lib.optional cfg.database.createLocally "postgresql.service";
354 after = [ "paperless-scheduler.service" ]
355 ++ lib.optional cfg.database.createLocally "postgresql.service";
356 serviceConfig = defaultServiceConfig // {
358 ExecStart = "${cfg.package}/bin/celery --app paperless worker --loglevel INFO";
359 Restart = "on-failure";
360 # The `mbind` syscall is needed for running the classifier.
361 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
362 # Needs to talk to mail server for automated import rules
363 PrivateNetwork = false;
368 systemd.services.paperless-consumer = {
369 description = "Paperless document consumer";
370 # Bind to `paperless-scheduler` so that the consumer never runs
372 bindsTo = [ "paperless-scheduler.service" ];
373 requires = lib.optional cfg.database.createLocally "postgresql.service";
374 after = [ "paperless-scheduler.service" ]
375 ++ lib.optional cfg.database.createLocally "postgresql.service";
376 serviceConfig = defaultServiceConfig // {
378 ExecStart = "${cfg.package}/bin/paperless-ngx document_consumer";
379 Restart = "on-failure";
382 # Allow the consumer to access the private /tmp directory of the server.
383 # This is required to support consuming files via a local folder.
384 unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
387 systemd.services.paperless-web = {
388 description = "Paperless web server";
389 # Bind to `paperless-scheduler` so that the web server never runs
391 bindsTo = [ "paperless-scheduler.service" ];
392 requires = lib.optional cfg.database.createLocally "postgresql.service";
393 after = [ "paperless-scheduler.service" ]
394 ++ lib.optional cfg.database.createLocally "postgresql.service";
395 # Setup PAPERLESS_SECRET_KEY.
396 # If this environment variable is left unset, paperless-ngx defaults
397 # to a well-known value, which is insecure.
399 secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key";
401 if [[ ! -f '${secretKeyFile}' ]]; then
404 tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}'
407 PAPERLESS_SECRET_KEY="$(cat '${secretKeyFile}')"
408 export PAPERLESS_SECRET_KEY
409 if [[ ! $PAPERLESS_SECRET_KEY ]]; then
410 echo "PAPERLESS_SECRET_KEY is empty, refusing to start."
413 exec ${cfg.package.python.pkgs.gunicorn}/bin/gunicorn \
414 -c ${cfg.package}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
416 serviceConfig = defaultServiceConfig // {
418 Restart = "on-failure";
421 # gunicorn needs setuid, liblapack needs mbind
422 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ];
423 # Needs to serve web page
424 PrivateNetwork = false;
426 environment = env // {
427 PYTHONPATH = "${cfg.package.python.pkgs.makePythonPath cfg.package.propagatedBuildInputs}:${cfg.package}/lib/paperless-ngx/src";
429 # Allow the web interface to access the private /tmp directory of the server.
430 # This is required to support uploading files via the web interface.
431 unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
434 users = lib.optionalAttrs (cfg.user == defaultUser) {
435 users.${defaultUser} = {
437 uid = config.ids.uids.paperless;
441 groups.${defaultUser} = {
442 gid = config.ids.gids.paperless;