falcon-nest/literal/virtualization/forgejo.org
2026-09-10 15:29:11 -05:00

7 KiB
Raw Blame History

Forgejo

Forgejo Configuration

There is an accompanying piece of documentation created by Claude called README-forgejo-setup.md. It is located in ~/Downloads

{
  config,
  lib,
  pkgs,
  ...
}:

let
  domain = <<forgejo-domain>>;
  acmeEmail = <<forgejo-email>>;
  secretsDir = <<forgejo-secrets>>;
  dockerJobImage = <<forgejo-job-image>>;
in
{
  <<forgejo-setup>>
}

Domain

"git.onionpatch.cloud"

Email

"shawn@shawnborton.info"

Secrets Dir

"/etc/forgejo/secrets"

Job Image

A general-purpose Actions job image. It ships Node.js (required by JS-based actions such as actions/checkout), git, bash and common CLI tools, without the ~18GB footprint of the fully-loaded "act-latest" image. Terraform, Python and pandoc are installed per-job via setup actions / apt-get – see the example workflows in .forgejo/workflows/.

"docker://catthehacker/ubuntu:runner-latest"

Forgejo User

services.forgejo already creates and runs the service as a user named "forgejo" by default. We declare it explicitly here so it's visible in the config, and give it an interactive shell so you can `sudo -u forgejo -i` for occasional admin/CLI work (creating the first admin account, running `forgejo admin …`, etc).

The Actions runner (section 4) intentionally does NOT run as this user – it's managed by its own module under a separate, dynamically allocated identity, so a compromised CI job can't directly touch the Forgejo server's state or the "docker" group.

users.users.forgejo = {
  isSystemUser = true;
  # group = "podman";
  description = "Forgejo git service";
  linger = true;
  # createHome = true;
  autoSubUidGidRange = true;
};

Storage

Only repository data and LFS objects live on the network share. Forgejo's own app state (config, DB-adjacent files, queues, indexer) stays on local disk at the default /var/lib/forgejo – changing `stateDir` is known to break the service on some nixpkgs releases (see NixOS/nixpkgs#385595), and app state doesn't need to survive on the network mount anyway.

CAVEAT: CIFS does not have the same locking/hardlink semantics as a local filesystem or NFS. Git operations are mostly fine over CIFS in practice, but if you see intermittent "unable to lock" or corrupted pack errors under concurrent load, consider switching this mount to NFS instead. Make sure forgejo.service won't start (or will block) until the share is actually mounted.

systemd.services.forgejo.serviceConfig.RequiresMountsFor = [ "/mnt/forgejo" ];

Setup

And now, forgejo itself. We'll use PostgreSQL for our backend.

The system already has an SSH daemon setup and chugging along nicely. No need to reinvent the wheel. We'll disable registration and use the CLI to create the first user. Finally, we'll allow future runners to use Github style runner setups.

Forgejo's own app state (`app.ini`, secrets it generates on first boot, queues, search index) stays on local disk at the default `/var/lib/forgejo`. Redirecting that whole directory to a network mount is known to break the service outright on some nixpkgs versions.

services.forgejo = {
  enable = true;
  user = "forgejo";
  group = "forgejo";
  database.type = "postgres";
  repositoryRoot = "/mnt/forgejo/repositories";

  settings = {
    server = {
      # DISABLE_SSH = true;
      DOMAIN = domain;
      ROOT_URL = "https://${domain}/";
      # HTTP_ADDR = "127.0.0.1";
      HTTP_ADDR = "0.0.0.0";
      # HTTP_PORT = 3000;
      # PROTOCOL = "https";
      SSH_PORT = 2112;
      # SSH_PORT = lib.head config.services.openssh.ports;
    };

    service = {
      DISABLE_REGISTRATION = false;
    };

    lfs = {
      enable = true;
      contentDir = "/mnt/forgejo/lfs";
    };

    actions = {
      ENABLED = true;
      DEFAULT_ACTIONS_URL = "github";
    };
  };
};

Reverse Proxy + TLS with Let's Encrypt and ACME

# security.acme = {
#   acceptTerms = true;
#   defaults.email = acmeEmail;
# };

services.nginx = {
  enable = true;
  recommendedProxySettings = true;
  recommendedTlsSettings = true;

  virtualHosts."${domain}" = {
    # forceSSL = true;
    # enableACME = true;
    extraConfig = ''
      client_max_body_size 512M;
    '';
    locations."/" = {
      proxyPass = "http://127.0.0.1:3000";
      proxyWebsockets = true;
    };
  };
};

Runners

Coming soon…

The Actions runner is kept separate on purpose: the docker-executor instance needs membership in the `podman` group, which is effectively root-equivalent (anyone in that group can mount the host filesystem via a container). Giving that same access to the account that owns your git data and admin session would mean a compromised CI job could reach the Forgejo server directly.

services.gitea-actions-runner = {
  package = pkgs.forgejo-runner;

  instances = {
    podman = {
      enable = true;
      name = "podman-runner";
      url = "https://${domain}";
      tokenFile = "${secretsDir}/forgejo-runner-token";
      labels = [ "docker:${dockerJobImage}" ];
      settings = {
        runner = {
          capacity = 2; # up to 2 concurrent containerized jobs
          timeout = "3h";
        };
      };
    };

    nix-host = {
      enable = true;
      name = "nix-host-runner";
      url = "https://${domain}";
      tokenFile = "${secretsDir}/forgejo-runner-token";
      labels = [ "nix-host:host" ];
      hostPackages = with pkgs; [
        nix
        git
        bash
        coreutils
        gnutar
        gzip
        nodejs_20 # needed by JS-based actions like actions/checkout
        cacert
      ];
      settings = {
        runner = {
          capacity = 1; # Nix builds are heavy; keep this to 1 at a time
          timeout = "3h";
        };
      };
    };
  };
};

Secret Placeholders

These files are created with dummy content ONLY if they don't already exist, so they survive `nixos-rebuild switch` without being clobbered once you've filled them in. Fill them in BEFORE your first switch where possible – see README-forgejo-setup.md.

system.activationScripts.forgejoSecretPlaceholders = ''
  mkdir -p ${secretsDir}
  chmod 700 ${secretsDir}

  if [ ! -f ${secretsDir}/forgejo-runner-token ]; then
    cat > ${secretsDir}/forgejo-runner-token <<'EOF'
    TOKEN=CHANGEME-generate-after-first-boot
    EOF

    chmod 600 ${secretsDir}/forgejo-runner-token
  fi
'';

Notes

Creating users

Since public registration is disabled by default, you'll need to run this command in the terminal

sudo forgejo-cli admin user create \
     --admin --username shawnb \
     --email shawn@shawnborton.info \
     --password 'qwZem4CxmT9j'