Gitea

The git server, and the container and Helm chart registry. Argo CD deploys from it, and Tekton builds from it (GitOps & CI).

The solution

  • Gitea, one rootless pod, at gitea.obiwalker.com, with login through Zitadel.

  • Database: a tenant on the shared postgres cluster, with its own database and login role. The cluster runs 3 instances, each on a different node's local disk, backed up to S3.

  • Files: repositories and other data on an NFS volume from nfs-1.

  • Registry: CI pushes each build's image here, tagged with the commit.

  • Backups: every night, a tarball of the whole volume goes to S3 on a keyless role that can only write.

Why a tenant on the shared cluster

Gitea has no install-order problem like Zitadel's, so it doesn't need a database cluster of its own. The shared cluster gives each tenant its own database, role and password, and adding one is two templates and a role entry in the cluster.

TODO: your reason for sharing, in your words.

Backups

The database is already covered: it's a tenant on the shared cluster, with point-in-time recovery from S3 (Data platform). Everything else is on the NFS volume: repositories, the registry's images and charts, attachments, and app.ini, which holds the keys Gitea encrypts data with. Until this backup, nfs-1 was the only copy of the git server Argo CD deploys from.

backup pod (on Gitea's node, volume mounted read-only)
 ├─ tar ──── the volume ────▶ gitea-.tar.gz
 └─ upload ─ its own token ─▶ AWS STS ──▶ write-only role ──▶ S3, gitea/ only
  • Write-only. The role can add backups and do nothing else: it can't list, read or delete them. Anything that takes over the cluster can't wipe its own backups. An S3 rule expires them after 7 days.

  • The file name is the restore point. A restore takes the files from the tarball and brings Gitea's database to the time in its name with point-in-time recovery, so the two match. The 7 days fit inside the database's 14-day window.

  • Safe while Gitea runs. tar takes no locks, and the files Gitea keeps open, its queue and search index, are left out. Gitea rebuilds them.

  • A plain tarball. Restoring needs the AWS CLI and tar, nothing else.

Restore drill, 2026-10-21:

Check Live Restored
Repositories, every one passing git fsck 15 15
Encryption keys in app.ini 2 2
Registry packages 572 MB 571 MB

The 604 MB backup took under three minutes. The role's limits were checked from inside the cluster: listing, reading and deleting backups were all denied.

Trade-offs and known limits

  • Nightly, not continuous. Unlike the database, a push made after the 04:00 UTC backup is only in S3 the next night. Restoring rolls Gitea's database back to the same point, so the two still match.

  • A failed backup is silent. Nothing alerts on it yet (Roadmap).

  • A full copy every night. About 600 MB, mostly registry images that are already compressed. At this size, 7 copies cost pennies a month, and a single file is the easiest thing to restore.

  • One pod. Gitea's default queue takes an exclusive lock on the shared volume, so a second pod can't start, not even during a rolling update.

Next

A written design for more than one pod:

  1. Clean rolling updates: move the queue, issue indexer and sessions off the volume, with no new infrastructure.

  2. Concurrent pods:

    • Add a small Redis for the queue, cache and sessions.

    • Move large files to MinIO.

    • Give every pod the same keys from sealed secrets.