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