php84Extensions.imagick: fix darwin build (#360575)
[NixPkgs.git] / nixos / modules / services / web-apps / changedetection-io.nix
blob32448486cc95654fcfbfa87a605ed7af258ed6a0
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.changedetection-io;
7 in
9   options.services.changedetection-io = {
10     enable = mkEnableOption "changedetection-io";
12     user = mkOption {
13       default = "changedetection-io";
14       type = types.str;
15       description = ''
16         User account under which changedetection-io runs.
17       '';
18     };
20     group = mkOption {
21       default = "changedetection-io";
22       type = types.str;
23       description = ''
24         Group account under which changedetection-io runs.
25       '';
26     };
28     listenAddress = mkOption {
29       type = types.str;
30       default = "localhost";
31       description = "Address the server will listen on.";
32     };
34     port = mkOption {
35       type = types.port;
36       default = 5000;
37       description = "Port the server will listen on.";
38     };
40     datastorePath = mkOption {
41       type = types.str;
42       default = "/var/lib/changedetection-io";
43       description = ''
44         The directory used to store all data for changedetection-io.
45       '';
46     };
48     baseURL = mkOption {
49       type = types.nullOr types.str;
50       default = null;
51       example = "https://changedetection-io.example";
52       description = ''
53         The base url used in notifications and `{base_url}` token.
54       '';
55     };
57     behindProxy = mkOption {
58       type = types.bool;
59       default = false;
60       description = ''
61         Enable this option when changedetection-io runs behind a reverse proxy, so that it trusts X-* headers.
62         It is recommend to run changedetection-io behind a TLS reverse proxy.
63       '';
64     };
66     environmentFile = mkOption {
67       type = types.nullOr types.path;
68       default = null;
69       example = "/run/secrets/changedetection-io.env";
70       description = ''
71         Securely pass environment variabels to changedetection-io.
73         This can be used to set for example a frontend password reproducible via `SALTED_PASS`
74         which convinetly also deactivates nags about the hosted version.
75         `SALTED_PASS` should be 64 characters long while the first 32 are the salt and the second the frontend password.
76         It can easily be retrieved from the settings file when first set via the frontend with the following command:
77         ``jq -r .settings.application.password /var/lib/changedetection-io/url-watches.json``
78       '';
79     };
81     webDriverSupport = mkOption {
82       type = types.bool;
83       default = false;
84       description = ''
85         Enable support for fetching web pages using WebDriver and Chromium.
86         This starts a headless chromium controlled by puppeteer in an oci container.
88         ::: {.note}
89         Playwright can currently leak memory.
90         See https://github.com/dgtlmoon/changedetection.io/wiki/Playwright-content-fetcher#playwright-memory-leak
91         :::
92       '';
93     };
95     playwrightSupport = mkOption {
96       type = types.bool;
97       default = false;
98       description = ''
99         Enable support for fetching web pages using playwright and Chromium.
100         This starts a headless Chromium controlled by puppeteer in an oci container.
102         ::: {.note}
103         Playwright can currently leak memory.
104         See https://github.com/dgtlmoon/changedetection.io/wiki/Playwright-content-fetcher#playwright-memory-leak
105         :::
106       '';
107     };
109     chromePort = mkOption {
110       type = types.port;
111       default = 4444;
112       description = ''
113         A free port on which webDriverSupport or playwrightSupport listen on localhost.
114       '';
115     };
116   };
118   config = mkIf cfg.enable {
119     assertions = [
120       {
121         assertion = !((cfg.webDriverSupport == true) && (cfg.playwrightSupport == true));
122         message = "'services.changedetection-io.webDriverSupport' and 'services.changedetection-io.playwrightSupport' cannot be used together.";
123       }
124     ];
126     systemd = let
127       defaultStateDir = cfg.datastorePath == "/var/lib/changedetection-io";
128     in {
129       services.changedetection-io = {
130         wantedBy = [ "multi-user.target" ];
131         after = [ "network.target" ];
132         serviceConfig = {
133           User = cfg.user;
134           Group = cfg.group;
135           StateDirectory = mkIf defaultStateDir "changedetection-io";
136           StateDirectoryMode = mkIf defaultStateDir "0750";
137           WorkingDirectory = cfg.datastorePath;
138           Environment = [ "HIDE_REFERER=true" ]
139             ++ lib.optional (cfg.baseURL != null) "BASE_URL=${cfg.baseURL}"
140             ++ lib.optional cfg.behindProxy "USE_X_SETTINGS=1"
141             ++ lib.optional cfg.webDriverSupport "WEBDRIVER_URL=http://127.0.0.1:${toString cfg.chromePort}/wd/hub"
142             ++ lib.optional cfg.playwrightSupport "PLAYWRIGHT_DRIVER_URL=ws://127.0.0.1:${toString cfg.chromePort}/?stealth=1&--disable-web-security=true";
143           EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
144           ExecStart = ''
145             ${pkgs.changedetection-io}/bin/changedetection.py \
146               -h ${cfg.listenAddress} -p ${toString cfg.port} -d ${cfg.datastorePath}
147           '';
148           ProtectHome = true;
149           ProtectSystem = true;
150           Restart = "on-failure";
151         };
152       };
153       tmpfiles.rules = mkIf (!defaultStateDir) [
154         "d ${cfg.datastorePath} 0750 ${cfg.user} ${cfg.group} - -"
155       ];
156     };
158     users = {
159       users = optionalAttrs (cfg.user == "changedetection-io") {
160         "changedetection-io" = {
161           isSystemUser = true;
162           group = "changedetection-io";
163         };
164       };
166       groups = optionalAttrs (cfg.group == "changedetection-io") {
167         "changedetection-io" = { };
168       };
169     };
171     virtualisation = {
172       oci-containers.containers = lib.mkMerge [
173         (mkIf cfg.webDriverSupport {
174           changedetection-io-webdriver = {
175             image = "selenium/standalone-chrome";
176             environment = {
177               VNC_NO_PASSWORD = "1";
178               SCREEN_WIDTH = "1920";
179               SCREEN_HEIGHT = "1080";
180               SCREEN_DEPTH = "24";
181             };
182             ports = [
183               "127.0.0.1:${toString cfg.chromePort}:4444"
184             ];
185             volumes = [
186               "/dev/shm:/dev/shm"
187             ];
188             extraOptions = [ "--network=bridge" ];
189           };
190         })
192         (mkIf cfg.playwrightSupport {
193           changedetection-io-playwright = {
194             image = "browserless/chrome";
195             environment = {
196               SCREEN_WIDTH = "1920";
197               SCREEN_HEIGHT = "1024";
198               SCREEN_DEPTH = "16";
199               ENABLE_DEBUGGER = "false";
200               PREBOOT_CHROME = "true";
201               CONNECTION_TIMEOUT = "300000";
202               MAX_CONCURRENT_SESSIONS = "10";
203               CHROME_REFRESH_TIME = "600000";
204               DEFAULT_BLOCK_ADS = "true";
205               DEFAULT_STEALTH = "true";
206             };
207             ports = [
208               "127.0.0.1:${toString cfg.chromePort}:3000"
209             ];
210             extraOptions = [ "--network=bridge" ];
211           };
212         })
213       ];
214       podman.defaultNetwork.settings.dns_enabled = true;
215     };
216   };