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