Files
stack/infra/gitea
kert daa45d99ce chore(infra): kasmvnc TURN_EXTERNAL_IP=auto + selkies upstream image + gitea avatar
- compose.yml: pin TURN_EXTERNAL_IP=auto on the zotero kasmvnc
  service. The image's entrypoint probes the public IP via
  `dig @ns1.google.com TXT o-o.myaddr.l.google.com`; on networks
  that return an edns0-client-subnet hint instead of a bare IP,
  the response lands in network.udp.public_ip and kasmvnc rejects
  it as invalid, FATAL-restart-looping the UI. "auto" lets
  kasmvnc figure it out itself and skips the brittle probe.

- infra/images/selkies/Dockerfile.upstream: track the upstream
  Dockerfile as a reference for the Zotero image build.

- infra/gitea/custom/public/assets/img/avatar_default.png: drop
  in a default avatar for self-hosted gitea.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:01:18 -04:00
..

Fhirworx Gitea image

This directory builds fhirworx/gitea:<tag> — upstream go-gitea/gitea at a pinned tag with the fhirworx theme baked in via bindata. No source changes to upstream; we only inject one CSS file and two brand SVGs into the upstream tree before its own build runs.

Layout

infra/gitea/
├── Dockerfile          # multi-stage build, see below
├── .dockerignore
├── theme/
│   └── theme-fhirworx.css   # ONE css file = the entire theme
├── brand/
│   ├── logo.svg        # → assets/logo.svg → make generate-images
│   └── favicon.svg     # → assets/favicon.svg
├── custom/             # bind-mounted at runtime as /var/lib/gitea/custom
│   └── templates/
│       └── home.tmpl   # custom anonymous landing page
└── README.md           # this file

Staying downstream from upstream Gitea

Goal: track upstream cleanly, never fork the source.

The build does only three things to the upstream tree:

  1. COPY theme/theme-fhirworx.css web_src/css/themes/theme-fhirworx.css
  2. COPY brand/logo.svg assets/logo.svg
  3. COPY brand/favicon.svg assets/favicon.svg

After these, the upstream make clean-all build runs unmodified — webpack processes our theme into public/assets/css/theme-fhirworx.css, the image generator regenerates every PNG/SVG variant from our SVGs, and bindata embeds the whole public/ tree into the Go binary.

Bumping Gitea

# 1. Update the pinned tag
sed -i 's/GITEA_VERSION=v1\.[0-9.]\+/GITEA_VERSION=v1.NEW.VER/' Dockerfile
# 2. Diff our theme against upstream's reference
diff theme/theme-fhirworx.css \
     <(curl -sL https://raw.githubusercontent.com/go-gitea/gitea/v1.NEW.VER/web_src/css/themes/theme-gitea-light.css)
# 3. Add any new --color-* vars upstream introduced
# 4. Rebuild
docker compose build gitea && docker compose up -d gitea

The theme file's variable order mirrors upstream's theme-gitea-light.css 1:1, on purpose, so step 2 produces a clean readable diff. Add variables upstream added; refresh values you've remapped.

Theme contract

The theme is a complete drop-in replacement for upstream's theme-gitea-light.css. It defines:

  • All ~140 --color-* variables Gitea references. Any var left undefined resolves to CSS initial (transparent bg / black text), which breaks surfaces like the navbar, secondary-nav, footer, clone panel, menu hover states. Historical bug: an early version defined ~30 vars and many components broke.

  • --fonts-override — Gitea's base.css composes --fonts-regular: var(--fonts-override, var(--fonts-proportional)), .... Setting --fonts-override propagates the editorial type (Source Serif 4) through every Fomantic UI component (menus, buttons, tabs, inputs).

  • A defensive #navbar color sweep. Fhirworx is the only design (compared against awesome-gitea's full theme list — Catppuccin, Rainnny GitHub, lutinglt, Earl Grey, Dark Arc, etc.) that puts a dark navbar over a light body. Every reference theme keeps both surfaces in the same luminance class. Because of that, Gitea's base CSS doesn't anticipate the inversion: any Fomantic class with color: var(--color-text) (e.g. .ui.button for the hamburger #navbar-expand-toggle) leaks near-black text into the dark navbar. The sweep at the end of theme-fhirworx.css forces nav-text on every text/icon element inside #navbar, and flips dropdown popouts back to the light body palette since they float over the page, not the bar.

Custom templates (still bind-mounted)

custom/templates/home.tmpl is the anonymous landing page. It overrides upstream's stock dashboard for unauthenticated visitors and renders the homelab service grid. It's bind-mounted via compose.yml, not baked in, because it's content not theme — easier to edit without rebuilding.

Compose wiring

gitea:
  build:
    context: ./infra/gitea
    args:
      GITEA_VERSION: v1.25.4
  image: fhirworx/gitea:v1.25.4
  environment:
    - GITEA__ui__THEMES=fhirworx
    - GITEA__ui__DEFAULT_THEME=fhirworx
  volumes:
    - gitea_data:/var/lib/gitea
    - gitea_config:/etc/gitea
    - ./infra/gitea/custom:/var/lib/gitea/custom

THEMES=fhirworx (single option) — no gitea-auto/light/dark/protanopia variants are exposed in the user appearance dropdown. fhirworx is the only choice and it's the default. Per-user theme column in the postgres "user" table should be set to 'fhirworx'.

What goes wrong if you bypass the build

Earlier iterations bind-mounted raw CSS files into stock gitea/gitea:1.25.4-rootless. Two persistent failures:

  • Inode drift: Docker's single-file bind mount tracks by inode. The Edit/Write tools rewrite atomically, replacing the inode. The container keeps pointing at the now-orphaned old inode and sees nothing change. Required docker restart after every edit.

  • Cache + version-pinning: The asset URL embeds Gitea's version (?v=1.25.4). Browsers cache aggressively for 6h. Edits to the CSS file don't change the URL → cache hit serves stale CSS forever (or until the user knows to hard-refresh). On Cloudflare it's even longer.

Baking the theme into bindata sidesteps both. The CSS only changes when you rebuild the image, which means the version string actually changes in the binary's metadata, and there's no inode tracking to drift.