1 { config, pkgs, lib, ... }:
5 cfg = config.services.paperless;
8 defaultUser = "paperless";
10 # Don't start a redis instance if the user sets a custom redis connection
11 enableRedis = !hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
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 GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
19 } // optionalAttrs (config.time.timeZone != null) {
20 PAPERLESS_TIME_ZONE = config.time.timeZone;
21 } // optionalAttrs enableRedis {
22 PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
24 lib.mapAttrs (_: toString) cfg.extraConfig
28 setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
29 in pkgs.writeShellScript "manage" ''
31 exec ${pkg}/bin/paperless-ngx "$@"
35 defaultServiceConfig = {
36 TemporaryFileSystem = "/:ro";
44 ] ++ (optional enableRedis redisServer.unixSocket);
50 CapabilityBoundingSet = "";
51 # ProtectClock adds DeviceAllow=char-rtc r
53 LockPersonality = true;
54 MemoryDenyWriteExecute = true;
55 NoNewPrivileges = true;
56 PrivateDevices = true;
58 PrivateNetwork = true;
62 # Breaks if the home dir of the user is in /home
63 # Also does not add much value in combination with the TemporaryFileSystem.
65 ProtectHostname = true;
66 # Would re-mount paths ignored by temporary root
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" ];
84 # Does not work well with the temporary root
89 meta.maintainers = with maintainers; [ erikarvstedt Flakebi ];
92 (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
95 options.services.paperless = {
97 type = lib.types.bool;
99 description = lib.mdDoc ''
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 = lib.mdDoc "Directory to store the Paperless data.";
117 mediaDir = mkOption {
119 default = "${cfg.dataDir}/media";
120 defaultText = literalExpression ''"''${dataDir}/media"'';
121 description = lib.mdDoc "Directory to store the Paperless documents.";
124 consumptionDir = mkOption {
126 default = "${cfg.dataDir}/consume";
127 defaultText = literalExpression ''"''${dataDir}/consume"'';
128 description = lib.mdDoc "Directory from which new documents are imported.";
131 consumptionDirIsPublic = mkOption {
134 description = lib.mdDoc "Whether all users can write to the consumption dir.";
137 passwordFile = mkOption {
138 type = types.nullOr types.path;
140 example = "/run/keys/paperless-password";
141 description = lib.mdDoc ''
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}`extraConfig.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 `extraConfig.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";`.
155 WARNING: Only use this on a trusted system without internet access to Paperless.
161 default = "localhost";
162 description = lib.mdDoc "Web interface address.";
168 description = lib.mdDoc "Web interface port.";
171 extraConfig = mkOption {
174 description = lib.mdDoc ''
175 Extra paperless config options.
177 See [the documentation](https://paperless-ngx.readthedocs.io/en/latest/configuration.html)
178 for available options.
181 PAPERLESS_OCR_LANGUAGE = "deu+eng";
182 PAPERLESS_DBHOST = "/run/postgresql";
188 default = defaultUser;
189 description = lib.mdDoc "User under which Paperless runs.";
193 type = types.package;
194 default = pkgs.paperless-ngx;
195 defaultText = literalExpression "pkgs.paperless-ngx";
196 description = lib.mdDoc "The Paperless package to use.";
200 config = mkIf cfg.enable {
201 services.redis.servers.paperless.enable = mkIf enableRedis true;
203 systemd.tmpfiles.rules = [
204 "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
205 "d '${cfg.mediaDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
206 (if cfg.consumptionDirIsPublic then
207 "d '${cfg.consumptionDir}' 777 - - - -"
209 "d '${cfg.consumptionDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
213 systemd.services.paperless-scheduler = {
214 description = "Paperless scheduler";
215 serviceConfig = defaultServiceConfig // {
217 ExecStart = "${pkg}/bin/paperless-ngx qcluster";
218 Restart = "on-failure";
219 # The `mbind` syscall is needed for running the classifier.
220 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
221 # Needs to talk to mail server for automated import rules
222 PrivateNetwork = false;
225 wantedBy = [ "multi-user.target" ];
226 wants = [ "paperless-consumer.service" "paperless-web.service" ];
229 ln -sf ${manage} ${cfg.dataDir}/paperless-manage
231 # Auto-migrate on first run or if the package has changed
232 versionFile="${cfg.dataDir}/src-version"
233 if [[ $(cat "$versionFile" 2>/dev/null) != ${pkg} ]]; then
234 ${pkg}/bin/paperless-ngx migrate
235 echo ${pkg} > "$versionFile"
238 + optionalString (cfg.passwordFile != null) ''
239 export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
240 export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password")
241 superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
242 superuserStateFile="${cfg.dataDir}/superuser-state"
244 if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
245 ${pkg}/bin/paperless-ngx manage_superuser
246 echo "$superuserState" > "$superuserStateFile"
249 } // optionalAttrs enableRedis {
250 after = [ "redis-paperless.service" ];
253 # Reading the user-provided password file requires root access
254 systemd.services.paperless-copy-password = mkIf (cfg.passwordFile != null) {
255 requiredBy = [ "paperless-scheduler.service" ];
256 before = [ "paperless-scheduler.service" ];
259 ${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
260 '${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
266 systemd.services.paperless-consumer = {
267 description = "Paperless document consumer";
268 serviceConfig = defaultServiceConfig // {
270 ExecStart = "${pkg}/bin/paperless-ngx document_consumer";
271 Restart = "on-failure";
274 # Bind to `paperless-scheduler` so that the consumer never runs
276 bindsTo = [ "paperless-scheduler.service" ];
277 after = [ "paperless-scheduler.service" ];
280 systemd.services.paperless-web = {
281 description = "Paperless web server";
282 serviceConfig = defaultServiceConfig // {
285 ${pkg.python.pkgs.gunicorn}/bin/gunicorn \
286 -c ${pkg}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
288 Restart = "on-failure";
290 # gunicorn needs setuid, liblapack needs mbind
291 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ];
292 # Needs to serve web page
293 PrivateNetwork = false;
294 } // lib.optionalAttrs (cfg.port < 1024) {
295 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
296 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
298 environment = env // {
299 PATH = mkForce pkg.path;
300 PYTHONPATH = "${pkg.python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/paperless-ngx/src";
302 # Allow the web interface to access the private /tmp directory of the server.
303 # This is required to support uploading files via the web interface.
304 unitConfig.JoinsNamespaceOf = "paperless-scheduler.service";
305 # Bind to `paperless-scheduler` so that the web server never runs
307 bindsTo = [ "paperless-scheduler.service" ];
308 after = [ "paperless-scheduler.service" ];
311 users = optionalAttrs (cfg.user == defaultUser) {
312 users.${defaultUser} = {
314 uid = config.ids.uids.paperless;
318 groups.${defaultUser} = {
319 gid = config.ids.gids.paperless;