From a3f216fc2cce2299cf66e174fb927cc05dde97dd Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 26 Jul 2026 09:57:28 +0300 Subject: forgejo: add a standalone Forgejo install at code.f3s.buetow.org Deliberately independent of the cgit git-server: separate namespace (services), separate NFS volumes, separate SSH NodePort (30222 vs 30022), and no shared storage. cgit keeps serving the existing 80 bare repos at c-git.f3s.buetow.org and is not touched. Forgejo starts empty; repos get migrated by hand later. ArgoCD deliberately keeps reading conf.git from the existing git-server, so Forgejo has no consumers and cannot take cluster deploys down with it. SQLite rather than a PostgreSQL pod: single writer (replicas 1 + Recreate) and NFSv4.2 does real byte-range locking, so the usual SQLite-on-NFS failure mode does not apply. Uses the -rootless image so the pod runs wholly as UID 1000 with all capabilities dropped, and both volumes carry the .nfs-sentinel guard. The installer is locked and registration disabled because the instance is reachable from the internet; the admin account is created via the CLI. code.f3s.buetow.org added to @f3s_hosts, which drives the DNS zone, the relayd route, the ACME cert and the gogios checks. Not yet activated: the ArgoCD Application still needs applying, the NFS directories creating, and the frontends deploying. See f3s/forgejo/README.md. Co-Authored-By: Claude Opus 5 --- f3s/forgejo/helm-chart/Chart.yaml | 5 + f3s/forgejo/helm-chart/templates/deployment.yaml | 152 +++++++++++++++++++++ f3s/forgejo/helm-chart/templates/ingress.yaml | 54 ++++++++ .../helm-chart/templates/persistent-volume.yaml | 66 +++++++++ f3s/forgejo/helm-chart/templates/service.yaml | 36 +++++ 5 files changed, 313 insertions(+) create mode 100644 f3s/forgejo/helm-chart/Chart.yaml create mode 100644 f3s/forgejo/helm-chart/templates/deployment.yaml create mode 100644 f3s/forgejo/helm-chart/templates/ingress.yaml create mode 100644 f3s/forgejo/helm-chart/templates/persistent-volume.yaml create mode 100644 f3s/forgejo/helm-chart/templates/service.yaml (limited to 'f3s/forgejo/helm-chart') diff --git a/f3s/forgejo/helm-chart/Chart.yaml b/f3s/forgejo/helm-chart/Chart.yaml new file mode 100644 index 0000000..d0bf564 --- /dev/null +++ b/f3s/forgejo/helm-chart/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: forgejo +description: A Helm chart for deploying Forgejo, the self-hosted git forge at code.f3s.buetow.org. +version: 0.1.0 +appVersion: "16.0.1" diff --git a/f3s/forgejo/helm-chart/templates/deployment.yaml b/f3s/forgejo/helm-chart/templates/deployment.yaml new file mode 100644 index 0000000..132fed4 --- /dev/null +++ b/f3s/forgejo/helm-chart/templates/deployment.yaml @@ -0,0 +1,152 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: forgejo + namespace: services + labels: + app: forgejo +spec: + replicas: 1 + # Recreate so the old pod fully terminates before the new one starts — + # avoids NFS-lock races on the hostPath-backed PVC during rolling updates. + # This also matters for SQLite: exactly one process may hold the database. + strategy: + type: Recreate + selector: + matchLabels: + app: forgejo + template: + metadata: + labels: + app: forgejo + spec: + securityContext: + # The -rootless image runs entirely as the unprivileged git user (1000). + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: nfs-check-data + image: busybox:stable + command: + - sh + - -c + - | + test -f /mnt/.nfs-sentinel || ( + echo "ERROR: NFS sentinel missing at /mnt/.nfs-sentinel" + echo "refusing to start; node likely has NFS unmounted" + echo "pod would otherwise bind-mount the local-XFS shadow" + exit 1 + ) + volumeMounts: + - name: forgejo-data + mountPath: /mnt + readOnly: true + - name: nfs-check-config + image: busybox:stable + command: + - sh + - -c + - | + test -f /mnt/.nfs-sentinel || ( + echo "ERROR: NFS sentinel missing at /mnt/.nfs-sentinel" + echo "refusing to start; node likely has NFS unmounted" + echo "pod would otherwise bind-mount the local-XFS shadow" + exit 1 + ) + volumeMounts: + - name: forgejo-config + mountPath: /mnt + readOnly: true + + containers: + - name: forgejo + image: codeberg.org/forgejo/forgejo:16.0.1-rootless + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3000 + name: http + protocol: TCP + - containerPort: 2222 + name: ssh + protocol: TCP + env: + # SQLite rather than a separate PostgreSQL pod: this is a single-writer + # instance (replicas 1 + Recreate), and NFSv4.2 does real byte-range + # locking, so the usual SQLite-on-NFS corruption mode does not apply. + # Revisit if this ever needs more than one replica. + - name: FORGEJO__database__DB_TYPE + value: "sqlite3" + - name: FORGEJO__database__PATH + value: "/var/lib/gitea/data/forgejo.db" + + # Public identity. ROOT_URL must match what relayd terminates TLS for, + # otherwise Forgejo generates clone URLs and redirects on the wrong host. + - name: FORGEJO__server__DOMAIN + value: "code.f3s.buetow.org" + - name: FORGEJO__server__ROOT_URL + value: "https://code.f3s.buetow.org/" + - name: FORGEJO__server__HTTP_PORT + value: "3000" + + # Built-in SSH server. SSH_LISTEN_PORT is what the container binds; + # SSH_PORT is what Forgejo advertises in clone URLs, i.e. the NodePort + # users actually reach. git-server already owns 30022, so this is 30222. + - name: FORGEJO__server__START_SSH_SERVER + value: "true" + - name: FORGEJO__server__SSH_LISTEN_PORT + value: "2222" + - name: FORGEJO__server__SSH_DOMAIN + value: "code.f3s.buetow.org" + - name: FORGEJO__server__SSH_PORT + value: "30222" + + # This instance is reachable from the public internet through relayd. + # Lock the installer (otherwise the first visitor gets the setup wizard) + # and keep signups closed; create the admin with the CLI, see README. + - name: FORGEJO__security__INSTALL_LOCK + value: "true" + - name: FORGEJO__service__DISABLE_REGISTRATION + value: "true" + + # Catches stale NFS file handles (ESTALE) after an NFS server restart, + # which a plain HTTP probe would not notice until a request touched disk. + livenessProbe: + exec: + command: ["test", "-f", "/var/lib/gitea/.nfs-sentinel"] + initialDelaySeconds: 60 + periodSeconds: 30 + failureThreshold: 3 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /api/healthz + port: 3000 + initialDelaySeconds: 20 + periodSeconds: 15 + failureThreshold: 3 + timeoutSeconds: 5 + volumeMounts: + - name: forgejo-data + mountPath: /var/lib/gitea + - name: forgejo-config + mountPath: /etc/gitea + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + + volumes: + - name: forgejo-data + persistentVolumeClaim: + claimName: forgejo-data-pvc + - name: forgejo-config + persistentVolumeClaim: + claimName: forgejo-config-pvc diff --git a/f3s/forgejo/helm-chart/templates/ingress.yaml b/f3s/forgejo/helm-chart/templates/ingress.yaml new file mode 100644 index 0000000..4916098 --- /dev/null +++ b/f3s/forgejo/helm-chart/templates/ingress.yaml @@ -0,0 +1,54 @@ +# Forgejo web UI ingress. +# +# code.f3s.buetow.org must also be listed in @f3s_hosts in frontends/Rexfile -- +# that array drives the DNS zone, the relayd routing rule and the ACME cert. +# Adding it here alone is not enough to make the name resolve or serve TLS. +# +# cgit stays where it is, at c-git.f3s.buetow.org; the two are unrelated. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: forgejo-ingress + namespace: services + annotations: + spec.ingressClassName: traefik + traefik.ingress.kubernetes.io/router.entrypoints: web +spec: + rules: + - host: code.f3s.buetow.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: forgejo + port: + number: 80 +--- +# LAN ingress. *.f3s.lan resolves to the storage VIP via Pi-hole, and Traefik +# terminates TLS here with the shared f3s-lan-tls cert. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: forgejo-ingress-lan + namespace: services + annotations: + spec.ingressClassName: traefik + traefik.ingress.kubernetes.io/router.entrypoints: web,websecure +spec: + tls: + - hosts: + - code.f3s.lan.buetow.org + secretName: f3s-lan-tls + rules: + - host: code.f3s.lan.buetow.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: forgejo + port: + number: 80 diff --git a/f3s/forgejo/helm-chart/templates/persistent-volume.yaml b/f3s/forgejo/helm-chart/templates/persistent-volume.yaml new file mode 100644 index 0000000..40c8788 --- /dev/null +++ b/f3s/forgejo/helm-chart/templates/persistent-volume.yaml @@ -0,0 +1,66 @@ +# Forgejo storage. Deliberately separate from the git-server/cgit volume: +# Forgejo owns its own repositories under /data/nfs/k3svolumes/forgejo/data and +# never touches the 80 bare repos cgit serves out of +# /data/nfs/k3svolumes/git-server/repos. +# +# Both directories must exist and contain a .nfs-sentinel file before the pod +# starts -- see the initContainers in deployment.yaml and the README. +apiVersion: v1 +kind: PersistentVolume +metadata: + name: forgejo-data-pv +spec: + capacity: + storage: 20Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + hostPath: + path: /data/nfs/k3svolumes/forgejo/data + type: Directory +--- +# app.ini lives here, and with it the SECRET_KEY and INTERNAL_TOKEN that Forgejo +# generates on first start. Losing this volume invalidates existing sessions and +# any stored credentials, so it is kept on NFS (ZFS-snapshotted) rather than in +# an emptyDir. +apiVersion: v1 +kind: PersistentVolume +metadata: + name: forgejo-config-pv +spec: + capacity: + storage: 1Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + hostPath: + path: /data/nfs/k3svolumes/forgejo/config + type: Directory +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: forgejo-data-pvc + namespace: services +spec: + storageClassName: "" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: forgejo-config-pvc + namespace: services +spec: + storageClassName: "" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/f3s/forgejo/helm-chart/templates/service.yaml b/f3s/forgejo/helm-chart/templates/service.yaml new file mode 100644 index 0000000..7c3bfe7 --- /dev/null +++ b/f3s/forgejo/helm-chart/templates/service.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: Service +metadata: + name: forgejo + namespace: services + labels: + app: forgejo +spec: + selector: + app: forgejo + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 3000 + type: ClusterIP +--- +# SSH for git clone/push. NodePort 30222 -- git-server/cgit already owns 30022, +# and the two installs are intentionally independent. +apiVersion: v1 +kind: Service +metadata: + name: forgejo-ssh + namespace: services + labels: + app: forgejo +spec: + selector: + app: forgejo + ports: + - name: ssh + protocol: TCP + port: 2222 + targetPort: 2222 + nodePort: 30222 + type: NodePort -- cgit v1.2.3