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