1 { config, pkgs, lib, ... }:
5 cfg = config.services.paperless;
7 defaultUser = "paperless";
8 defaultFont = "${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf";
10 # Don't start a redis instance if the user sets a custom redis connection
11 enableRedis = !(cfg.settings ? PAPERLESS_REDIS);
12 redisServer = config.services.redis.servers.paperless;
15 PAPERLESS_DATA_DIR = cfg.dataDir;
16 PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
17 PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
18 PAPERLESS_THUMBNAIL_FONT_NAME = defaultFont;
19 GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
20 } // optionalAttrs (config.time.timeZone != null) {
21 PAPERLESS_TIME_ZONE = config.time.timeZone;
22 } // optionalAttrs enableRedis {
23 PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
24 } // optionalAttrs (cfg.settings.PAPERLESS_ENABLE_NLTK or true) {
25 PAPERLESS_NLTK_DIR = pkgs.symlinkJoin {
26 name = "paperless_ngx_nltk_data";
27 paths = cfg.package.nltkData;
29 } // optionalAttrs (cfg.openMPThreadingWorkaround) {
30 OMP_NUM_THREADS = "1";
31 } // (lib.mapAttrs (_: s:
32 if (lib.isAttrs s || lib.isList s) then builtins.toJSON s
33 else if lib.isBool s then lib.boolToString s
37 manage = pkgs.writeShellScript "manage" ''
38 set -o allexport # Export the following env vars
39 ${lib.toShellVars env}
40 exec ${cfg.package}/bin/paperless-ngx "$@"
43 defaultServiceConfig = {
44 Slice = "system-paperless.slice";
51 CacheDirectory = "paperless";
52 CapabilityBoundingSet = "";
53 # ProtectClock adds DeviceAllow=char-rtc r
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 = optional enableRedis redisServer.user;
82 SystemCallArchitectures = "native";
83 SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
88 meta.maintainers = with maintainers; [ leona SuperSandro2000 erikarvstedt ];
91 (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
92 (mkRenamedOptionModule [ "services" "paperless" "extraConfig" ] [ "services" "paperless" "settings" ])
95 options.services.paperless = {
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`.
113 default = "/var/lib/paperless";
114 description = "Directory to store the Paperless data.";
117 mediaDir = mkOption {
119 default = "${cfg.dataDir}/media";
120 defaultText = literalExpression ''"''${dataDir}/media"'';
121 description = "Directory to store the Paperless documents.";
124 consumptionDir = mkOption {
126 default = "${cfg.dataDir}/consume";
127 defaultText = literalExpression ''"''${dataDir}/consume"'';
128 description = "Directory from which new documents are imported.";
131 consumptionDirIsPublic = mkOption {
134 description = "Whether all users can write to the consumption dir.";
137 passwordFile = mkOption {
138 type = types.nullOr 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.
161 default = "localhost";
162 description = "Web interface address.";
168 description = "Web interface port.";
171 settings = 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_DBHOST = "/run/postgresql";
189 PAPERLESS_CONSUMER_IGNORE_PATTERN = [ ".DS_STORE/*" "desktop.ini" ];
190 PAPERLESS_OCR_USER_ARGS = {
192 pdfa_image_compression = "lossless";
199 default = defaultUser;
200 description = "User under which Paperless runs.";
203 package = mkPackageOption pkgs "paperless-ngx" { } // {
204 apply = pkg: pkg.override {
205 tesseract5 = pkg.tesseract5.override {
206 # always enable detection modules
207 # tesseract fails to build when eng is not present
208 enableLanguages = if cfg.settings ? PAPERLESS_OCR_LANGUAGE then
210 [ "equ" "osd" "eng" ]
211 ++ lib.splitString "+" cfg.settings.PAPERLESS_OCR_LANGUAGE
218 openMPThreadingWorkaround = mkEnableOption ''
219 a workaround for document classifier timeouts.
221 Paperless uses OpenBLAS via scikit-learn for document classification.
223 The default is to use threading for OpenMP but this would cause the
224 document classifier to spin on one core seemingly indefinitely if there
225 are large amounts of classes per classification; causing it to
226 effectively never complete due to running into timeouts.
228 This sets `OMP_NUM_THREADS` to `1` in order to mitigate the issue. See
229 https://github.com/NixOS/nixpkgs/issues/240591 for more information
230 '' // mkOption { default = true; };
233 config = mkIf cfg.enable {
234 services.redis.servers.paperless.enable = mkIf enableRedis true;
236 systemd.slices.system-paperless = {
237 description = "Paperless Document Management System Slice";
238 documentation = [ "https://docs.paperless-ngx.com" ];
241 systemd.tmpfiles.settings."10-paperless" = let
244 inherit (config.users.users.${cfg.user}) group;
247 "${cfg.dataDir}".d = defaultRule;
248 "${cfg.mediaDir}".d = defaultRule;
249 "${cfg.consumptionDir}".d = if cfg.consumptionDirIsPublic then { mode = "777"; } else defaultRule;
252 systemd.services.paperless-scheduler = {
253 description = "Paperless Celery Beat";
254 wantedBy = [ "multi-user.target" ];
255 wants = [ "paperless-consumer.service" "paperless-web.service" "paperless-task-queue.service" ];
256 serviceConfig = defaultServiceConfig // {
258 ExecStart = "${cfg.package}/bin/celery --app paperless beat --loglevel INFO";
259 Restart = "on-failure";
260 LoadCredential = lib.optionalString (cfg.passwordFile != null) "PAPERLESS_ADMIN_PASSWORD:${cfg.passwordFile}";
265 ln -sf ${manage} ${cfg.dataDir}/paperless-manage
267 # Auto-migrate on first run or if the package has changed
268 versionFile="${cfg.dataDir}/src-version"
269 version=$(cat "$versionFile" 2>/dev/null || echo 0)
271 if [[ $version != ${cfg.package.version} ]]; then
272 ${cfg.package}/bin/paperless-ngx migrate
274 # Parse old version string format for backwards compatibility
275 version=$(echo "$version" | grep -ohP '[^-]+$')
279 [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]]
282 if versionLessThan 1.12.0; then
283 # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1
284 echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade."
285 ${cfg.package}/bin/paperless-ngx document_index reindex
288 echo ${cfg.package.version} > "$versionFile"
291 + optionalString (cfg.passwordFile != null) ''
292 export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
293 export PAPERLESS_ADMIN_PASSWORD=$(cat $CREDENTIALS_DIRECTORY/PAPERLESS_ADMIN_PASSWORD)
294 superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
295 superuserStateFile="${cfg.dataDir}/superuser-state"
297 if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
298 ${cfg.package}/bin/paperless-ngx manage_superuser
299 echo "$superuserState" > "$superuserStateFile"
302 } // optionalAttrs enableRedis {
303 after = [ "redis-paperless.service" ];
306 systemd.services.paperless-task-queue = {
307 description = "Paperless Celery Workers";
308 after = [ "paperless-scheduler.service" ];
309 serviceConfig = defaultServiceConfig // {
311 ExecStart = "${cfg.package}/bin/celery --app paperless worker --loglevel INFO";
312 Restart = "on-failure";
313 # The `mbind` syscall is needed for running the classifier.
314 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
315 # Needs to talk to mail server for automated import rules
316 PrivateNetwork = false;
321 systemd.services.paperless-consumer = {
322 description = "Paperless document consumer";
323 # Bind to `paperless-scheduler` so that the consumer never runs
325 bindsTo = [ "paperless-scheduler.service" ];
326 after = [ "paperless-scheduler.service" ];
327 serviceConfig = defaultServiceConfig // {
329 ExecStart = "${cfg.package}/bin/paperless-ngx document_consumer";
330 Restart = "on-failure";
333 # Allow the consumer to access the private /tmp directory of the server.
334 # This is required to support consuming files via a local folder.
335 unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
338 systemd.services.paperless-web = {
339 description = "Paperless web server";
340 # Bind to `paperless-scheduler` so that the web server never runs
342 bindsTo = [ "paperless-scheduler.service" ];
343 after = [ "paperless-scheduler.service" ];
344 # Setup PAPERLESS_SECRET_KEY.
345 # If this environment variable is left unset, paperless-ngx defaults
346 # to a well-known value, which is insecure.
348 secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key";
350 if [[ ! -f '${secretKeyFile}' ]]; then
353 tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}'
356 export PAPERLESS_SECRET_KEY=$(cat '${secretKeyFile}')
357 if [[ ! $PAPERLESS_SECRET_KEY ]]; then
358 echo "PAPERLESS_SECRET_KEY is empty, refusing to start."
361 exec ${cfg.package.python.pkgs.gunicorn}/bin/gunicorn \
362 -c ${cfg.package}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
364 serviceConfig = defaultServiceConfig // {
366 Restart = "on-failure";
369 # gunicorn needs setuid, liblapack needs mbind
370 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ];
371 # Needs to serve web page
372 PrivateNetwork = false;
373 } // lib.optionalAttrs (cfg.port < 1024) {
374 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
375 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
377 environment = env // {
378 PYTHONPATH = "${cfg.package.python.pkgs.makePythonPath cfg.package.propagatedBuildInputs}:${cfg.package}/lib/paperless-ngx/src";
380 # Allow the web interface to access the private /tmp directory of the server.
381 # This is required to support uploading files via the web interface.
382 unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
385 users = optionalAttrs (cfg.user == defaultUser) {
386 users.${defaultUser} = {
388 uid = config.ids.uids.paperless;
392 groups.${defaultUser} = {
393 gid = config.ids.gids.paperless;