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 settings = lib.mkOption {
62 description = ''
63 Configuration of GlitchTip. See <https://glitchtip.com/documentation/install#configuration> for more information.
64 '';
65 default = { };
66 defaultText = lib.literalExpression ''
67 {
68 DEBUG = 0;
69 DEBUG_TOOLBAR = 0;
70 DATABASE_URL = lib.mkIf config.services.glitchtip.database.createLocally "postgresql://@/glitchtip";
71 REDIS_URL = lib.mkIf config.services.glitchtip.redis.createLocally "unix://''${config.services.redis.servers.glitchtip.unixSocket}";
72 CELERY_BROKER_URL = lib.mkIf config.services.glitchtip.redis.createLocally "redis+socket://''${config.services.redis.servers.glitchtip.unixSocket}";
73 }
74 '';
75 example = {
76 GLITCHTIP_DOMAIN = "https://glitchtip.example.com";
77 DATABASE_URL = "postgres://postgres:postgres@postgres/postgres";
78 };
79
80 type = lib.types.submodule {
81 freeformType =
82 with lib.types;
83 attrsOf (oneOf [
84 str
85 int
86 bool
87 ]);
88
89 options = {
90 GLITCHTIP_DOMAIN = lib.mkOption {
91 type = lib.types.str;
92 description = "The URL under which GlitchTip is externally reachable.";
93 example = "https://glitchtip.example.com";
94 };
95
96 ENABLE_USER_REGISTRATION = lib.mkOption {
97 type = lib.types.bool;
98 description = ''
99 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.
100 '';
101 default = false;
102 };
103
104 ENABLE_ORGANIZATION_CREATION = lib.mkOption {
105 type = lib.types.bool;
106 description = ''
107 When false, only superusers will be able to create new organizations after the first. When true, any user can create a new organization.
108 '';
109 default = false;
110 };
111 };
112 };
113 };
114
115 environmentFiles = lib.mkOption {
116 type = lib.types.listOf lib.types.path;
117 default = [ ];
118 example = [ "/run/secrets/glitchtip.env" ];
119 description = ''
120 Files to load environment variables from in addition to [](#opt-services.glitchtip.settings).
121 This is useful to avoid putting secrets into the nix store.
122 See <https://glitchtip.com/documentation/install#configuration> for more information.
123 '';
124 };
125
126 database.createLocally = lib.mkOption {
127 type = lib.types.bool;
128 default = true;
129 description = ''
130 Whether to enable and configure a local PostgreSQL database server.
131 '';
132 };
133
134 redis.createLocally = lib.mkOption {
135 type = lib.types.bool;
136 default = true;
137 description = ''
138 Whether to enable and configure a local Redis instance.
139 '';
140 };
141
142 gunicorn.extraArgs = lib.mkOption {
143 type = lib.types.listOf lib.types.str;
144 default = [ ];
145 description = "Extra arguments for gunicorn.";
146 };
147
148 celery.extraArgs = lib.mkOption {
149 type = lib.types.listOf lib.types.str;
150 default = [ ];
151 description = "Extra arguments for celery.";
152 };
153 };
154 };
155
156 config = lib.mkIf cfg.enable {
157 services.glitchtip.settings = {
158 DEBUG = lib.mkDefault 0;
159 DEBUG_TOOLBAR = lib.mkDefault 0;
160 PYTHONPATH = "${python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/glitchtip";
161 DATABASE_URL = lib.mkIf cfg.database.createLocally "postgresql://@/glitchtip";
162 REDIS_URL = lib.mkIf cfg.redis.createLocally "unix://${config.services.redis.servers.glitchtip.unixSocket}";
163 CELERY_BROKER_URL = lib.mkIf cfg.redis.createLocally "redis+socket://${config.services.redis.servers.glitchtip.unixSocket}";
164 GLITCHTIP_VERSION = pkg.version;
165 };
166
167 systemd.services =
168 let
169 commonService = {
170 wantedBy = [ "multi-user.target" ];
171
172 wants = [ "network-online.target" ];
173 requires =
174 lib.optional cfg.database.createLocally "postgresql.service"
175 ++ lib.optional cfg.redis.createLocally "redis-glitchtip.service";
176 after =
177 [ "network-online.target" ]
178 ++ lib.optional cfg.database.createLocally "postgresql.service"
179 ++ lib.optional cfg.redis.createLocally "redis-glitchtip.service";
180
181 inherit environment;
182 };
183
184 commonServiceConfig = {
185 User = cfg.user;
186 Group = cfg.group;
187 RuntimeDirectory = "glitchtip";
188 StateDirectory = "glitchtip";
189 EnvironmentFile = cfg.environmentFiles;
190 WorkingDirectory = "${pkg}/lib/glitchtip";
191
192 # hardening
193 AmbientCapabilities = "";
194 CapabilityBoundingSet = [ "" ];
195 DevicePolicy = "closed";
196 LockPersonality = true;
197 MemoryDenyWriteExecute = true;
198 NoNewPrivileges = true;
199 PrivateDevices = true;
200 PrivateTmp = true;
201 PrivateUsers = true;
202 ProcSubset = "pid";
203 ProtectClock = true;
204 ProtectControlGroups = true;
205 ProtectHome = true;
206 ProtectHostname = true;
207 ProtectKernelLogs = true;
208 ProtectKernelModules = true;
209 ProtectKernelTunables = true;
210 ProtectProc = "invisible";
211 ProtectSystem = "strict";
212 RemoveIPC = true;
213 RestrictAddressFamilies = [ "AF_INET AF_INET6 AF_UNIX" ];
214 RestrictNamespaces = true;
215 RestrictRealtime = true;
216 RestrictSUIDSGID = true;
217 SystemCallArchitectures = "native";
218 SystemCallFilter = [
219 "@system-service"
220 "~@privileged"
221 "~@resources"
222 ];
223 UMask = "0077";
224 };
225 in
226 {
227 glitchtip = commonService // {
228 description = "GlitchTip";
229
230 preStart = ''
231 ${lib.getExe pkg} migrate
232 '';
233
234 serviceConfig = commonServiceConfig // {
235 ExecStart = ''
236 ${lib.getExe python.pkgs.gunicorn} \
237 --bind=${cfg.listenAddress}:${toString cfg.port} \
238 ${lib.concatStringsSep " " cfg.gunicorn.extraArgs} \
239 glitchtip.wsgi
240 '';
241 };
242 };
243
244 glitchtip-worker = commonService // {
245 description = "GlitchTip Job Runner";
246
247 serviceConfig = commonServiceConfig // {
248 ExecStart = ''
249 ${lib.getExe python.pkgs.celery} \
250 -A glitchtip worker \
251 -B -s /run/glitchtip/celerybeat-schedule \
252 ${lib.concatStringsSep " " cfg.celery.extraArgs}
253 '';
254 };
255 };
256 };
257
258 services.postgresql = lib.mkIf cfg.database.createLocally {
259 enable = true;
260 ensureDatabases = [ "glitchtip" ];
261 ensureUsers = [
262 {
263 name = "glitchtip";
264 ensureDBOwnership = true;
265 }
266 ];
267 };
268
269 services.redis.servers.glitchtip.enable = cfg.redis.createLocally;
270
271 users.users = lib.mkIf (cfg.user == "glitchtip") {
272 glitchtip = {
273 home = "/var/lib/glitchtip";
274 group = cfg.group;
275 extraGroups = lib.optionals cfg.redis.createLocally [ "redis-glitchtip" ];
276 isSystemUser = true;
277 };
278 };
279
280 users.groups = lib.mkIf (cfg.group == "glitchtip") { glitchtip = { }; };
281
282 environment.systemPackages =
283 let
284 glitchtip-manage = pkgs.writeShellScriptBin "glitchtip-manage" ''
285 set -o allexport
286 ${lib.toShellVars environment}
287 ${lib.concatMapStringsSep "\n" (f: "source ${f}") cfg.environmentFiles}
288 ${config.security.wrapperDir}/sudo -E -u ${cfg.user} ${lib.getExe pkg} "$@"
289 '';
290 in
291 [ glitchtip-manage ];
292 };
293}