{ config, lib, pkgs, ... }: let cfg = config.services.weblate; dataDir = "/var/lib/weblate"; settingsDir = "${dataDir}/settings"; finalPackage = cfg.package.overridePythonAttrs (old: { # We only support the PostgreSQL backend in this module dependencies = old.dependencies ++ cfg.package.optional-dependencies.postgres; # Use a settings module in dataDir, to avoid having to rebuild the package # when user changes settings. makeWrapperArgs = (old.makeWrapperArgs or [ ]) ++ [ "--set PYTHONPATH \"${settingsDir}\"" "--set DJANGO_SETTINGS_MODULE \"settings\"" ]; }); inherit (finalPackage) python; pythonEnv = python.buildEnv.override { extraLibs = with python.pkgs; [ (toPythonModule finalPackage) celery ]; }; # This extends and overrides the weblate/settings_example.py code found in upstream. weblateConfig = '' # This was autogenerated by the NixOS module. SITE_TITLE = "Weblate" SITE_DOMAIN = "${cfg.localDomain}" # TLS terminates at the reverse proxy, but this setting controls how links to weblate are generated. ENABLE_HTTPS = True SESSION_COOKIE_SECURE = ENABLE_HTTPS DATA_DIR = "${dataDir}" CACHE_DIR = f"{DATA_DIR}/cache" STATIC_ROOT = "${finalPackage.static}" MEDIA_ROOT = "/var/lib/weblate/media" COMPRESS_ROOT = "${finalPackage.static}" COMPRESS_OFFLINE = True DEBUG = False DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "HOST": "/run/postgresql", "NAME": "weblate", "USER": "weblate", } } with open("${cfg.djangoSecretKeyFile}") as f: SECRET_KEY = f.read().rstrip("\n") CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "unix://${config.services.redis.servers.weblate.unixSocket}", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "PASSWORD": None, "CONNECTION_POOL_KWARGS": {}, }, "KEY_PREFIX": "weblate", "TIMEOUT": 3600, }, "avatar": { "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", "LOCATION": "/var/lib/weblate/avatar-cache", "TIMEOUT": 86400, "OPTIONS": {"MAX_ENTRIES": 1000}, } } CELERY_TASK_ALWAYS_EAGER = False CELERY_BROKER_URL = "redis+socket://${config.services.redis.servers.weblate.unixSocket}" CELERY_RESULT_BACKEND = CELERY_BROKER_URL VCS_BACKENDS = ("weblate.vcs.git.GitRepository",) SITE_URL = "https://{}".format(SITE_DOMAIN) # WebAuthn OTP_WEBAUTHN_RP_NAME = SITE_TITLE OTP_WEBAUTHN_RP_ID = SITE_DOMAIN.split(":")[0] OTP_WEBAUTHN_ALLOWED_ORIGINS = [SITE_URL] '' + lib.optionalString cfg.smtp.enable '' ADMINS = (("Weblate Admin", "${cfg.smtp.user}"),) EMAIL_HOST = "${cfg.smtp.host}" EMAIL_USE_TLS = True EMAIL_HOST_USER = "${cfg.smtp.user}" SERVER_EMAIL = "${cfg.smtp.user}" DEFAULT_FROM_EMAIL = "${cfg.smtp.user}" EMAIL_PORT = 587 with open("${cfg.smtp.passwordFile}") as f: EMAIL_HOST_PASSWORD = f.read().rstrip("\n") '' + cfg.extraConfig; settings_py = pkgs.runCommand "weblate_settings.py" { inherit weblateConfig; passAsFile = [ "weblateConfig" ]; } '' mkdir -p $out cat \ ${finalPackage}/${python.sitePackages}/weblate/settings_example.py \ $weblateConfigPath \ > $out/settings.py ''; environment = { PYTHONPATH = "${settingsDir}:${pythonEnv}/${python.sitePackages}/"; DJANGO_SETTINGS_MODULE = "settings"; # We run Weblate through gunicorn, so we can't utilise the env var set in the wrapper. inherit (finalPackage) GI_TYPELIB_PATH; }; weblatePath = with pkgs; [ gitSVN borgbackup #optional git-review tesseract licensee mercurial ]; in { options = { services.weblate = { enable = lib.mkEnableOption "Weblate service"; package = lib.mkPackageOption pkgs "weblate" { }; localDomain = lib.mkOption { description = "The domain name serving your Weblate instance."; example = "weblate.example.org"; type = lib.types.str; }; djangoSecretKeyFile = lib.mkOption { description = '' Location of the Django secret key. This should be a path pointing to a file with secure permissions (not /nix/store). Can be generated with `weblate-generate-secret-key` which is available as the `weblate` user. ''; type = lib.types.path; }; extraConfig = lib.mkOption { type = lib.types.lines; default = ""; description = '' Text to append to `settings.py` Weblate configuration file. ''; }; smtp = { enable = lib.mkEnableOption "Weblate SMTP support"; user = lib.mkOption { description = "SMTP login name."; example = "weblate@example.org"; type = lib.types.str; }; host = lib.mkOption { description = "SMTP host used when sending emails to users."; type = lib.types.str; example = "127.0.0.1"; }; passwordFile = lib.mkOption { description = '' Location of a file containing the SMTP password. This should be a path pointing to a file with secure permissions (not /nix/store). ''; type = lib.types.path; }; }; }; }; config = lib.mkIf cfg.enable { systemd.tmpfiles.rules = [ "L+ ${settingsDir} - - - - ${settings_py}" ]; services.nginx = { enable = true; virtualHosts."${cfg.localDomain}" = { forceSSL = true; enableACME = true; locations = { "= /favicon.ico".alias = "${finalPackage}/${python.sitePackages}/weblate/static/favicon.ico"; "/static/".alias = "${finalPackage.static}/"; "/media/".alias = "/var/lib/weblate/media/"; "/".proxyPass = "http://unix:///run/weblate.socket"; }; }; }; systemd.services.weblate-postgresql-setup = { description = "Weblate PostgreSQL setup"; after = [ "postgresql.service" ]; serviceConfig = { Type = "oneshot"; User = "postgres"; Group = "postgres"; ExecStart = '' ${config.services.postgresql.package}/bin/psql weblate -c "CREATE EXTENSION IF NOT EXISTS pg_trgm" ''; }; }; systemd.services.weblate-migrate = { description = "Weblate migration"; after = [ "weblate-postgresql-setup.service" ]; requires = [ "weblate-postgresql-setup.service" ]; # We want this to be active on boot, not just on socket activation wantedBy = [ "multi-user.target" ]; inherit environment; path = weblatePath; serviceConfig = { Type = "oneshot"; StateDirectory = "weblate"; User = "weblate"; Group = "weblate"; ExecStart = "${finalPackage}/bin/weblate migrate --noinput"; }; }; systemd.services.weblate-celery = { description = "Weblate Celery"; after = [ "network.target" "redis.service" "postgresql.service" ]; # We want this to be active on boot, not just on socket activation wantedBy = [ "multi-user.target" ]; environment = environment // { CELERY_WORKER_RUNNING = "1"; }; path = weblatePath; # Recommendations from: # https://github.com/WeblateOrg/weblate/blob/main/weblate/examples/celery-weblate.service serviceConfig = let # We have to push %n through systemd's replacement, therefore %%n. pidFile = "/run/celery/weblate-%%n.pid"; nodes = "celery notify memory backup translate"; cmd = verb: '' ${pythonEnv}/bin/celery multi ${verb} \ ${nodes} \ -A "weblate.utils" \ --pidfile=${pidFile} \ --logfile=/var/log/celery/weblate-%%n%%I.log \ --loglevel=DEBUG \ --beat:celery \ --queues:celery=celery \ --prefetch-multiplier:celery=4 \ --queues:notify=notify \ --prefetch-multiplier:notify=10 \ --queues:memory=memory \ --prefetch-multiplier:memory=10 \ --queues:translate=translate \ --prefetch-multiplier:translate=4 \ --concurrency:backup=1 \ --queues:backup=backup \ --prefetch-multiplier:backup=2 ''; in { Type = "forking"; User = "weblate"; Group = "weblate"; WorkingDirectory = "${finalPackage}/${python.sitePackages}/weblate/"; RuntimeDirectory = "celery"; RuntimeDirectoryPreserve = "restart"; LogsDirectory = "celery"; ExecStart = cmd "start"; ExecReload = cmd "restart"; ExecStop = '' ${pythonEnv}/bin/celery multi stopwait \ ${nodes} \ --pidfile=${pidFile} ''; Restart = "always"; }; }; systemd.services.weblate = { description = "Weblate Gunicorn app"; after = [ "network.target" "weblate-migrate.service" "weblate-celery.service" ]; requires = [ "weblate-migrate.service" "weblate-celery.service" "weblate.socket" ]; inherit environment; path = weblatePath; serviceConfig = { Type = "notify"; NotifyAccess = "all"; ExecStart = let gunicorn = python.pkgs.gunicorn.overridePythonAttrs (old: { # Allows Gunicorn to set a meaningful process name dependencies = (old.dependencies or [ ]) ++ old.optional-dependencies.setproctitle; }); in '' ${gunicorn}/bin/gunicorn \ --name=weblate \ --bind='unix:///run/weblate.socket' \ weblate.wsgi ''; ExecReload = "kill -s HUP $MAINPID"; KillMode = "mixed"; PrivateTmp = true; WorkingDirectory = dataDir; StateDirectory = "weblate"; RuntimeDirectory = "weblate"; User = "weblate"; Group = "weblate"; }; }; systemd.sockets.weblate = { before = [ "nginx.service" ]; wantedBy = [ "sockets.target" ]; socketConfig = { ListenStream = "/run/weblate.socket"; SocketUser = "weblate"; SocketGroup = "weblate"; SocketMode = "770"; }; }; services.redis.servers.weblate = { enable = true; user = "weblate"; unixSocket = "/run/redis-weblate/redis.sock"; unixSocketPerm = 770; }; services.postgresql = { enable = true; ensureUsers = [ { name = "weblate"; ensureDBOwnership = true; } ]; ensureDatabases = [ "weblate" ]; }; users.users.weblate = { isSystemUser = true; group = "weblate"; packages = [ finalPackage ] ++ weblatePath; }; users.groups.weblate.members = [ config.services.nginx.user ]; }; meta.maintainers = with lib.maintainers; [ erictapen ]; }