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\""
24 inherit (finalPackage) python;
26 pythonEnv = python.buildEnv.override {
27 extraLibs = with python.pkgs; [
28 (toPythonModule finalPackage)
33 # This extends and overrides the weblate/settings_example.py code found in upstream.
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.
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
53 "ENGINE": "django.db.backends.postgresql",
54 "HOST": "/run/postgresql",
60 with open("${cfg.djangoSecretKeyFile}") as f:
61 SECRET_KEY = f.read().rstrip("\n")
65 "BACKEND": "django_redis.cache.RedisCache",
66 "LOCATION": "unix://${config.services.redis.servers.weblate.unixSocket}",
68 "CLIENT_CLASS": "django_redis.client.DefaultClient",
70 "CONNECTION_POOL_KWARGS": {},
72 "KEY_PREFIX": "weblate",
76 "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
77 "LOCATION": "/var/lib/weblate/avatar-cache",
79 "OPTIONS": {"MAX_ENTRIES": 1000},
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)
93 OTP_WEBAUTHN_RP_NAME = SITE_TITLE
94 OTP_WEBAUTHN_RP_ID = SITE_DOMAIN.split(":")[0]
95 OTP_WEBAUTHN_ALLOWED_ORIGINS = [SITE_URL]
98 + lib.optionalString cfg.smtp.enable ''
99 ADMINS = (("Weblate Admin", "${cfg.smtp.user}"),)
101 EMAIL_HOST = "${cfg.smtp.host}"
103 EMAIL_HOST_USER = "${cfg.smtp.user}"
104 SERVER_EMAIL = "${cfg.smtp.user}"
105 DEFAULT_FROM_EMAIL = "${cfg.smtp.user}"
107 with open("${cfg.smtp.passwordFile}") as f:
108 EMAIL_HOST_PASSWORD = f.read().rstrip("\n")
113 pkgs.runCommand "weblate_settings.py"
115 inherit weblateConfig;
116 passAsFile = [ "weblateConfig" ];
121 ${finalPackage}/${python.sitePackages}/weblate/settings_example.py \
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;
133 weblatePath = with pkgs; [
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;
158 djangoSecretKeyFile = lib.mkOption {
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.
166 type = lib.types.path;
169 extraConfig = lib.mkOption {
170 type = lib.types.lines;
173 Text to append to `settings.py` Weblate configuration file.
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;
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";
191 passwordFile = lib.mkOption {
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).
197 type = lib.types.path;
204 config = lib.mkIf cfg.enable {
206 systemd.tmpfiles.rules = [ "L+ ${settingsDir} - - - - ${settings_py}" ];
210 virtualHosts."${cfg.localDomain}" = {
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";
225 systemd.services.weblate-postgresql-setup = {
226 description = "Weblate PostgreSQL setup";
227 after = [ "postgresql.service" ];
233 ${config.services.postgresql.package}/bin/psql weblate -c "CREATE EXTENSION IF NOT EXISTS pg_trgm"
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" ];
248 StateDirectory = "weblate";
251 ExecStart = "${finalPackage}/bin/weblate migrate --noinput";
255 systemd.services.weblate-celery = {
256 description = "Weblate Celery";
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";
268 # Recommendations from:
269 # https://github.com/WeblateOrg/weblate/blob/main/weblate/examples/celery-weblate.service
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";
276 ${pythonEnv}/bin/celery multi ${verb} \
279 --pidfile=${pidFile} \
280 --logfile=/var/log/celery/weblate-%%n%%I.log \
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
300 WorkingDirectory = "${finalPackage}/${python.sitePackages}/weblate/";
301 RuntimeDirectory = "celery";
302 RuntimeDirectoryPreserve = "restart";
303 LogsDirectory = "celery";
304 ExecStart = cmd "start";
305 ExecReload = cmd "restart";
307 ${pythonEnv}/bin/celery multi stopwait \
315 systemd.services.weblate = {
316 description = "Weblate Gunicorn app";
319 "weblate-migrate.service"
320 "weblate-celery.service"
323 "weblate-migrate.service"
324 "weblate-celery.service"
331 NotifyAccess = "all";
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;
340 ${gunicorn}/bin/gunicorn \
342 --bind='unix:///run/weblate.socket' \
345 ExecReload = "kill -s HUP $MAINPID";
348 WorkingDirectory = dataDir;
349 StateDirectory = "weblate";
350 RuntimeDirectory = "weblate";
356 systemd.sockets.weblate = {
357 before = [ "nginx.service" ];
358 wantedBy = [ "sockets.target" ];
360 ListenStream = "/run/weblate.socket";
361 SocketUser = "weblate";
362 SocketGroup = "weblate";
367 services.redis.servers.weblate = {
370 unixSocket = "/run/redis-weblate/redis.sock";
371 unixSocketPerm = 770;
374 services.postgresql = {
379 ensureDBOwnership = true;
382 ensureDatabases = [ "weblate" ];
385 users.users.weblate = {
388 packages = [ finalPackage ] ++ weblatePath;
391 users.groups.weblate.members = [ config.services.nginx.user ];
394 meta.maintainers = with lib.maintainers; [ erictapen ];