1{
2 config,
3 lib,
4 options,
5 pkgs,
6 buildEnv,
7 ...
8}:
9
10with lib;
11
12let
13 defaultUser = "healthchecks";
14 cfg = config.services.healthchecks;
15 opt = options.services.healthchecks;
16 pkg = cfg.package;
17 boolToPython = b: if b then "True" else "False";
18 environment = {
19 PYTHONPATH = pkg.pythonPath;
20 STATIC_ROOT = cfg.dataDir + "/static";
21 }
22 // lib.filterAttrs (_: v: !builtins.isNull v) cfg.settings;
23
24 environmentFile = pkgs.writeText "healthchecks-environment" (
25 lib.generators.toKeyValue { } environment
26 );
27
28 healthchecksManageScript = pkgs.writeShellScriptBin "healthchecks-manage" ''
29 sudo=exec
30 if [[ "$USER" != "${cfg.user}" ]]; then
31 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env --preserve-env=PYTHONPATH'
32 fi
33 export $(cat ${environmentFile} | xargs)
34 ${lib.optionalString (cfg.settingsFile != null) "export $(cat ${cfg.settingsFile} | xargs)"}
35 $sudo ${pkg}/opt/healthchecks/manage.py "$@"
36 '';
37in
38{
39 options.services.healthchecks = {
40 enable = mkEnableOption "healthchecks" // {
41 description = ''
42 Enable healthchecks.
43 It is expected to be run behind a HTTP reverse proxy.
44 '';
45 };
46
47 package = mkPackageOption pkgs "healthchecks" { };
48
49 user = mkOption {
50 default = defaultUser;
51 type = types.str;
52 description = ''
53 User account under which healthchecks runs.
54
55 ::: {.note}
56 If left as the default value this user will automatically be created
57 on system activation, otherwise you are responsible for
58 ensuring the user exists before the healthchecks service starts.
59 :::
60 '';
61 };
62
63 group = mkOption {
64 default = defaultUser;
65 type = types.str;
66 description = ''
67 Group account under which healthchecks runs.
68
69 ::: {.note}
70 If left as the default value this group will automatically be created
71 on system activation, otherwise you are responsible for
72 ensuring the group exists before the healthchecks service starts.
73 :::
74 '';
75 };
76
77 listenAddress = mkOption {
78 type = types.str;
79 default = "localhost";
80 description = "Address the server will listen on.";
81 };
82
83 port = mkOption {
84 type = types.port;
85 default = 8000;
86 description = "Port the server will listen on.";
87 };
88
89 dataDir = mkOption {
90 type = types.str;
91 default = "/var/lib/healthchecks";
92 description = ''
93 The directory used to store all data for healthchecks.
94
95 ::: {.note}
96 If left as the default value this directory will automatically be created before
97 the healthchecks server starts, otherwise you are responsible for ensuring the
98 directory exists with appropriate ownership and permissions.
99 :::
100 '';
101 };
102
103 settingsFile = lib.mkOption {
104 type = lib.types.nullOr lib.types.path;
105 default = null;
106 description = opt.settings.description;
107 };
108
109 settings = lib.mkOption {
110 description = ''
111 Environment variables which are read by healthchecks `(local)_settings.py`.
112
113 Settings which are explicitly covered in options below, are type-checked and/or transformed
114 before added to the environment, everything else is passed as a string.
115
116 See <https://healthchecks.io/docs/self_hosted_configuration/>
117 for a full documentation of settings.
118
119 We add additional variables to this list inside the packages `local_settings.py.`
120 - `STATIC_ROOT` to set a state directory for dynamically generated static files.
121 - `SECRET_KEY_FILE` to read `SECRET_KEY` from a file at runtime and keep it out of
122 /nix/store.
123 - `_FILE` variants for several values that hold sensitive information in
124 [Healthchecks configuration](https://healthchecks.io/docs/self_hosted_configuration/) so
125 that they also can be read from a file and kept out of /nix/store. To see which values
126 have support for a `_FILE` variant, run:
127 - `nix-instantiate --eval --expr '(import <nixpkgs> {}).healthchecks.secrets'`
128 - or `nix eval 'nixpkgs#healthchecks.secrets'` if the flake support has been enabled.
129
130 If the same variable is set in both `settings` and `settingsFile` the value from `settingsFile` has priority.
131 '';
132 type = types.submodule (settings: {
133 freeformType = types.attrsOf types.str;
134 options = {
135 ALLOWED_HOSTS = lib.mkOption {
136 type = types.listOf types.str;
137 default = [ "*" ];
138 description = "The host/domain names that this site can serve.";
139 apply = lib.concatStringsSep ",";
140 };
141
142 SECRET_KEY_FILE = mkOption {
143 type = types.nullOr types.path;
144 description = "Path to a file containing the secret key.";
145 default = null;
146 };
147
148 DEBUG = mkOption {
149 type = types.bool;
150 default = false;
151 description = "Enable debug mode.";
152 apply = boolToPython;
153 };
154
155 REGISTRATION_OPEN = mkOption {
156 type = types.bool;
157 default = false;
158 description = ''
159 A boolean that controls whether site visitors can create new accounts.
160 Set it to false if you are setting up a private Healthchecks instance,
161 but it needs to be publicly accessible (so, for example, your cloud
162 services can send pings to it).
163 If you close new user registration, you can still selectively invite
164 users to your team account.
165 '';
166 apply = boolToPython;
167 };
168
169 DB = mkOption {
170 type = types.enum [
171 "sqlite"
172 "postgres"
173 "mysql"
174 ];
175 default = "sqlite";
176 description = "Database engine to use.";
177 };
178
179 DB_NAME = mkOption {
180 type = types.str;
181 default = if settings.config.DB == "sqlite" then "${cfg.dataDir}/healthchecks.sqlite" else "hc";
182 defaultText = lib.literalExpression ''
183 if config.${settings.options.DB} == "sqlite"
184 then "''${config.${opt.dataDir}}/healthchecks.sqlite"
185 else "hc"
186 '';
187 description = "Database name.";
188 };
189 };
190 });
191 };
192 };
193
194 config = mkIf cfg.enable {
195 environment.systemPackages = [ healthchecksManageScript ];
196
197 systemd.targets.healthchecks = {
198 description = "Target for all Healthchecks services";
199 wantedBy = [ "multi-user.target" ];
200 wants = [ "network-online.target" ];
201 after = [
202 "network.target"
203 "network-online.target"
204 ];
205 };
206
207 systemd.services =
208 let
209 commonConfig = {
210 WorkingDirectory = cfg.dataDir;
211 User = cfg.user;
212 Group = cfg.group;
213 EnvironmentFile = [
214 environmentFile
215 ]
216 ++ lib.optional (cfg.settingsFile != null) cfg.settingsFile;
217 StateDirectory = mkIf (cfg.dataDir == "/var/lib/healthchecks") "healthchecks";
218 StateDirectoryMode = mkIf (cfg.dataDir == "/var/lib/healthchecks") "0750";
219 };
220 in
221 {
222 healthchecks-migration = {
223 description = "Healthchecks migrations";
224 wantedBy = [ "healthchecks.target" ];
225
226 serviceConfig = commonConfig // {
227 Restart = "on-failure";
228 Type = "oneshot";
229 ExecStart = ''
230 ${pkg}/opt/healthchecks/manage.py migrate
231 '';
232 };
233 };
234
235 healthchecks = {
236 description = "Healthchecks WSGI Service";
237 wantedBy = [ "healthchecks.target" ];
238 after = [ "healthchecks-migration.service" ];
239
240 preStart = ''
241 ${pkg}/opt/healthchecks/manage.py collectstatic --no-input
242 ${pkg}/opt/healthchecks/manage.py remove_stale_contenttypes --no-input
243 ''
244 + lib.optionalString (cfg.settings.DEBUG != "True") "${pkg}/opt/healthchecks/manage.py compress";
245
246 serviceConfig = commonConfig // {
247 Restart = "always";
248 ExecStart = ''
249 ${pkgs.python3Packages.gunicorn}/bin/gunicorn hc.wsgi \
250 --bind ${cfg.listenAddress}:${toString cfg.port} \
251 --pythonpath ${pkg}/opt/healthchecks
252 '';
253 };
254 };
255
256 healthchecks-sendalerts = {
257 description = "Healthchecks Alert Service";
258 wantedBy = [ "healthchecks.target" ];
259 after = [ "healthchecks.service" ];
260
261 serviceConfig = commonConfig // {
262 Restart = "always";
263 ExecStart = ''
264 ${pkg}/opt/healthchecks/manage.py sendalerts
265 '';
266 };
267 };
268
269 healthchecks-sendreports = {
270 description = "Healthchecks Reporting Service";
271 wantedBy = [ "healthchecks.target" ];
272 after = [ "healthchecks.service" ];
273
274 serviceConfig = commonConfig // {
275 Restart = "always";
276 ExecStart = ''
277 ${pkg}/opt/healthchecks/manage.py sendreports --loop
278 '';
279 };
280 };
281 };
282
283 users.users = optionalAttrs (cfg.user == defaultUser) {
284 ${defaultUser} = {
285 description = "healthchecks service owner";
286 isSystemUser = true;
287 group = defaultUser;
288 };
289 };
290
291 users.groups = optionalAttrs (cfg.user == defaultUser) {
292 ${defaultUser} = {
293 members = [ defaultUser ];
294 };
295 };
296 };
297}