{ config, lib, pkgs, homeyConfig, ... }: # Gitea — self-hosted Git service. # # Auth model: LDAP authentication is configured through Gitea's admin UI # (or CLI) after first start. Reverse proxy auth headers from Caddy/Authelia # handle transparent login. # # Volume layout: # /gitea/data/ → /data (repos, sqlite db, avatars, lfs, etc.) # # Configuration strategy: all settings are passed as GITEA__
__ # environment variables. Gitea writes its own app.ini into /data/gitea/conf/ # on first start; the env vars override every key at runtime without touching # that file. This avoids the bind-mount / read-only-fs problem where Gitea # needs to rewrite its own config file on startup. # # Non-secret settings go in the `environment` block (they are fine in the # Nix store). Secret settings go into /run/gitea-secrets.env via ExecStartPre # (never in the store). # # Secrets consumed from sops: # gitea/admin_password # gitea/lfs_jwt_secret # gitea/oauth2_jwt_secret # gitea/internal_token let cfg = config.homey.gitea; dataDir = config.homey.storage.mountPoint; domain = homeyConfig.domain; in { options.homey.gitea = { enable = lib.mkEnableOption "Gitea Git server"; image = lib.mkOption { type = lib.types.str; default = "docker.io/gitea/gitea:latest"; }; port = lib.mkOption { type = lib.types.port; default = 3000; description = "Host port Gitea listens on (bound to 127.0.0.1)."; }; }; config = lib.mkIf cfg.enable { # ----------------------------------------------------------------------- # Secrets # ----------------------------------------------------------------------- sops.secrets."gitea/admin_password" = { owner = "root"; }; sops.secrets."gitea/lfs_jwt_secret" = { owner = "root"; }; sops.secrets."gitea/oauth2_jwt_secret" = { owner = "root"; }; sops.secrets."gitea/internal_token" = { owner = "root"; }; # ----------------------------------------------------------------------- # Container # ----------------------------------------------------------------------- virtualisation.oci-containers.containers.gitea = { image = cfg.image; ports = [ "127.0.0.1:${toString cfg.port}:3000" ]; # All non-secret settings via GITEA__
__ env vars. # These are safe to store in the Nix store. environment = { USER_UID = "1000"; USER_GID = "1000"; GITEA_CUSTOM = "/data/gitea"; # [DEFAULT] GITEA____APP_NAME = homeyConfig.organization; GITEA____RUN_MODE = "prod"; # [repository] GITEA__repository__ROOT = "/data/git/repositories"; # [server] GITEA__server__APP_DATA_PATH = "/data/gitea"; GITEA__server__DOMAIN = "git.${domain}"; GITEA__server__HTTP_PORT = toString cfg.port; GITEA__server__ROOT_URL = "https://git.${domain}/"; GITEA__server__DISABLE_SSH = "true"; GITEA__server__START_SSH_SERVER = "false"; GITEA__server__SSH_PORT = "2222"; GITEA__server__SSH_LISTEN_PORT = "2222"; GITEA__server__LFS_START_SERVER = "true"; GITEA__server__OFFLINE_MODE = "false"; # [lfs] GITEA__lfs__PATH = "/data/git/lfs"; # [database] GITEA__database__DB_TYPE = "sqlite3"; GITEA__database__PATH = "/data/gitea/gitea.db"; GITEA__database__LOG_SQL = "false"; # [indexer] GITEA__indexer__ISSUE_INDEXER_PATH = "/data/gitea/indexers/issues.bleve"; # [session] GITEA__session__PROVIDER = "file"; GITEA__session__PROVIDER_CONFIG = "/data/gitea/sessions"; # [picture] GITEA__picture__AVATAR_UPLOAD_PATH = "/data/gitea/avatars"; GITEA__picture__REPOSITORY_AVATAR_UPLOAD_PATH = "/data/gitea/repo-avatars"; GITEA__picture__DISABLE_GRAVATAR = "false"; # [attachment] GITEA__attachment__PATH = "/data/gitea/attachments"; # [log] GITEA__log__MODE = "console"; GITEA__log__LEVEL = "info"; GITEA__log__ROOT_PATH = "/data/gitea/log"; # [security] GITEA__security__INSTALL_LOCK = "true"; GITEA__security__REVERSE_PROXY_LIMIT = "1"; GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES = "*"; # [service] GITEA__service__DISABLE_REGISTRATION = "true"; GITEA__service__REQUIRE_SIGNIN_VIEW = "false"; GITEA__service__REGISTER_EMAIL_CONFIRM = "false"; GITEA__service__ENABLE_NOTIFY_MAIL = "false"; GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION = "true"; GITEA__service__ENABLE_CAPTCHA = "false"; GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION = "true"; GITEA__service__DEFAULT_ENABLE_TIMETRACKING = "true"; GITEA__service__NO_REPLY_ADDRESS = "noreply.localhost"; GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION = "true"; GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = "true"; # [mailer] GITEA__mailer__ENABLED = "false"; # [openid] GITEA__openid__ENABLE_OPENID_SIGNIN = "false"; GITEA__openid__ENABLE_OPENID_SIGNUP = "false"; # [oauth2] GITEA__oauth2__ENABLED = "false"; }; # Secret env vars written at runtime by ExecStartPre — never in store. environmentFiles = [ "/run/gitea-secrets.env" ]; volumes = [ "${dataDir}/gitea/data:/data" ]; extraOptions = [ "--network=homey" ]; }; # ----------------------------------------------------------------------- # ExecStartPre: write ephemeral secrets env file # ExecStopPost: clean it up # ----------------------------------------------------------------------- systemd.services."podman-gitea" = { serviceConfig = { ExecStartPre = [ (pkgs.writeShellScript "gitea-write-secrets" '' set -euo pipefail LFS=$(cat ${config.sops.secrets."gitea/lfs_jwt_secret".path}) OAUTH=$(cat ${config.sops.secrets."gitea/oauth2_jwt_secret".path}) TOKEN=$(cat ${config.sops.secrets."gitea/internal_token".path}) printf '%s\n' \ "GITEA__server__LFS_JWT_SECRET=$LFS" \ "GITEA__security__INTERNAL_TOKEN=$TOKEN" \ "GITEA__oauth2__JWT_SECRET=$OAUTH" \ > /run/gitea-secrets.env chmod 600 /run/gitea-secrets.env '') ]; ExecStopPost = [ (pkgs.writeShellScript "gitea-cleanup-secrets" '' rm -f /run/gitea-secrets.env '') ]; }; after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; # ----------------------------------------------------------------------- # Ensure the Gitea admin user exists with the correct password after start. # Runs as a oneshot after podman-gitea; idempotent (create or update). # ----------------------------------------------------------------------- systemd.services."gitea-admin-setup" = { description = "Ensure Gitea admin user exists with correct password"; wantedBy = [ "multi-user.target" ]; after = [ "podman-gitea.service" ]; requires = [ "podman-gitea.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' set -euo pipefail PASS=$(cat ${config.sops.secrets."gitea/admin_password".path}) # Wait until Gitea's HTTP endpoint is up (max 60 s) for i in $(seq 1 60); do if ${pkgs.curl}/bin/curl -sf http://127.0.0.1:${toString cfg.port}/ -o /dev/null; then break fi sleep 1 done # Sync password if admin exists; create if not. if ! ${pkgs.podman}/bin/podman exec -u 1000 gitea \ gitea admin user change-password --username admin --password "$PASS" 2>/dev/null; then ${pkgs.podman}/bin/podman exec -u 1000 gitea \ gitea admin user create \ --username admin \ --password "$PASS" \ --email "admin@${domain}" \ --admin fi ''; }; }; }