1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7
8let
9 cfg = config.services.glitchtip;
10 pkg = cfg.package;
11 inherit (pkg.passthru) python;
12
13 environment = lib.mapAttrs (
14 _: value:
15 if value == true then
16 "True"
17 else if value == false then
18 "False"
19 else
20 toString value
21 ) cfg.settings;
22in
23
24{
25 meta.maintainers = with lib.maintainers; [
26 defelo
27 felbinger
28 ];
29
30 options = {
31 services.glitchtip = {
32 enable = lib.mkEnableOption "GlitchTip";
33
34 package = lib.mkPackageOption pkgs "glitchtip" { };
35
36 user = lib.mkOption {
37 type = lib.types.str;
38 description = "The user account under which GlitchTip runs.";
39 default = "glitchtip";
40 };
41
42 group = lib.mkOption {
43 type = lib.types.str;
44 description = "The group under which GlitchTip runs.";
45 default = "glitchtip";
46 };
47
48 listenAddress = lib.mkOption {
49 type = lib.types.str;
50 description = "The address to listen on.";
51 default = "127.0.0.1";
52 example = "0.0.0.0";
53 };
54
55 port = lib.mkOption {
56 type = lib.types.port;
57 description = "The port to listen on.";
58 default = 8000;
59 };
60
61 stateDir = lib.mkOption {
62 type = lib.types.path;
63 description = "State directory of glitchtip.";
64 default = "/var/lib/glitchtip";
65 };
66
67 settings = lib.mkOption {
68 description = ''
69 Configuration of GlitchTip. See <https://glitchtip.com/documentation/install#configuration> for more information.
70 '';
71 default = { };
72 defaultText = lib.literalExpression ''
73 {
74 DEBUG = 0;
75 DEBUG_TOOLBAR = 0;
76 DATABASE_URL = lib.mkIf config.services.glitchtip.database.createLocally "postgresql://@/glitchtip";
77 REDIS_URL = lib.mkIf config.services.glitchtip.redis.createLocally "unix://''${config.services.redis.servers.glitchtip.unixSocket}";
78 CELERY_BROKER_URL = lib.mkIf config.services.glitchtip.redis.createLocally "redis+socket://''${config.services.redis.servers.glitchtip.unixSocket}";
79 }
80 '';
81 example = {
82 GLITCHTIP_DOMAIN = "https://glitchtip.example.com";
83 DATABASE_URL = "postgres://postgres:postgres@postgres/postgres";
84 };
85
86 type = lib.types.submodule {
87 freeformType =
88 with lib.types;
89 attrsOf (oneOf [
90 str
91 int
92 bool
93 ]);
94
95 options = {
96 GLITCHTIP_DOMAIN = lib.mkOption {
97 type = lib.types.str;
98 description = "The URL under which GlitchTip is externally reachable.";
99 example = "https://glitchtip.example.com";
100 };
101
102 ENABLE_USER_REGISTRATION = lib.mkOption {
103 type = lib.types.bool;
104 description = ''
105 When true, any user will be able to register. When false, user self-signup is disabled after the first user is registered. Subsequent users must be created by a superuser on the backend and organization invitations may only be sent to existing users.
106 '';
107 default = false;
108 };
109
110 ENABLE_ORGANIZATION_CREATION = lib.mkOption {
111 type = lib.types.bool;
112 description = ''
113 When false, only superusers will be able to create new organizations after the first. When true, any user can create a new organization.
114 '';
115 default = false;
116 };
117 };
118 };
119 };
120
121 environmentFiles = lib.mkOption {
122 type = lib.types.listOf lib.types.path;
123 default = [ ];
124 example = [ "/run/secrets/glitchtip.env" ];
125 description = ''
126 Files to load environment variables from in addition to [](#opt-services.glitchtip.settings).
127 This is useful to avoid putting secrets into the nix store.
128 See <https://glitchtip.com/documentation/install#configuration> for more information.
129 '';
130 };
131
132 database.createLocally = lib.mkOption {
133 type = lib.types.bool;
134 default = true;
135 description = ''
136 Whether to enable and configure a local PostgreSQL database server.
137 '';
138 };
139
140 redis.createLocally = lib.mkOption {
141 type = lib.types.bool;
142 default = true;
143 description = ''
144 Whether to enable and configure a local Redis instance.
145 '';
146 };
147
148 gunicorn.extraArgs = lib.mkOption {
149 type = lib.types.listOf lib.types.str;
150 default = [ ];
151 description = "Extra arguments for gunicorn.";
152 };
153
154 celery.extraArgs = lib.mkOption {
155 type = lib.types.listOf lib.types.str;
156 default = [ ];
157 description = "Extra arguments for celery.";
158 };
159 };
160 };
161
162 config = lib.mkIf cfg.enable {
163 services.glitchtip.settings = {
164 DEBUG = lib.mkDefault 0;
165 DEBUG_TOOLBAR = lib.mkDefault 0;
166 PYTHONPATH = "${python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/glitchtip";
167 DATABASE_URL = lib.mkIf cfg.database.createLocally "postgresql://@/glitchtip";
168 REDIS_URL = lib.mkIf cfg.redis.createLocally "unix://${config.services.redis.servers.glitchtip.unixSocket}";
169 CELERY_BROKER_URL = lib.mkIf cfg.redis.createLocally "redis+socket://${config.services.redis.servers.glitchtip.unixSocket}";
170 GLITCHTIP_VERSION = pkg.version;
171 };
172
173 systemd.services =
174 let
175 commonService = {
176 wantedBy = [ "multi-user.target" ];
177
178 wants = [ "network-online.target" ];
179 requires =
180 lib.optional cfg.database.createLocally "postgresql.target"
181 ++ lib.optional cfg.redis.createLocally "redis-glitchtip.service";
182 after = [
183 "network-online.target"
184 ]
185 ++ lib.optional cfg.database.createLocally "postgresql.target"
186 ++ lib.optional cfg.redis.createLocally "redis-glitchtip.service";
187
188 inherit environment;
189 };
190
191 commonServiceConfig = {
192 User = cfg.user;
193 Group = cfg.group;
194 RuntimeDirectory = "glitchtip";
195 StateDirectory = "glitchtip";
196 EnvironmentFile = cfg.environmentFiles;
197 WorkingDirectory = "${pkg}/lib/glitchtip";
198 BindPaths = [ "${cfg.stateDir}/uploads:${pkg}/lib/glitchtip/uploads" ];
199
200 # hardening
201 AmbientCapabilities = "";
202 CapabilityBoundingSet = [ "" ];
203 DevicePolicy = "closed";
204 LockPersonality = true;
205 MemoryDenyWriteExecute = true;
206 NoNewPrivileges = true;
207 PrivateDevices = true;
208 PrivateTmp = true;
209 PrivateUsers = true;
210 ProcSubset = "pid";
211 ProtectClock = true;
212 ProtectControlGroups = true;
213 ProtectHome = true;
214 ProtectHostname = true;
215 ProtectKernelLogs = true;
216 ProtectKernelModules = true;
217 ProtectKernelTunables = true;
218 ProtectProc = "invisible";
219 ProtectSystem = "strict";
220 RemoveIPC = true;
221 RestrictAddressFamilies = [ "AF_INET AF_INET6 AF_UNIX" ];
222 RestrictNamespaces = true;
223 RestrictRealtime = true;
224 RestrictSUIDSGID = true;
225 SystemCallArchitectures = "native";
226 SystemCallFilter = [
227 "@system-service"
228 "~@privileged"
229 "~@resources"
230 "@chown"
231 ];
232 UMask = "0077";
233 };
234 in
235 {
236 glitchtip = commonService // {
237 description = "GlitchTip";
238
239 preStart = ''
240 ${lib.getExe pkg} migrate
241 '';
242
243 serviceConfig = commonServiceConfig // {
244 ExecStart = ''
245 ${lib.getExe python.pkgs.gunicorn} \
246 --bind=${cfg.listenAddress}:${toString cfg.port} \
247 ${lib.concatStringsSep " " cfg.gunicorn.extraArgs} \
248 glitchtip.wsgi
249 '';
250 };
251 };
252
253 glitchtip-worker = commonService // {
254 description = "GlitchTip Job Runner";
255
256 serviceConfig = commonServiceConfig // {
257 ExecStart = ''
258 ${lib.getExe python.pkgs.celery} \
259 -A glitchtip worker \
260 -B -s /run/glitchtip/celerybeat-schedule \
261 ${lib.concatStringsSep " " cfg.celery.extraArgs}
262 '';
263 };
264 };
265 };
266
267 services.postgresql = lib.mkIf cfg.database.createLocally {
268 enable = true;
269 ensureDatabases = [ "glitchtip" ];
270 ensureUsers = [
271 {
272 name = "glitchtip";
273 ensureDBOwnership = true;
274 }
275 ];
276 };
277
278 services.redis.servers.glitchtip.enable = cfg.redis.createLocally;
279
280 users.users = lib.mkIf (cfg.user == "glitchtip") {
281 glitchtip = {
282 group = cfg.group;
283 extraGroups = lib.optionals cfg.redis.createLocally [ "redis-glitchtip" ];
284 isSystemUser = true;
285 };
286 };
287
288 users.groups = lib.mkIf (cfg.group == "glitchtip") { glitchtip = { }; };
289
290 systemd.tmpfiles.settings.glitchtip."${cfg.stateDir}/uploads".d = { inherit (cfg) user group; };
291
292 environment.systemPackages =
293 let
294 glitchtip-manage = pkgs.writeShellScriptBin "glitchtip-manage" ''
295 set -o allexport
296 ${lib.toShellVars environment}
297 ${lib.concatMapStringsSep "\n" (f: "source ${f}") cfg.environmentFiles}
298 ${config.security.wrapperDir}/sudo -E -u ${cfg.user} ${lib.getExe pkg} "$@"
299 '';
300 in
301 [ glitchtip-manage ];
302 };
303}