7 KiB
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"
"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'