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>
65 lines
1.9 KiB
YAML
65 lines
1.9 KiB
YAML
# Run Pulumi, then push the refreshed VM configuration onto the instance.
|
|
#
|
|
# Pulumi uploads the vm/ tree as bucket objects; the last step is what makes the
|
|
# VM actually pick them up, instead of waiting for the next reboot.
|
|
|
|
substitutions:
|
|
_REGION: us-east1
|
|
_ZONE: us-east1-b
|
|
_VM: gitea-vm
|
|
_STACK: prod
|
|
|
|
options:
|
|
logging: CLOUD_LOGGING_ONLY
|
|
|
|
timeout: 1800s
|
|
|
|
availableSecrets:
|
|
secretManager:
|
|
- versionName: projects/$PROJECT_ID/secrets/pulumi-config-passphrase/versions/latest
|
|
env: PULUMI_CONFIG_PASSPHRASE
|
|
|
|
steps:
|
|
- id: pulumi
|
|
name: pulumi/pulumi-go:latest
|
|
dir: infra
|
|
entrypoint: bash
|
|
secretEnv: [PULUMI_CONFIG_PASSPHRASE]
|
|
args:
|
|
- -c
|
|
- |
|
|
set -euo pipefail
|
|
# Self-managed GCS backend: no external SaaS dependency, and the state
|
|
# bucket is versioned so history is recoverable.
|
|
pulumi login "gs://$PROJECT_ID-pulumi-state"
|
|
pulumi stack select "${_STACK}"
|
|
|
|
# Google credentials come from the build's metadata server; the GCS
|
|
# backend and the gcp provider both pick them up automatically.
|
|
if [ "$BRANCH_NAME" = "main" ]; then
|
|
pulumi up --yes --non-interactive
|
|
else
|
|
echo "branch $BRANCH_NAME is not main -- preview only"
|
|
pulumi preview --non-interactive
|
|
fi
|
|
|
|
- id: config-sync
|
|
name: gcr.io/google.com/cloudsdktool/cloud-sdk:slim
|
|
entrypoint: bash
|
|
env:
|
|
- HOME=/workspace
|
|
args:
|
|
- -c
|
|
- |
|
|
set -euo pipefail
|
|
if [ "$BRANCH_NAME" != "main" ]; then
|
|
echo "preview build -- skipping VM config sync"
|
|
exit 0
|
|
fi
|
|
# Re-render templates and restart only what actually changed.
|
|
gcloud compute ssh "${_VM}" \
|
|
--zone="${_ZONE}" \
|
|
--tunnel-through-iap \
|
|
--quiet \
|
|
--command 'sudo systemctl start gitea-config-sync.service && sudo systemctl status --no-pager gitea-config-sync.service'
|