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 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 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 — allapp.inikeys configurable via environment variables- Database-aware backup —
tarfor SQLite,pg_dump/mysqldumpfor 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-credentialsConfiguration 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. |
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.comPass 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.
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.