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>
79 lines
3.4 KiB
Makefile
79 lines
3.4 KiB
Makefile
# Convenience wrappers. Everything here is also runnable by hand; nothing in the
|
|
# deployment depends on make.
|
|
|
|
PROJECT ?= $(shell cd infra && pulumi config get gcp:project 2>/dev/null)
|
|
REGION ?= $(shell cd infra && pulumi config get gcp:region 2>/dev/null || echo us-east1)
|
|
ZONE ?= $(shell cd infra && pulumi config get gitea:zone 2>/dev/null || echo $(REGION)-a)
|
|
VM ?= gitea-vm
|
|
|
|
.PHONY: help
|
|
help:
|
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
|
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
|
|
|
|
.PHONY: bootstrap
|
|
bootstrap: ## One-time project setup (run before the first `make up`)
|
|
scripts/bootstrap.sh $(PROJECT) $(REGION)
|
|
|
|
.PHONY: fmt
|
|
fmt: ## Format Go sources
|
|
cd infra && gofmt -w .
|
|
|
|
.PHONY: check
|
|
check: ## Build and vet the Pulumi program, and syntax-check the shell scripts
|
|
cd infra && go build ./... && go vet ./...
|
|
bash -n vm/bootstrap.sh scripts/bootstrap.sh
|
|
@command -v shellcheck >/dev/null && shellcheck -S warning vm/bootstrap.sh scripts/bootstrap.sh || echo "shellcheck not installed -- skipped"
|
|
|
|
.PHONY: preview
|
|
preview: check ## Show what `pulumi up` would change
|
|
cd infra && pulumi preview
|
|
|
|
.PHONY: up
|
|
up: check ## Apply the infrastructure
|
|
cd infra && pulumi up
|
|
|
|
# --region: the triggers are 2nd-gen and therefore regional. Without this flag
|
|
# `builds submit` runs in the global region -- a different worker pool from
|
|
# every automated build.
|
|
# --service-account: the build's last step reaches the VM over an IAP tunnel,
|
|
# needing iap.tunnelResourceAccessor + compute.osAdminLogin. Those are granted
|
|
# to cb-image@, not to whatever default Cloud Build account this project
|
|
# happens to have -- and on newer projects the legacy default does not exist.
|
|
# Without this the images push fine and the rollout step fails.
|
|
.PHONY: build
|
|
build: ## Build and roll out the container images via Cloud Build
|
|
gcloud builds submit --config cloudbuild/image.yaml --project $(PROJECT) \
|
|
--region=$(REGION) \
|
|
--service-account=projects/$(PROJECT)/serviceAccounts/cb-image@$(PROJECT).iam.gserviceaccount.com \
|
|
--substitutions=_REGION=$(REGION),_ZONE=$(ZONE)
|
|
|
|
.PHONY: rollout
|
|
rollout: ## Pull the latest :prod images onto the VM right now
|
|
gcloud compute ssh $(VM) --zone=$(ZONE) --tunnel-through-iap --project=$(PROJECT) \
|
|
--command 'sudo systemctl start podman-auto-update.service && sudo podman ps'
|
|
|
|
.PHONY: sync
|
|
sync: ## Re-render VM config from the bucket and restart what changed
|
|
gcloud compute ssh $(VM) --zone=$(ZONE) --tunnel-through-iap --project=$(PROJECT) \
|
|
--command 'sudo systemctl start gitea-config-sync.service && sudo journalctl -u gitea-config-sync -n 40 --no-pager'
|
|
|
|
.PHONY: ssh
|
|
ssh: ## Shell on the VM through IAP
|
|
gcloud compute ssh $(VM) --zone=$(ZONE) --tunnel-through-iap --project=$(PROJECT)
|
|
|
|
.PHONY: logs
|
|
logs: ## Tail Gitea and Caddy logs
|
|
gcloud compute ssh $(VM) --zone=$(ZONE) --tunnel-through-iap --project=$(PROJECT) \
|
|
--command 'sudo journalctl -u gitea -u caddy -f'
|
|
|
|
.PHONY: status
|
|
status: ## Health summary from the VM
|
|
gcloud compute ssh $(VM) --zone=$(ZONE) --tunnel-through-iap --project=$(PROJECT) \
|
|
--command 'sudo systemctl status --no-pager gitea caddy nftables fail2ban; sudo podman ps; sudo systemctl list-timers --no-pager'
|
|
|
|
.PHONY: backup
|
|
backup: ## Take an on-demand Gitea dump to the backup bucket
|
|
gcloud compute ssh $(VM) --zone=$(ZONE) --tunnel-through-iap --project=$(PROJECT) \
|
|
--command 'sudo systemctl start gitea-backup.service && sudo journalctl -u gitea-backup -n 20 --no-pager'
|