grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / weblate.nix
blob24995009b11893d83d0415698d8e2e5f1ee2cc5c
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
8 let
9   cfg = config.services.weblate;
11   dataDir = "/var/lib/weblate";
12   settingsDir = "${dataDir}/settings";
14   finalPackage = cfg.package.overridePythonAttrs (old: {
15     # We only support the PostgreSQL backend in this module
16     dependencies = old.dependencies ++ cfg.package.optional-dependencies.postgres;
17     # Use a settings module in dataDir, to avoid having to rebuild the package
18     # when user changes settings.
19     makeWrapperArgs = (old.makeWrapperArgs or [ ]) ++ [
20       "--set PYTHONPATH  \"${settingsDir}\""
21       "--set DJANGO_SETTINGS_MODULE \"settings\""
22     ];
23   });
24   inherit (finalPackage) python;
26   pythonEnv = python.buildEnv.override {
27     extraLibs = with python.pkgs; [
28       (toPythonModule finalPackage)
29       celery
30     ];
31   };
33   # This extends and overrides the weblate/settings_example.py code found in upstream.
34   weblateConfig =
35     ''
36       # This was autogenerated by the NixOS module.
38       SITE_TITLE = "Weblate"
39       SITE_DOMAIN = "${cfg.localDomain}"
40       # TLS terminates at the reverse proxy, but this setting controls how links to weblate are generated.
41       ENABLE_HTTPS = True
42       SESSION_COOKIE_SECURE = ENABLE_HTTPS
43       DATA_DIR = "${dataDir}"
44       CACHE_DIR = f"{DATA_DIR}/cache"
45       STATIC_ROOT = "${finalPackage.static}"
46       MEDIA_ROOT = "/var/lib/weblate/media"
47       COMPRESS_ROOT = "${finalPackage.static}"
48       COMPRESS_OFFLINE = True
49       DEBUG = False
51       DATABASES = {
52         "default": {
53           "ENGINE": "django.db.backends.postgresql",
54           "HOST": "/run/postgresql",
55           "NAME": "weblate",
56           "USER": "weblate",
57         }
58       }
60       with open("${cfg.djangoSecretKeyFile}") as f:
61         SECRET_KEY = f.read().rstrip("\n")
63       CACHES = {
64         "default": {
65           "BACKEND": "django_redis.cache.RedisCache",
66           "LOCATION": "unix://${config.services.redis.servers.weblate.unixSocket}",
67           "OPTIONS": {
68               "CLIENT_CLASS": "django_redis.client.DefaultClient",
69               "PASSWORD": None,
70               "CONNECTION_POOL_KWARGS": {},
71           },
72           "KEY_PREFIX": "weblate",
73           "TIMEOUT": 3600,
74         },
75         "avatar": {
76           "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
77           "LOCATION": "/var/lib/weblate/avatar-cache",
78           "TIMEOUT": 86400,
79           "OPTIONS": {"MAX_ENTRIES": 1000},
80         }
81       }
84       CELERY_TASK_ALWAYS_EAGER = False
85       CELERY_BROKER_URL = "redis+socket://${config.services.redis.servers.weblate.unixSocket}"
86       CELERY_RESULT_BACKEND = CELERY_BROKER_URL
88       VCS_BACKENDS = ("weblate.vcs.git.GitRepository",)
90       SITE_URL = "https://{}".format(SITE_DOMAIN)
92       # WebAuthn
93       OTP_WEBAUTHN_RP_NAME = SITE_TITLE
94       OTP_WEBAUTHN_RP_ID = SITE_DOMAIN.split(":")[0]
95       OTP_WEBAUTHN_ALLOWED_ORIGINS = [SITE_URL]
97     ''
98     + lib.optionalString cfg.smtp.enable ''
99       ADMINS = (("Weblate Admin", "${cfg.smtp.user}"),)
101       EMAIL_HOST = "${cfg.smtp.host}"
102       EMAIL_USE_TLS = True
103       EMAIL_HOST_USER = "${cfg.smtp.user}"
104       SERVER_EMAIL = "${cfg.smtp.user}"
105       DEFAULT_FROM_EMAIL = "${cfg.smtp.user}"
106       EMAIL_PORT = 587
107       with open("${cfg.smtp.passwordFile}") as f:
108         EMAIL_HOST_PASSWORD = f.read().rstrip("\n")
110     ''
111     + cfg.extraConfig;
112   settings_py =
113     pkgs.runCommand "weblate_settings.py"
114       {
115         inherit weblateConfig;
116         passAsFile = [ "weblateConfig" ];
117       }
118       ''
119         mkdir -p $out
120         cat \
121           ${finalPackage}/${python.sitePackages}/weblate/settings_example.py \
122           $weblateConfigPath \
123           > $out/settings.py
124       '';
126   environment = {
127     PYTHONPATH = "${settingsDir}:${pythonEnv}/${python.sitePackages}/";
128     DJANGO_SETTINGS_MODULE = "settings";
129     # We run Weblate through gunicorn, so we can't utilise the env var set in the wrapper.
130     inherit (finalPackage) GI_TYPELIB_PATH;
131   };
133   weblatePath = with pkgs; [
134     gitSVN
135     borgbackup
137     #optional
138     git-review
139     tesseract
140     licensee
141     mercurial
142   ];
146   options = {
147     services.weblate = {
148       enable = lib.mkEnableOption "Weblate service";
150       package = lib.mkPackageOption pkgs "weblate" { };
152       localDomain = lib.mkOption {
153         description = "The domain name serving your Weblate instance.";
154         example = "weblate.example.org";
155         type = lib.types.str;
156       };
158       djangoSecretKeyFile = lib.mkOption {
159         description = ''
160           Location of the Django secret key.
162           This should be a path pointing to a file with secure permissions (not /nix/store).
164           Can be generated with `weblate-generate-secret-key` which is available as the `weblate` user.
165         '';
166         type = lib.types.path;
167       };
169       extraConfig = lib.mkOption {
170         type = lib.types.lines;
171         default = "";
172         description = ''
173           Text to append to `settings.py` Weblate configuration file.
174         '';
175       };
177       smtp = {
178         enable = lib.mkEnableOption "Weblate SMTP support";
179         user = lib.mkOption {
180           description = "SMTP login name.";
181           example = "weblate@example.org";
182           type = lib.types.str;
183         };
185         host = lib.mkOption {
186           description = "SMTP host used when sending emails to users.";
187           type = lib.types.str;
188           example = "127.0.0.1";
189         };
191         passwordFile = lib.mkOption {
192           description = ''
193             Location of a file containing the SMTP password.
195             This should be a path pointing to a file with secure permissions (not /nix/store).
196           '';
197           type = lib.types.path;
198         };
199       };
201     };
202   };
204   config = lib.mkIf cfg.enable {
206     systemd.tmpfiles.rules = [ "L+ ${settingsDir} - - - - ${settings_py}" ];
208     services.nginx = {
209       enable = true;
210       virtualHosts."${cfg.localDomain}" = {
212         forceSSL = true;
213         enableACME = true;
215         locations = {
216           "= /favicon.ico".alias = "${finalPackage}/${python.sitePackages}/weblate/static/favicon.ico";
217           "/static/".alias = "${finalPackage.static}/";
218           "/media/".alias = "/var/lib/weblate/media/";
219           "/".proxyPass = "http://unix:///run/weblate.socket";
220         };
222       };
223     };
225     systemd.services.weblate-postgresql-setup = {
226       description = "Weblate PostgreSQL setup";
227       after = [ "postgresql.service" ];
228       serviceConfig = {
229         Type = "oneshot";
230         User = "postgres";
231         Group = "postgres";
232         ExecStart = ''
233           ${config.services.postgresql.package}/bin/psql weblate -c "CREATE EXTENSION IF NOT EXISTS pg_trgm"
234         '';
235       };
236     };
238     systemd.services.weblate-migrate = {
239       description = "Weblate migration";
240       after = [ "weblate-postgresql-setup.service" ];
241       requires = [ "weblate-postgresql-setup.service" ];
242       # We want this to be active on boot, not just on socket activation
243       wantedBy = [ "multi-user.target" ];
244       inherit environment;
245       path = weblatePath;
246       serviceConfig = {
247         Type = "oneshot";
248         StateDirectory = "weblate";
249         User = "weblate";
250         Group = "weblate";
251         ExecStart = "${finalPackage}/bin/weblate migrate --noinput";
252       };
253     };
255     systemd.services.weblate-celery = {
256       description = "Weblate Celery";
257       after = [
258         "network.target"
259         "redis.service"
260         "postgresql.service"
261       ];
262       # We want this to be active on boot, not just on socket activation
263       wantedBy = [ "multi-user.target" ];
264       environment = environment // {
265         CELERY_WORKER_RUNNING = "1";
266       };
267       path = weblatePath;
268       # Recommendations from:
269       # https://github.com/WeblateOrg/weblate/blob/main/weblate/examples/celery-weblate.service
270       serviceConfig =
271         let
272           # We have to push %n through systemd's replacement, therefore %%n.
273           pidFile = "/run/celery/weblate-%%n.pid";
274           nodes = "celery notify memory backup translate";
275           cmd = verb: ''
276             ${pythonEnv}/bin/celery multi ${verb} \
277               ${nodes} \
278               -A "weblate.utils" \
279               --pidfile=${pidFile} \
280               --logfile=/var/log/celery/weblate-%%n%%I.log \
281               --loglevel=DEBUG \
282               --beat:celery \
283               --queues:celery=celery \
284               --prefetch-multiplier:celery=4 \
285               --queues:notify=notify \
286               --prefetch-multiplier:notify=10 \
287               --queues:memory=memory \
288               --prefetch-multiplier:memory=10 \
289               --queues:translate=translate \
290               --prefetch-multiplier:translate=4 \
291               --concurrency:backup=1 \
292               --queues:backup=backup \
293               --prefetch-multiplier:backup=2
294           '';
295         in
296         {
297           Type = "forking";
298           User = "weblate";
299           Group = "weblate";
300           WorkingDirectory = "${finalPackage}/${python.sitePackages}/weblate/";
301           RuntimeDirectory = "celery";
302           RuntimeDirectoryPreserve = "restart";
303           LogsDirectory = "celery";
304           ExecStart = cmd "start";
305           ExecReload = cmd "restart";
306           ExecStop = ''
307             ${pythonEnv}/bin/celery multi stopwait \
308               ${nodes} \
309               --pidfile=${pidFile}
310           '';
311           Restart = "always";
312         };
313     };
315     systemd.services.weblate = {
316       description = "Weblate Gunicorn app";
317       after = [
318         "network.target"
319         "weblate-migrate.service"
320         "weblate-celery.service"
321       ];
322       requires = [
323         "weblate-migrate.service"
324         "weblate-celery.service"
325         "weblate.socket"
326       ];
327       inherit environment;
328       path = weblatePath;
329       serviceConfig = {
330         Type = "notify";
331         NotifyAccess = "all";
332         ExecStart =
333           let
334             gunicorn = python.pkgs.gunicorn.overridePythonAttrs (old: {
335               # Allows Gunicorn to set a meaningful process name
336               dependencies = (old.dependencies or [ ]) ++ old.optional-dependencies.setproctitle;
337             });
338           in
339           ''
340             ${gunicorn}/bin/gunicorn \
341               --name=weblate \
342               --bind='unix:///run/weblate.socket' \
343               weblate.wsgi
344           '';
345         ExecReload = "kill -s HUP $MAINPID";
346         KillMode = "mixed";
347         PrivateTmp = true;
348         WorkingDirectory = dataDir;
349         StateDirectory = "weblate";
350         RuntimeDirectory = "weblate";
351         User = "weblate";
352         Group = "weblate";
353       };
354     };
356     systemd.sockets.weblate = {
357       before = [ "nginx.service" ];
358       wantedBy = [ "sockets.target" ];
359       socketConfig = {
360         ListenStream = "/run/weblate.socket";
361         SocketUser = "weblate";
362         SocketGroup = "weblate";
363         SocketMode = "770";
364       };
365     };
367     services.redis.servers.weblate = {
368       enable = true;
369       user = "weblate";
370       unixSocket = "/run/redis-weblate/redis.sock";
371       unixSocketPerm = 770;
372     };
374     services.postgresql = {
375       enable = true;
376       ensureUsers = [
377         {
378           name = "weblate";
379           ensureDBOwnership = true;
380         }
381       ];
382       ensureDatabases = [ "weblate" ];
383     };
385     users.users.weblate = {
386       isSystemUser = true;
387       group = "weblate";
388       packages = [ finalPackage ] ++ weblatePath;
389     };
391     users.groups.weblate.members = [ config.services.nginx.user ];
392   };
394   meta.maintainers = with lib.maintainers; [ erictapen ];