Skip to content

Gitea

Deploy Gitea on Kubernetes — a lightweight self-hosted Git service with a full web UI, issue tracking, CI/CD (Gitea Actions), package registry, and SSH access. The chart uses the official rootless image (UID 1000) for improved security.

Gitea 28 upgrade

Gitea 28 changes Git network allow/block-list behavior and defaults completed Actions runs to 400 days of retention. Review GITEA__security__* overrides and set GITEA__actions__RUN_RETENTION_DAYS=0 before upgrading if historical runs must be retained. Back up the database, repositories, LFS objects, packages, attachments, and configuration first.

gitea.rootUrl is required — wrong value breaks all clone URLs and webhooks

Gitea uses gitea.rootUrl to construct all clone URLs, webhook callback addresses, and OAuth redirect URIs. If this value is wrong or missing, users will see incorrect clone URLs and webhooks will fail silently. Always set it to the full public URL (e.g. https://git.example.com/).

Key Features

  • Rootless official image — runs as UID 1000 for improved pod security
  • Dual service — separate HTTP (3000) and SSH (2222) services
  • Configurable HTTP listener — the process listener and Kubernetes Service port remain independent
  • Three database backends — SQLite (default), PostgreSQL, MySQL with auto-detection
  • Singleton-safe rollouts — Recreate avoids concurrent writers and ReadWriteOnce multi-attach failures
  • Pod annotations — integrate secret reloaders and other rollout controllers without post-render patches
  • Admin bootstrap Job — optional first-run Job to create the admin user
  • GITEA__ env configuration — all app.ini keys configurable via environment variables
  • Database-aware backup — tar for SQLite, pg_dump/mysqldump for SQL databases
  • External Secrets guard — admin credentials can be reconciled by ESO without chart-managed drift
  • Optional NetworkPolicy — limit HTTP and SSH ingress to declared peers

Chart Documentation

Installation

HTTPS repository:

helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install gitea helmforge/gitea -f values.yaml

OCI registry:

helm install gitea oci://ghcr.io/helmforgedev/helm/gitea -f values.yaml

Deployment Examples

# values.yaml — Gitea with SQLite (zero database configuration)
gitea:
  rootUrl: 'https://git.example.com/' # required; wrong value breaks all clone URLs
  sshDomain: git.example.com
  sshPort: 2222
  disableRegistration: true # private instance
  requireSignIn: true # require login to view any page

admin:
  existingSecret: gitea-admin-credentials
  # secret keys: admin-username, admin-password, admin-email

persistence:
  enabled: true
  size: 50Gi # repositories + LFS objects + config + SQLite

ingress:
  enabled: true
  ingressClassName: traefik
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: git.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: gitea-tls
      hosts:
        - git.example.com
# values.yaml — Gitea with bundled PostgreSQL (recommended for production)
gitea:
  rootUrl: 'https://git.example.com/'
  sshDomain: git.example.com
  disableRegistration: true
  requireSignIn: true

admin:
  existingSecret: gitea-admin-credentials

postgresql:
  enabled: true
  auth:
    database: gitea
    username: gitea
    password: 'strong-db-password'
  primary:
    persistence:
      enabled: true
      size: 20Gi

persistence:
  enabled: true
  size: 100Gi # repositories + LFS objects (no SQLite with PostgreSQL active)

backup:
  enabled: true
  schedule: '0 3 * * *'
  s3:
    endpoint: https://s3.amazonaws.com
    bucket: gitea-backups
    existingSecret: gitea-s3-credentials

ingress:
  enabled: true
  ingressClassName: traefik
  hosts:
    - host: git.example.com
      paths:
        - path: /
          pathType: Prefix
# values.yaml — Gitea with SSH exposed via NodePort for external Git access
gitea:
  rootUrl: 'https://git.example.com/'
  sshDomain: k8s-node.example.com # public hostname or IP of the Kubernetes node
  sshPort: 30022 # must match service.ssh.nodePort

admin:
  existingSecret: gitea-admin-credentials

service:
  http:
    type: ClusterIP
    port: 3000
  ssh:
    enabled: true
    type: NodePort
    port: 2222
    nodePort: 30022 # expose SSH on node port 30022

postgresql:
  enabled: true
  auth:
    password: 'strong-db-password'

persistence:
  enabled: true
  size: 100Gi

ingress:
  enabled: true
  ingressClassName: traefik
  hosts:
    - host: git.example.com
      paths:
        - path: /
          pathType: Prefix
# values.yaml — Gitea with external PostgreSQL and SMTP via GITEA__ env
gitea:
  rootUrl: 'https://git.example.com/'
  sshDomain: git.example.com
  disableRegistration: true
  requireSignIn: true
  extraEnv:
    - name: GITEA__mailer__ENABLED
      value: 'true'
    - name: GITEA__mailer__FROM
      value: '[email protected]'
    - name: GITEA__mailer__SMTP_ADDR
      value: smtp.example.com
    - name: GITEA__mailer__SMTP_PORT
      value: '587'
    - name: GITEA__mailer__USER
      valueFrom:
        secretKeyRef:
          name: gitea-smtp-credentials
          key: username
    - name: GITEA__mailer__PASSWD
      valueFrom:
        secretKeyRef:
          name: gitea-smtp-credentials
          key: password

admin:
  existingSecret: gitea-admin-credentials

database:
  mode: external
  external:
    vendor: postgres
    host: postgres.database.svc.cluster.local
    name: gitea
    username: gitea
    existingSecret: gitea-db-credentials
    existingSecretPasswordKey: database-password

persistence:
  enabled: true
  size: 100Gi

backup:
  enabled: true
  schedule: '0 3 * * *'
  s3:
    endpoint: https://s3.amazonaws.com
    bucket: gitea-backups
    existingSecret: gitea-s3-credentials

Configuration Reference

Core

Parameter Type Default Description
replicaCount integer 1 Pod replicas; the default state model is singleton.
strategy.type string Recreate Deployment strategy for single-writer local state.
podAnnotations object {} Additional annotations for the Gitea pod.
nameOverride string "" Override the chart name.
fullnameOverride string "" Override the full release name.
containerPorts.http integer 3000 HTTP listener used by the Gitea process.

Image

Parameter Type Default Description
image.repository string docker.io/gitea/gitea Gitea image.
image.tag string "28.2.0-rootless" Rootless image tag (UID 1000).

Gitea Configuration

Parameter Type Default Description
gitea.appName string "Gitea: ..." Application display name.
gitea.runMode string prod Run mode: dev, prod, test.
gitea.rootUrl string "" Required. Full public URL (e.g. https://git.example.com/).
gitea.sshDomain string "" SSH domain shown in clone URLs. Defaults to localhost unless set.
gitea.sshPort integer 2222 SSH listen port (rootless image default).
gitea.lfsEnabled boolean true Enable Git Large File Storage.
gitea.disableRegistration boolean false Disable user self-registration. Set true for private instances.
gitea.requireSignIn boolean false Require login to view any page.
gitea.extraEnv array [] Extra env vars in GITEA__SECTION__KEY format for full app.ini control.
Configure any app.ini setting via GITEA__SECTION__KEY environment variables

Gitea supports overriding any app.ini configuration key via environment variables using the pattern GITEA__<SECTION>__<KEY>. For example, to enable SMTP:

GITEA__mailer__ENABLED=true
GITEA__mailer__SMTP_ADDR=smtp.example.com

Pass these via gitea.extraEnv; for sensitive values, use valueFrom.secretKeyRef.

Admin User

Parameter Type Default Description
admin.username string "" Admin username. Triggers post-install Job to create user if set.
admin.password string "" Admin password. Ignored when existingSecret is set.
admin.email string "" Admin email address.
admin.existingSecret string "" Existing secret. Keys: admin-username, admin-password, admin-email.

Database

Auto-detection precedence (database.mode: auto):

Priority Condition Result
1 database.external.host or external.existingSecret External DB
2 postgresql.enabled: true PostgreSQL subchart
3 mysql.enabled: true MySQL subchart
4 None of the above SQLite (default)
Parameter Type Default Description
database.mode string auto Mode: auto, sqlite, external, postgresql, mysql.
database.sqlite.file string /var/lib/gitea/data/gitea.db SQLite file path inside the data volume.
database.external.vendor string postgres External DB vendor: postgres or mysql.
database.external.host string "" External database hostname.
database.external.existingSecret string "" Existing secret with database password.

Subcharts

Parameter Type Default Description
postgresql.enabled boolean false Deploy the bundled PostgreSQL subchart.
postgresql.auth.password string "" Password. Auto-generated if empty.
postgresql.primary.persistence.size string 8Gi PVC size for PostgreSQL.
mysql.enabled boolean false Deploy the bundled MySQL subchart.
mysql.primary.persistence.size string 8Gi PVC size for MySQL.

Persistence

Parameter Type Default Description
persistence.enabled boolean true Enable PVC for /var/lib/gitea (repositories, LFS, config, SQLite).
persistence.size string 10Gi PVC size. Size based on repository count and LFS usage.
persistence.storageClass string "" StorageClass for the PVC.
persistence.existingClaim string "" Use an existing PVC.

Service

Parameter Type Default Description
service.http.type string ClusterIP HTTP service type.
service.http.port integer 3000 HTTP service port.
service.ssh.enabled boolean true Enable a separate SSH service.
service.ssh.type string ClusterIP SSH service type (ClusterIP, NodePort, LoadBalancer).
service.ssh.port integer 2222 SSH service port.
service.ssh.nodePort string "" NodePort for SSH (only when type: NodePort).

containerPorts.http configures the actual process through GITEA__server__HTTP_PORT. service.http.port only controls the Service-facing port and targets the named http container port.

Security and Network Policy

Parameter Type Default Description
serviceAccount.automountServiceAccountToken boolean false Mount a Kubernetes API token into Gitea and chart jobs.
networkPolicy.enabled boolean false Render an ingress NetworkPolicy for the Gitea pod.
networkPolicy.ingressFrom array [] Allowed peers; empty permits pods in the release namespace.

The rootless process runs as UID/GID 1000, drops all capabilities, disables privilege escalation, and uses RuntimeDefault seccomp. The built-in wait-for-db init container inherits the same restricted security context. Enable the NetworkPolicy only after including the peers used by an ingress controller or Gateway running outside the release namespace. If admin bootstrap is configured, the chart always allows that Job to reach Gitea HTTP so the post-install hook cannot deadlock.

Ingress and Probes

Parameter Type Default Description
ingress.enabled boolean false Enable an Ingress for HTTP traffic.
ingress.ingressClassName string "" Ingress class name.
startupProbe.enabled boolean true Startup probe on /api/healthz.
livenessProbe.enabled boolean true Liveness probe on /api/healthz.
readinessProbe.enabled boolean true Readiness probe on /api/healthz.
resources object {} CPU and memory requests/limits.
extraManifests array [] Extra Kubernetes manifests.

Gateway API

Parameter Type Default Description
gateway.enabled boolean false Render a Gateway API HTTPRoute for HTTP traffic.
gateway.parentRefs array [] Required parent Gateway references.
gateway.hostnames array [] Optional HTTPRoute hostnames.
gateway.path string / HTTPRoute path value.
gateway.pathType string PathPrefix HTTPRoute path match type.

External Secrets

External Secrets integration reconciles admin credentials only. Set admin.existingSecret so the ExternalSecret owns the target Secret and the chart does not create a competing admin Secret.

admin:
  username: gitea_admin
  existingSecret: gitea-admin
externalSecrets:
  enabled: true
  secretStoreRef:
    name: platform-secrets
    kind: SecretStore
  data:
    - secretKey: admin-username
      remoteRef:
        key: gitea/admin
        property: username
    - secretKey: admin-password
      remoteRef:
        key: gitea/admin
        property: password
    - secretKey: admin-email
      remoteRef:
        key: gitea/admin
        property: email

Backup

Database-aware backup: SQLite archives /var/lib/gitea (repositories + LFS + SQLite). PostgreSQL uses pg_dump. MySQL uses mysqldump.

Backup covers the database only for PostgreSQL/MySQL — Git repositories reside in the PVC

When using PostgreSQL or MySQL, the backup CronJob only captures the database via pg_dump or mysqldump. Git repository data and LFS objects stored in the /var/lib/gitea PVC are not included. Back up the PVC separately using Velero, NFS snapshots, or storage provider snapshots. SQLite mode uses tar to archive the full /var/lib/gitea, covering repositories and the database.

Parameter Type Default Description
backup.enabled boolean false Enable scheduled S3 backup CronJob.
backup.schedule string "0 3 * * *" Cron schedule.
backup.s3.endpoint string "" S3-compatible endpoint URL.
backup.s3.bucket string "" Target bucket name.
backup.s3.existingSecret string "" Existing secret with S3 credentials.
backup.database.postgresDumpArgs string "" Extra arguments for pg_dump.
backup.database.mysqlDumpArgs string --single-transaction ... Extra mysqldump arguments.

Upgrade Notes

The Deployment now defaults to Recreate for SQLite, PostgreSQL, MySQL, and external database modes. SQL backends do not externalize repositories, queues, indexes, or configuration from the Gitea data volume. The new default prevents replacement-pod overlap during upgrades and avoids multi-attach failures with a ReadWriteOnce PVC. It does not enforce a singleton replica count: keep replicaCount: 1 unless every shared-state dependency has been externalized and concurrent storage access has been validated. Only select RollingUpdate after meeting the same requirements.

When changing an existing server-side-applied Deployment from RollingUpdate to Recreate, clear the API-defaulted rolling settings before the Helm upgrade:

kubectl patch deployment <release-name>-gitea --type=merge \
  -p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'

The chart omits stale strategy.rollingUpdate values whenever Recreate is selected, including upgrades that use --reuse-values.

The default image is docker.io/gitea/gitea:28.2.0-rootless. Gitea 28 introduces an internal proxy for Git network operations. Existing ALLOWED_HOST_LIST, BLOCKED_HOST_LIST, ALLOWED_DOMAINS, and BLOCKED_DOMAINS overrides must be reviewed: wildcard and domain matching changed, the external preset was removed, and an exclusive allow list now requires EGRESS_MODE=strict.

Completed Actions runs now default to 400 days of retention. Set GITEA__actions__RUN_RETENTION_DAYS=0 before upgrading if all historical runs must be retained. Gitea applies required database migrations at first startup; back up every persistent asset before production rollout. Review the v28.0.0 migration notes and v28.2.0 fixes.

More Information