Forgejo Git Hosting
Forgejo is a self-hosted, lightweight Git hosting service. It is deployed on the K3s cluster as a Helm-managed workload, accessible at git.kevinryan.io. All repository data is stored on a persistent volume, and database credentials are sourced from Azure Key Vault via the External Secrets Operator.
Architecture
Section titled “Architecture”graph TD
subgraph cluster["K3s Cluster"]
traefik["Traefik\n(websecure)"]
forgejo["Forgejo\n(git.kevinryan.io)"]
pvc["PersistentVolume\n(10Gi — git repos)"]
eso["External Secrets\nOperator"]
end
cloudflare["Cloudflare\n(git.kevinryan.io A record)"]
pg["Azure PostgreSQL\n(forgejo_db)"]
kv["Azure Key Vault"]
browser["Git client / browser"] -->|"HTTPS"| cloudflare
cloudflare --> traefik
traefik --> forgejo
forgejo -->|"read/write"| pg
forgejo -->|"repository data"| pvc
kv -->|"pg-admin-*\nforgejo-admin-password"| eso
eso -->|"forgejo-db secret\nforgejo-admin secret"| forgejo
Kubernetes Deployment
Section titled “Kubernetes Deployment”Forgejo is deployed via the forgejo-contrib Helm chart (OCI registry at oci://codeberg.org/forgejo-contrib), managed by Flux CD. All manifests live under k8s/forgejo/.
Namespace
Section titled “Namespace”apiVersion: v1kind: Namespacemetadata: name: forgejoHelm Repository
Section titled “Helm Repository”The chart is served as an OCI artifact from Codeberg. The HelmRepository uses type: oci to address the registry directly — no index URL needed.
apiVersion: source.toolkit.fluxcd.io/v1kind: HelmRepositorymetadata: name: forgejo namespace: forgejospec: type: oci url: oci://codeberg.org/forgejo-contrib interval: 1hHelm Release
Section titled “Helm Release”apiVersion: helm.toolkit.fluxcd.io/v2kind: HelmReleasemetadata: name: forgejo namespace: forgejospec: interval: 1h chart: spec: chart: forgejo version: "16.2.1" sourceRef: kind: HelmRepository name: forgejo namespace: forgejo interval: 1h install: remediation: retries: 5 upgrade: remediation: retries: 5 values: postgresql: enabled: false postgresql-ha: enabled: false persistence: enabled: true size: 10Gi gitea: admin: existingSecret: forgejo-admin config: server: DOMAIN: git.kevinryan.io ROOT_URL: https://git.kevinryan.io HTTP_PORT: 3000 database: DB_TYPE: postgres NAME: forgejo_db SSL_MODE: require additionalConfigFromEnvs: - name: FORGEJO__DATABASE__HOST valueFrom: secretKeyRef: name: forgejo-db key: db_host - name: FORGEJO__DATABASE__USER valueFrom: secretKeyRef: name: forgejo-db key: db_user - name: FORGEJO__DATABASE__PASSWD valueFrom: secretKeyRef: name: forgejo-db key: db_passwordKey configuration decisions:
- Built-in databases disabled —
postgresql.enabled: falseandpostgresql-ha.enabled: falseprevent the chart from deploying its own PostgreSQL sidecars. The external Azure PostgreSQL instance is used instead. - Persistence enabled — A 10Gi
PersistentVolumeClaimis created for the Forgejo data directory. Without this, all git repository data would be lost on pod restart. ROOT_URLset explicitly — Required for Forgejo to generate correct clone URLs and redirect links. Without it, Forgejo infers the URL from the incoming request, which can break behind a reverse proxy.- DB credentials via env vars — Sensitive values (host, user, password) are injected at runtime from the
forgejo-dbKubernetes secret usingFORGEJO__DATABASE__*environment variables, which Forgejo reads directly into itsapp.iniconfiguration. - Admin secret — The
gitea.admin.existingSecretfield points to theforgejo-adminsecret, which providesusername,email, andpasswordkeys for the initial admin account.
Ingress
Section titled “Ingress”Traefik routes git.kevinryan.io to the Forgejo HTTP service on port 3000:
apiVersion: traefik.io/v1alpha1kind: IngressRoutemetadata: name: forgejo namespace: forgejospec: entryPoints: - websecure routes: - match: Host(`git.kevinryan.io`) kind: Rule services: - name: forgejo-http port: 3000 tls: {}TLS termination is handled by Traefik using the cluster-wide certificate configuration (same pattern as all other services).
Secrets Management
Section titled “Secrets Management”Two ExternalSecret resources pull credentials from Azure Key Vault via the azure-keyvault ClusterSecretStore.
Database credentials (forgejo-db)
Section titled “Database credentials (forgejo-db)”apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: forgejo-db namespace: forgejospec: refreshInterval: 1h secretStoreRef: kind: ClusterSecretStore name: azure-keyvault target: name: forgejo-db creationPolicy: Owner data: - secretKey: db_host remoteRef: key: pg-fqdn - secretKey: db_user remoteRef: key: pg-admin-username - secretKey: db_password remoteRef: key: pg-admin-passwordThe three keys are consumed by the HelmRelease via additionalConfigFromEnvs — each maps to a FORGEJO__DATABASE__* environment variable that Forgejo applies to its running configuration.
Admin credentials (forgejo-admin)
Section titled “Admin credentials (forgejo-admin)”apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: forgejo-admin namespace: forgejospec: refreshInterval: 1h secretStoreRef: kind: ClusterSecretStore name: azure-keyvault target: name: forgejo-admin creationPolicy: Owner template: engineVersion: v2 data: username: "forgejo-admin" password: "{{ .forgejo_admin_password }}" data: - secretKey: forgejo_admin_password remoteRef: key: forgejo-admin-passwordThe admin username and email are hardcoded in the template; only the password is stored in Key Vault. The forgejo-admin-password secret is generated by Terraform using a random_password resource and stored in Key Vault on terraform apply.
Database
Section titled “Database”Forgejo stores all data in a dedicated database on the shared Azure PostgreSQL Flexible Server:
| Setting | Value |
|---|---|
| Database name | forgejo_db |
| Server | Azure PostgreSQL Flexible Server (v16), psql-kevinryan-io |
| SSL | Required (sslmode=require) |
| Network | Private subnet, no public access |
| Provisioned by | Terraform (infra/main.tf — module.postgresql.databases) |
Flux CD Integration
Section titled “Flux CD Integration”The Forgejo Kustomization declares a dependsOn on external-secrets-store, ensuring the ClusterSecretStore (and therefore Azure Key Vault access) is available before Forgejo’s ExternalSecret resources are reconciled.
apiVersion: kustomize.toolkit.fluxcd.io/v1kind: Kustomizationmetadata: name: forgejo namespace: flux-systemspec: dependsOn: - name: external-secrets-store interval: 10m0s path: ./k8s/forgejo prune: true sourceRef: kind: GitRepository name: flux-systemThe HelmRelease is reconciled by the Flux helm-controller. On first deployment, the controller pulls the chart from oci://codeberg.org/forgejo-contrib/forgejo:16.2.1, creates the PersistentVolumeClaim, and waits for the forgejo-db and forgejo-admin secrets to be populated by ESO before the pod starts.
The git.kevinryan.io A record is managed by Terraform as a standalone resource — it is intentionally not included in the cloudflare module’s subdomains list. The module’s subdomains list feeds a Cloudflare cache ruleset that caches all traffic for 24 hours, which would break git push/pull/clone operations.
resource "cloudflare_record" "git" { zone_id = var.cloudflare_zone_id name = "git" content = module.network.public_ip_address type = "A" proxied = true ttl = 1}Traffic is proxied through Cloudflare for DDoS protection, but no caching rules apply to git.kevinryan.io.
Post-Deployment Notes
Section titled “Post-Deployment Notes”- SSH access is not configured. Only HTTPS git operations are available. A
LoadBalancerservice on port 22 can be added if SSH-based git is needed. SECRET_KEYandINTERNAL_TOKENare auto-generated by the Forgejo chart on first install and stored in a Kubernetes Secret. They survive pod restarts but are lost on chart uninstall. If full reproducibility is required, these should be generated once and stored in Key Vault.- After adding
forgejo_dbto the Terraform PostgreSQL module’sdatabaseslist,terraform applymust be run before the Helm release will reconcile — Forgejo’s migrations will fail if the database does not exist.