# NixOS module for repertory-api # # This module fetches the application source directly from a self-hosted Forgejo instance at a pinned # tag/commit, builds a Python environment, runs Alembic migrations on start, # and manages the Flask service under systemd. # # ── Typical usage in configuration.nix ─────────────────────────────────────── # # imports = [ (fetchTarball { # url = "https://git.yourdomain.com/yourusername/repertory-api/archive/v1.0.0.tar.gz"; # sha256 = "0000000000000000000000000000000000000000000000000000"; # } + "/nixos/module.nix") ]; # # ── Secrets ────────────────────────────────────────────────────────────────── # # The module expects two sops-nix secrets to exist. Add to configuration.nix: # # sops.secrets."repertory-api-key" = {}; # sops.secrets."repertory-db-url" = {}; # # ── Minimal configuration.nix example ──────────────────────────────────────── # # services.repertory-api = { # enable = true; # domain = "git.yourdomain.com"; # owner = "yourusername"; # version = "v1.0.0"; # rev = "abc123..."; # git commit SHA for the tag # sha256 = "sha256-..."; # `nix-prefetch-url --unpack ` # }; # # ── Updating ───────────────────────────────────────────────────────────────── # # 1. Push a new tag to your Forgejo instance (e.g. v1.1.0). # 2. Get the new sha256: # nix-prefetch-url --unpack \ # https://git.yourdomain.com/yourusername/repertory-api/archive/v1.1.0.tar.gz # 3. Update `version`, `rev`, and `sha256` in your configuration.nix. # 4. nixos-rebuild switch # → Nix fetches the new source, rebuilds the Python env, restarts the # service, and runs migrations automatically in preStart. # # ───────────────────────────────────────────────────────────────────────────── { config, lib, pkgs, ... }: with lib; let cfg = config.services.repertory-api; # Fetch the application source from the self-hosted Forgejo instance at the # pinned rev. Nix verifies the sha256 hash, so this is fully reproducible. appSrc = pkgs.fetchFromGitea { domain = cfg.domain; owner = cfg.owner; repo = "repertory-api"; rev = cfg.rev; sha256 = cfg.sha256; }; # Build the Python environment with all runtime dependencies. # This mirrors pyproject.toml [project.dependencies]. # uv is a local dev tool — on NixOS, Nix itself plays the role of # dependency resolver and the Python env is built at evaluation time. pythonEnv = pkgs.python3.withPackages (ps: with ps; [ flask flask-sqlalchemy sqlalchemy psycopg2 python-dotenv alembic ]); in { options.services.repertory-api = { enable = mkEnableOption "Repertory API"; domain = mkOption { type = types.str; description = "Hostname of your self-hosted Forgejo instance."; example = "git.yourdomain.com"; }; owner = mkOption { type = types.str; description = "Forgejo username / org that owns the repertory-api repo."; example = "yourusername"; }; version = mkOption { type = types.str; description = "Human-readable version label (used only for documentation/logging)."; example = "v1.0.0"; }; rev = mkOption { type = types.str; description = "Git commit SHA for the tag you want to deploy."; example = "abc1234def5678"; }; sha256 = mkOption { type = types.str; description = '' sha256 hash of the fetched source tarball. Obtain with: nix-prefetch-url --unpack \ https://git.yourdomain.com//repertory-api/archive/.tar.gz ''; example = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }; port = mkOption { type = types.port; default = 5000; description = "Port the Flask app listens on."; }; user = mkOption { type = types.str; default = "repertory"; description = "Unix user to run the service as."; }; group = mkOption { type = types.str; default = "repertory"; description = "Unix group to run the service as."; }; apiKeySecret = mkOption { type = types.str; default = "/run/secrets/repertory-api-key"; description = "Path to the SOPS-decrypted file containing the raw API key."; }; dbUrlSecret = mkOption { type = types.str; default = "/run/secrets/repertory-db-url"; description = "Path to the SOPS-decrypted file containing the DATABASE_URL string."; }; }; config = mkIf cfg.enable { users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; home = "/var/lib/repertory-api"; createHome = true; }; users.groups.${cfg.group} = {}; systemd.services.repertory-api = { description = "Repertory API (Flask) — ${cfg.version}"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "postgresql.service" ]; serviceConfig = { User = cfg.user; Group = cfg.group; WorkingDirectory = appSrc; Restart = "on-failure"; RestartSec = "5s"; ExecStart = "${pythonEnv}/bin/python -m flask --app wsgi:app run --host=0.0.0.0 --port=${toString cfg.port}"; # Secrets are injected via the env file written in preStart. EnvironmentFiles = [ "/run/repertory-api/env" ]; # Security hardening ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; NoNewPrivileges = true; # /run/repertory-api is on tmpfs and holds only the mode-600 env file. ReadWritePaths = [ "/run/repertory-api" ]; }; preStart = '' # Write secrets into a tmpfs env file so nothing is on-disk in plaintext. mkdir -p /run/repertory-api chmod 700 /run/repertory-api printf 'API_KEY=%s\n' "$(cat ${cfg.apiKeySecret})" > /run/repertory-api/env printf 'DATABASE_URL=%s\n' "$(cat ${cfg.dbUrlSecret})" >> /run/repertory-api/env chmod 600 /run/repertory-api/env # Run any pending Alembic migrations before the server starts. # On a fresh database this creates all tables; on subsequent deploys # it applies only new migration files. export DATABASE_URL="$(cat ${cfg.dbUrlSecret})" cd ${appSrc} ${pythonEnv}/bin/python -m alembic upgrade head ''; }; }; }