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>
2.4 KiB
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:
- 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. - 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
[actions] ENABLED = trueis already set invm/config/app.ini.tmpl, so no Gitea-side config change is needed to start.- Mirror the repository into Gitea and verify history, LFS objects, and tags.
- Stand up the chosen executor and prove it can run
cloudbuild/image.yamlend to end, including the IAP rollout step. - Point
gitea:githubOwnerat nothing / remove the GitHub trigger. The Pulumi program already tolerates GitHub being unconfigured —build.Newlogs a warning and skips the triggers — so the stack still applies cleanly. - 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 byscripts/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.