Files
Gitea/docs/migrate-to-gitea-scm.md
T
JMR-devandClaude Opus 5 c0382d5d31 Gitea on GCE: podman quadlets, Pulumi, Cloud Build
Self-hosted Gitea on a single e2-small AlmaLinux 10 VM in us-east1,
serving gitea.jasonmross.dev.

Runtime is podman quadlets (systemd .container/.network/.volume units).
Both images are built on Debian 13: Gitea from a GPG-verified release
binary, and Caddy from an xcaddy build carrying the Google Cloud DNS
provider (ACME DNS-01) and the Coraza WAF with the OWASP CRS embedded.

Infrastructure is a Pulumi program in Go against a GCS state backend.
Cloud Build handles CI: a push trigger for images, one for infra, and a
weekly scheduled rebuild. Everything Cloud Build touches is 2nd gen.

Notable design decisions, each documented where it lives:

- Quadlets track a floating :prod tag. AutoUpdate=registry compares
  digests for a tag, so a digest-pinned image silently disables
  auto-updates.
- Git transport and LFS bypass the WAF. With the bypass removed, a plain
  git push returns 403 -- packfiles trip CRS reliably.
- gitea:wafMode drives both SecRuleEngine and whether the fail2ban jail
  acting on WAF verdicts exists. Banning on detections that were never
  blocks would turn a tuning false positive into an nftables ban.
- fail2ban bans at the nftables prerouting hook. Published container
  ports are DNAT'd and never traverse INPUT, where the stock actions
  install their rules.
- The DNS zone, backup bucket, and Gitea signing secrets are not
  Pulumi-owned, so pulumi destroy cannot take them with it.
- The podman subnet is pinned because it is what Gitea's
  REVERSE_PROXY_TRUSTED_PROXIES names.

Three update layers: dnf5-automatic for the OS, podman-auto-update with
health-gated rollback for containers, and a weekly image rebuild that
gives the second layer something to pull.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:45:33 -05:00

2.4 KiB

Moving CI off GitHub and onto this Gitea instance

Today the repository lives on GitHub and Cloud Build watches it through a cloudbuildv2 connection. The intent is to move the repository into the Gitea instance this repo deploys. Doing that well is mostly about not building a loop that can strand itself.

The bootstrapping problem

The runner that deploys the VM must not live on the VM it deploys. If a Gitea Actions runner on gitea-vm executes the infra pipeline, then any change that restarts Gitea — a config sync, an image rollout, a reboot — kills the job mid-flight. Worse, a bad change locks you out of the tool needed to fix it.

Two workable shapes:

  1. Keep Cloud Build as the executor. Gitea fires a webhook; Cloud Build runs the build. Cloud Build has no first-class Gitea trigger, so this means a small authenticated endpoint (Cloud Run or a Cloud Function) that validates the webhook signature and calls cloudbuild.projects.triggers.run. The deploy path stays entirely outside the VM. This is the recommended option.
  2. A Gitea Actions runner on separate infrastructure — a second small VM or a Cloud Run job. Self-contained, but you now operate a runner, and the infra pipeline still needs GCP credentials.

Either way, keep the GitHub trigger working until the replacement has successfully deployed at least once.

Order of operations

  1. [actions] ENABLED = true is already set in vm/config/app.ini.tmpl, so no Gitea-side config change is needed to start.
  2. Mirror the repository into Gitea and verify history, LFS objects, and tags.
  3. Stand up the chosen executor and prove it can run cloudbuild/image.yaml end to end, including the IAP rollout step.
  4. Point gitea:githubOwner at nothing / remove the GitHub trigger. The Pulumi program already tolerates GitHub being unconfigured — build.New logs a warning and skips the triggers — so the stack still applies cleanly.
  5. Keep the GitHub repo as an archived mirror for a while. It is the cheapest possible disaster recovery for the repo that contains the deployment.

Do not forget

  • The cb-infra@ service account and the Pulumi state bucket are created by scripts/bootstrap.sh, not by Pulumi. They survive this migration untouched.
  • Backups (gitea-backup.timer) become considerably more important once the repository that describes the infrastructure lives on the infrastructure. Verify a restore before you cut over, not after.