Skip to main content

Overview

Gitea is a lightweight, self-hosted Git service — repositories, pull requests, issues, and a built-in package registry — backed by PostgreSQL. This template deploys a single Gitea server with persistent storage, an automatically wired PostgreSQL database, a public HTTPS web UI with Git-over-HTTPS, and optional Git-over-SSH. The site administrator’s login and the three long-lived signing keys are not template values. They come from one dictionary secret you create before installing, so they never pass through Helm and never land in the release.
Template version 1.1.0 is a breaking change. gitea.admin.username, gitea.admin.password, gitea.admin.email, gitea.security.secretKey, gitea.security.internalToken and gitea.security.jwtSecret were all removed, and an install or upgrade that still sets any of them now fails at render. If you are running 1.0.0, read Upgrading From Earlier Versions before you touch the release — two of those keys cannot be rotated safely.

Architecture

  • Gitea — Stateful, single-replica workload running the rootless image. Serves the web UI, Git-over-HTTPS, and the package registry on port 3000, plus the built-in SSH server on 2222.
  • PostgreSQL — Backing database provisioned from the postgres template as a subchart and connected to Gitea on startup.

What Gets Created

  • Stateful Gitea Workload — (RELEASE_NAME-gitea): the Gitea server, bootstrapped with the site-admin account from your auth secret on first boot.
  • Stateful PostgreSQL Workload — Single-replica Postgres, automatically connected to Gitea.
  • Volume Sets — A persistent volume set for Gitea (/var/lib/gitea: repositories, LFS objects, attachments, and SSH host keys) and one for PostgreSQL data, both with optional autoscaling.
  • Bootstrap Secret — (RELEASE_NAME-gitea-bootstrap): an opaque secret holding the admin-bootstrap script mounted into the container. The template creates no credential secret of its own.
  • Identity & Policy — An identity bound to the Gitea workload with reveal scoped to exactly three secrets: the bootstrap script, the PostgreSQL config secret, and the auth secret you created.
  • Direct Load Balancer (optional) — A raw-TCP port for Git-over-SSH, created only when ssh.enabled is true.
This template does not create a GVC. You must deploy it into an existing GVC.

Upgrading From Earlier Versions

Template version 1.0.0 took the admin login and all three security values as plain Helm values and shipped published defaults for every one of them. Those are not merely a web login: secretKey encrypts the 2FA secrets, access tokens and mirror credentials stored in the database. Each removed key is rejected at render with a message naming its replacement, so the upgrade fails before anything is applied and the running release is left untouched. There are no compatibility fallbacks.
Put your existing values into the secret — do not generate new ones for a live install. Two of the three cannot be rotated:
  • Changing secretKey makes every 2FA secret, access token and mirror credential already in the database permanently unreadable.
  • Changing jwtSecret invalidates every OAuth2 token already issued.
If your install is still carrying the published 1.0.0 defaults, everything it has encrypted is protected by values printed in a public repository — but rotating them in place is destructive in exactly the way above. The safe path is a fresh 1.1.0 install with new key material and a repository migration onto it. At minimum, change the administrator’s password in the Gitea UI immediately.

Prerequisites

One secret must exist before you install. Its values never pass through Helm, so they never land in the release. Secrets are org-level, so no GVC flag is involved.
1

Create the auth secret

A dictionary secret holding exactly six keys:
Set gitea.auth.secretName to the name you used. Nothing else is required for a default install.
2

Read the secret back later

The -o yaml is required — plain cpln secret reveal prints only a summary table, not the values:
jwtSecret has a format constraint the other two keys do not. It must be base64url of exactly 32 bytes — 43 characters, no padding — or Gitea errors on startup. That is what openssl rand -base64 32 | tr '+/' '-_' | tr -d '=' produces, and it is the reason the command pipes through tr. Do not substitute a hex string or a passphrase here. (Older guidance to run gitea generate secret needs the gitea binary, which you do not have outside the container.)
Create the secret before installing. The template refuses to render when the name is blank, but a name pointing at a secret that does not exist installs “successfully” and then wedges: every resource reports created, the workload never becomes ready, and cpln logs returns zero lines because no container ever starts. The only diagnostic is status.versions[].message:
It names the missing secret: The secret <name> no longer exists. Workload updates are paused until the secret is added or the reference to the secret removed. The command is get-deployments — plain cpln workload get has no versions key at all. Creating the secret recovers the workload on its own: poll for 5.5 to 10.5 minutes rather than time-boxing it (8 minutes 35 seconds measured here). cpln workload force-redeployment RELEASE_NAME-gitea --gvc GVC_NAME cuts that to roughly 90 seconds.

Installation

To install, follow the instructions for your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Configuration

The default values.yaml for this template:

Image and Resources

  • image — The Gitea image. The template uses the rootless variant, which runs as UID 1000 and serves SSH on port 2222 inside the container.
  • resources — Reservation and limit for the Gitea container (minCpu / maxCpu / minMemory / maxMemory).

Storage

  • volumeset.capacity — Initial Gitea volume size in GiB (minimum 10). Holds repositories, LFS objects, attachments, and SSH host keys.
  • volumeset.autoscaling.enabled — Automatically expand the volume as it fills. When enabled:
    • maxCapacity — Maximum volume size in GiB.
    • minFreePercentage — Trigger a scale-up when free space drops below this percentage.
    • scalingFactor — Multiply the current capacity by this factor when scaling up.

