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>
97 lines
3.6 KiB
Docker
97 lines
3.6 KiB
Docker
# syntax=docker/dockerfile:1
|
|
#
|
|
# Gitea on a Debian 13 (trixie) base.
|
|
#
|
|
# Deliberately NOT the upstream image: that one is Alpine-based and its
|
|
# GITEA__section__KEY environment support comes from an `environment-to-ini`
|
|
# entrypoint helper, not from the gitea binary. We template app.ini on the host
|
|
# and bind-mount it read-only instead, so no such helper is needed here.
|
|
|
|
ARG DEBIAN_TAG=13-slim
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 1: fetch and verify the release binary.
|
|
# The checksum alone proves nothing (it is served from the same place as the
|
|
# binary); the detached signature is the actual integrity guarantee.
|
|
# ---------------------------------------------------------------------------
|
|
FROM debian:${DEBIAN_TAG} AS fetch
|
|
|
|
ARG GITEA_VERSION
|
|
ARG GITEA_GPG_KEY=7C9E68152594688862D62AF62D9AE806EC1592E2
|
|
ARG TARGETARCH=amd64
|
|
|
|
RUN set -eux; \
|
|
apt-get update; \
|
|
apt-get install -y --no-install-recommends \
|
|
ca-certificates curl gnupg xz-utils; \
|
|
rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /tmp/gitea
|
|
|
|
RUN set -eux; \
|
|
test -n "${GITEA_VERSION}" || { echo "GITEA_VERSION build-arg is required" >&2; exit 1; }; \
|
|
base="https://github.com/go-gitea/gitea/releases/download/v${GITEA_VERSION}"; \
|
|
file="gitea-${GITEA_VERSION}-linux-${TARGETARCH}.xz"; \
|
|
curl -fsSL -o "${file}" "${base}/${file}"; \
|
|
curl -fsSL -o "${file}.sha256" "${base}/${file}.sha256"; \
|
|
curl -fsSL -o "${file}.asc" "${base}/${file}.asc"; \
|
|
sha256sum -c "${file}.sha256"; \
|
|
export GNUPGHOME="$(mktemp -d)"; \
|
|
for ks in keys.openpgp.org keyserver.ubuntu.com pgp.mit.edu; do \
|
|
gpg --batch --keyserver "hkps://${ks}" --recv-keys "${GITEA_GPG_KEY}" && break; \
|
|
done; \
|
|
gpg --batch --verify "${file}.asc" "${file}"; \
|
|
gpgconf --kill all; \
|
|
rm -rf "${GNUPGHOME}"; \
|
|
xz -d "${file}"; \
|
|
mv "gitea-${GITEA_VERSION}-linux-${TARGETARCH}" /tmp/gitea/gitea; \
|
|
chmod 0755 /tmp/gitea/gitea; \
|
|
/tmp/gitea/gitea --version
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 2: runtime.
|
|
# ---------------------------------------------------------------------------
|
|
FROM debian:${DEBIAN_TAG}
|
|
|
|
ARG GITEA_VERSION
|
|
|
|
LABEL org.opencontainers.image.title="gitea" \
|
|
org.opencontainers.image.description="Gitea on a Debian 13 base" \
|
|
org.opencontainers.image.version="${GITEA_VERSION}" \
|
|
org.opencontainers.image.source="https://github.com/go-gitea/gitea" \
|
|
org.opencontainers.image.base.name="docker.io/library/debian:13-slim"
|
|
|
|
RUN set -eux; \
|
|
apt-get update; \
|
|
apt-get install -y --no-install-recommends \
|
|
ca-certificates \
|
|
curl \
|
|
git \
|
|
git-lfs \
|
|
openssh-client \
|
|
tzdata; \
|
|
rm -rf /var/lib/apt/lists/*; \
|
|
groupadd --gid 1000 git; \
|
|
useradd --uid 1000 --gid 1000 --home-dir /var/lib/gitea --shell /bin/bash git; \
|
|
mkdir -p /var/lib/gitea /etc/gitea; \
|
|
chown -R 1000:1000 /var/lib/gitea
|
|
|
|
COPY --from=fetch --chown=root:root --chmod=0755 /tmp/gitea/gitea /usr/local/bin/gitea
|
|
|
|
ENV GITEA_WORK_DIR=/var/lib/gitea \
|
|
GITEA_CUSTOM=/var/lib/gitea/custom
|
|
|
|
USER 1000:1000
|
|
WORKDIR /var/lib/gitea
|
|
|
|
# HTTP (behind Caddy) and the built-in SSH server.
|
|
EXPOSE 3000 2222
|
|
|
|
# Informational only -- the quadlet declares the authoritative healthcheck,
|
|
# because `Notify=healthy` needs it defined there to gate unit startup.
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
|
|
CMD curl -fsS http://127.0.0.1:3000/api/healthz || exit 1
|
|
|
|
ENTRYPOINT ["/usr/local/bin/gitea"]
|
|
CMD ["web", "--config", "/etc/gitea/app.ini"]
|