Files
Gitea/vm/config/app.ini.tmpl
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

108 lines
3.3 KiB
Cheetah

; Rendered by vm/bootstrap.sh -> /etc/gitea/app.ini (owner 1000:1000, mode 0400).
;
; This file is FULLY MANAGED. Gitea's GITEA__section__KEY environment support
; comes from the upstream image's `environment-to-ini` entrypoint helper, which
; does not exist on our Debian base -- so configuration happens here, on the
; host, and the file is bind-mounted read-only.
;
; INSTALL_LOCK=true means the web installer is never reachable. Editing Gitea
; settings through the UI that map to app.ini will NOT persist; change the
; template in the repo and let the config-sync pipeline re-render it.
APP_NAME = ${APP_NAME}
RUN_USER = git
RUN_MODE = prod
WORK_PATH = /var/lib/gitea
[server]
PROTOCOL = http
HTTP_ADDR = 0.0.0.0
HTTP_PORT = 3000
DOMAIN = ${DOMAIN}
ROOT_URL = https://${DOMAIN}/
APP_DATA_PATH = /var/lib/gitea/data
DISABLE_SSH = false
; Built-in Go SSH server -- no sshd inside the container, and the host's sshd
; keeps port 22 for OS Login / IAP admin access.
START_SSH_SERVER = true
BUILTIN_SSH_SERVER_USER = git
SSH_DOMAIN = ${DOMAIN}
SSH_LISTEN_HOST = 0.0.0.0
SSH_LISTEN_PORT = 2222
; Advertised in clone URLs; must match the published host port.
SSH_PORT = 2222
LFS_START_SERVER = true
LFS_JWT_SECRET = ${GITEA_LFS_JWT_SECRET}
OFFLINE_MODE = true
[database]
DB_TYPE = sqlite3
PATH = /var/lib/gitea/data/gitea.db
; WAL is what makes SQLite tolerable under concurrent reads.
SQLITE_JOURNAL_MODE = WAL
SQLITE_TIMEOUT = 500
[repository]
ROOT = /var/lib/gitea/data/gitea-repositories
DEFAULT_BRANCH = main
DEFAULT_PRIVATE = private
DISABLE_HTTP_GIT = false
[repository.upload]
TEMP_PATH = /var/lib/gitea/data/tmp/uploads
[lfs]
PATH = /var/lib/gitea/data/lfs
[security]
INSTALL_LOCK = true
SECRET_KEY = ${GITEA_SECRET_KEY}
INTERNAL_TOKEN = ${GITEA_INTERNAL_TOKEN}
; Without these two, Gitea sees Caddy's address as the client for every request
; and fail2ban ends up banning the reverse proxy, locking everyone out.
REVERSE_PROXY_TRUSTED_PROXIES = ${TRUSTED_PROXIES}
REVERSE_PROXY_LIMIT = 1
PASSWORD_HASH_ALGO = argon2
[oauth2]
JWT_SECRET = ${GITEA_OAUTH2_JWT_SECRET}
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = ${REQUIRE_SIGNIN_VIEW}
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = false
DEFAULT_KEEP_EMAIL_PRIVATE = true
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
[session]
PROVIDER = file
PROVIDER_CONFIG = /var/lib/gitea/data/sessions
COOKIE_SECURE = true
[mailer]
ENABLED = false
[log]
; console -> journald -> Cloud Logging, and it is what fail2ban's systemd
; backend reads via CONTAINER_NAME=gitea. Do not switch to file logging without
; updating vm/fail2ban/jail.d/gitea.local.
MODE = console
LEVEL = info
ROOT_PATH = /var/lib/gitea/log
[actions]
; Enabled now so the eventual migration off GitHub Cloud Build triggers onto
; Gitea Actions does not need a config change + restart.
ENABLED = true
DEFAULT_ACTIONS_URL = github
[cron.update_checker]
ENABLED = false
[ui]
DEFAULT_THEME = gitea-auto