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
postgrescluster, 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:
Clean rolling updates: move the queue, issue indexer and sessions off the volume, with no new infrastructure.
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.