201 lines
7 KiB
Nix
201 lines
7 KiB
Nix
|
|
# 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 <tarball url>`
|
||
|
|
# };
|
||
|
|
#
|
||
|
|
# ── 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/<owner>/repertory-api/archive/<rev>.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
|
||
|
|
'';
|
||
|
|
};
|
||
|
|
};
|
||
|
|
}
|