Gitea

  • gitea.auth.secretName — Name of the prerequisite dictionary secret holding the admin login and the three signing keys. It must exist before you install.
  • gitea.disableRegistrationtrue (default) restricts new accounts to admin invites; false allows open self-registration.

Access

  • publicAccess.enabled — Serves the HTTPS web UI, Git-over-HTTPS, and the package registry on the auto-assigned *.cpln.app canonical endpoint.
  • ssh.enabled — Exposes Git-over-SSH via a direct TCP load balancer. Off by default — see The SSH and HTTPS Endpoint Trade-off.
  • ssh.externalPort — The public SSH port clients connect to; also advertised in SSH clone URLs.
  • ssh.domain — The SSH host advertised in clone URLs. Empty uses the web domain.
  • internalAccess.type — Controls which workloads can reach Gitea over the internal network (none, same-gvc, same-org, or workload-list).
Public access is on by default, deliberately. git clone and git push from laptops and CI are the point of the service, and both are proven to work against the public endpoint. Self-registration is closed by default, and after 1.1.0 there is no published default credential to protect against — the admin login is one you created.Set publicAccess.enabled: false to keep Gitea inside the GVC and reach the UI with cpln port-forward RELEASE_NAME-gitea 3000:3000 --gvc GVC_NAME. A firewall change takes roughly 30 seconds to 5 minutes to propagate, so re-test rather than trusting the first response.

Backing Database

Bundled plumbing: the database serves Gitea only and is unreachable from outside the GVC, and nobody ever types this password into a client, so it stays a value.
  • postgres.credentials.username / password / database — Credentials for the bundled PostgreSQL, applied on first startup. Change the password before installing — the shipped change-me-gitea-db is a published placeholder.
  • postgres.config.credentialsSecretName — Name of the dictionary secret the chart creates from those three values and hands to the bundled PostgreSQL. Secret names are organization-wide, so give each release its own name; a second release left on the default is refused at install and creates nothing, leaving the first release untouched.
  • postgres.resources — Reservation and limit for the PostgreSQL container.
  • postgres.volumeset.capacity and postgres.volumeset.autoscaling — Initial Postgres volume size in GiB (minimum 10) and its autoscaling settings.
  • postgres.internalAccess.type — Controls which workloads can reach PostgreSQL.
PostgreSQL credentials are only applied on first startup, when the data directory is empty. Changing them afterwards has no effect on the running database — use PostgreSQL’s own commands (for example ALTER USER) instead.

Connecting

Sign in at the canonical endpoint to create repositories, users, and organizations. Clone and push over HTTPS against the same endpoint — full clone, push, and anonymous clone of a public repository all work from outside the GVC:

The SSH and HTTPS Endpoint Trade-off

A Control Plane workload has exactly one *.cpln.app canonical endpoint, and it can serve either the HTTPS web UI (web UI plus Git-over-HTTPS on port 443) or a raw-TCP load balancer for SSH — not both. Enabling ssh.enabled repoints that single endpoint to SSH, which takes the public web UI offline. Because of this, ssh.enabled is false by default, which keeps the public web UI and Git-over-HTTPS working out of the box. Git-over-HTTPS supports full clone, push, and pull, so SSH is not required for normal use.
Enable ssh.enabled: true only if you either (a) serve the web UI through a custom domain, so the canonical endpoint is free for SSH, or (b) only need Git-over-SSH. Turning it on repoints the public *.cpln.app endpoint to SSH on ssh.externalPort and makes the public HTTPS web UI on 443 unreachable.
With ssh.enabled: false, Gitea’s REST API still reports a placeholder ssh://git@localhost:2222/... clone URL on a repository object. It is cosmetic: the web UI’s clone widget offers only the HTTPS URL, so only a script reading ssh_url from the API is affected.

Important Notes

  • The auth secret must exist before you install. Without it the deployment wedges with no log output at all; see Prerequisites for the one diagnostic that names it.
  • Never rotate secretKey or jwtSecret after install. A new secretKey makes existing 2FA secrets, tokens and mirror credentials permanently unreadable, and a new jwtSecret invalidates issued OAuth2 tokens.
  • jwtSecret must be base64url of exactly 32 bytes — any other length is rejected at startup. Use the command in Prerequisites.
  • Public SSH and the public web UI cannot share one endpoint — SSH is off by default. See The SSH and HTTPS Endpoint Trade-off.
  • Single replica only. A rolling restart or upgrade incurs brief downtime. Do not raise the workload scale above 1 — replicas would each get separate repository volumes and corrupt state.
  • The first Helm upgrade after an install re-applies the bundled PostgreSQL, so expect Gitea to be briefly unreachable while the database restarts. Later upgrades do not do this.
  • Editing the auth secret after first boot does not change the existing administrator account — the data directory keeps it. Change the password in the Gitea UI instead.
  • Data lives on the volume set and survives redeploys under the same release name. To fully reset, helm uninstall (which deletes the volume set) then reinstall.

External References

Gitea Documentation

Official Gitea product documentation

Configuration Cheat Sheet

Full reference of Gitea configuration options

Package Registry

Using Gitea’s built-in package registry

Gitea Template

View the source files, default values, and chart definition