feat(stacks/forgejo): add self-hosted Git stack

First stack mirrored 1:1 from /opt/ai-apps/forgejo/ on the server.
Includes docker-compose.yml (forgejo + postgres 16), .env.example
template (NO real secrets), backup.sh (nightly pg_dump + tar), plus
Agent.md and README.md.

Known gotchas documented in Agent.md:
- Volume mount on /data not /var/lib/gitea
- SSH port 2222 in container (system sshd occupies 22)
- OIDC config lives in DB table login_source, not app.ini

Refs OP#1119
This commit is contained in:
Benjamin Weinlich 2026-04-11 22:19:25 +02:00
parent 723ee00388
commit 8ba375caaa
5 changed files with 277 additions and 0 deletions

View file

@ -0,0 +1,18 @@
# Forgejo Stack — environment template
# Copy to .env and fill in actual values. NEVER commit .env to Git.
# Postgres password (generate: openssl rand -hex 32)
DB_PASSWORD=<openssl rand -hex 32>
# Forgejo internal crypto
FORGEJO_SECRET_KEY=<openssl rand -hex 32>
FORGEJO_INTERNAL_TOKEN=<openssl rand -hex 64>
# SMTP via Mailcow
SMTP_USER=electric-horses@sdda.eu
SMTP_PASSWORD=<aus Mailcow>
# OIDC (Authentik) — applied via CLI after first boot, reference only here
OIDC_CLIENT_ID=<from Authentik Application 'Forgejo'>
OIDC_CLIENT_SECRET=<from Authentik Application 'Forgejo'>
OIDC_DISCOVERY_URL=https://welcome.sdda.eu/application/o/forgejo/.well-known/openid-configuration

64
stacks/forgejo/Agent.md Normal file
View file

@ -0,0 +1,64 @@
# Agent Briefing — Forgejo Stack
Du arbeitest am Forgejo-Git-Hosting. Lies erst `../../Agent.md` am Repo-Root für globale Konventionen.
## Live auf
- **URL:** https://code.sdda.eu
- **SSH:** `ssh://git@code.sdda.eu:222/<user>/<repo>.git`
- **Server:** ai-apps (Hetzner cx22, 10.0.0.8)
- **Pfad auf Server:** `/opt/ai-apps/forgejo/`
- **Live seit:** 2026-04-11
- **Version:** Forgejo 10 (`codeberg.org/forgejo/forgejo:10`)
## Authentifizierung
- Primär: **Nativer OIDC via Authentik** (nicht ForwardAuth)
- Application in Authentik: `forgejo` auf `welcome.sdda.eu`
- Zugangskontrolle: Gruppen-Policy `forgejo-users` erforderlich
- Launch-URL: `https://code.sdda.eu/user/oauth2/authentik` (für Silent SSO aus Authentik-Dashboard)
- Fallback: lokaler `admin-local` User mit `prohibit_login=true` (Emergency)
- Siehe ADR-0003 (OIDC statt ForwardAuth), ADR-0006 (Silent-SSO-Launch-URL)
## Kritische Gotchas
1. **Volume-Mount auf `/data`**, NICHT `/var/lib/gitea`. Forgejo schreibt alles nach `/data`. Siehe ADR-0005.
2. **SSH-Port-Kollision:** Forgejo-Image hat system-sshd auf 22, deshalb Forgejos eigener Server auf Container-Port 2222 → Host-Port 222.
3. **OIDC-Config lebt in der Postgres-DB** (Tabelle `login_source`), NICHT in `app.ini`. Zum Ändern: `docker exec -u git forgejo sh -c 'cd / && forgejo admin auth update-oauth --id 1 ...'`
4. **`forgejo admin` CLI** braucht `-u git` und `cd /`: `docker exec -u git forgejo sh -c 'cd / && forgejo admin user list'`
5. **User-Promotion zum Admin** via SQL am saubersten: `UPDATE "user" SET is_admin = true WHERE lower_name = 'NAME';`
## Ops-Kommandos
```bash
ssh ai-apps
cd /opt/ai-apps/forgejo
# Status
docker compose ps
docker logs forgejo --tail 50
# Restart
docker compose restart forgejo
# Update (Major-Version manuell, Tag auf :10 gepinnt)
docker compose pull
docker compose up -d
# Backup manuell
bash backup.sh
# User-Verwaltung
docker exec -u git forgejo sh -c 'cd / && forgejo admin user list'
docker exec forgejo-db psql -U forgejo -d forgejo \
-c "UPDATE \"user\" SET is_admin = true WHERE lower_name = 'NAME';"
```
## Backup
- Script: `backup.sh` in diesem Ordner (spiegelt `/opt/ai-apps/forgejo/backup.sh`)
- Cron: `0 3 * * *` auf ai-apps → `/opt/backups/forgejo/`
- Retention: 14 Tage
- Format: `forgejo-db-<ts>.sql.gz` + `forgejo-data-<ts>.tar.gz`
- Offsite: noch nicht (Tier 2 via rclone → Nextcloud geplant M7.5)
- Restore-Prozedur: siehe `docs/runbooks/forgejo-backup-restore.md` (wird in M7.3 hinzugefügt)
## Related
- `../../Agent.md` — Repo-weites Briefing
- `../../docs/architecture/ai-apps-stacks.md` — Server-Kontext
- ADRs 0001-0006 (in iCloud-Ordner, Spiegelung hier in M7.3)

63
stacks/forgejo/README.md Normal file
View file

@ -0,0 +1,63 @@
# Forgejo — Self-Hosted Git
**Live:** https://code.sdda.eu
**Git SSH:** `ssh://git@code.sdda.eu:222/<user>/<repo>.git`
## Was ist das?
Unser selbst-gehostetes Git-Hosting, basierend auf [Forgejo](https://forgejo.org/) — einem non-profit Community-Fork von Gitea, gesteuert von Codeberg e.V. Hier leben Code, Infrastructure-as-Code (wie dieses Repo!), Dokumentation und alle künftigen internen Tools.
## Schnelles Setup auf neuem Host
```bash
# Vorausgesetzt: Traefik mit Let's Encrypt läuft bereits, DNS ist gesetzt,
# Authentik OIDC-Provider ist konfiguriert (Client-ID + Secret bereit)
cp .env.example .env
# Secrets generieren
sed -i '' -e "s|<openssl rand -hex 32>|$(openssl rand -hex 32)|g" .env # macOS
# ODER: sed -i -e "s|...|...|g" .env auf Linux
# SMTP + OIDC Credentials manuell eintragen
docker compose up -d
sleep 30
# Admin-Fallback anlegen
docker exec -u git forgejo forgejo admin user create \
--username admin-local \
--email admin-local@sdda.eu \
--password "<strong-password>" \
--admin --must-change-password=false
# OIDC einrichten
docker exec -u git forgejo forgejo admin auth add-oauth \
--name authentik \
--provider openidConnect \
--key "<CLIENT_ID>" \
--secret "<CLIENT_SECRET>" \
--auto-discover-url "https://welcome.sdda.eu/application/o/forgejo/.well-known/openid-configuration" \
--scopes "openid profile email" \
--skip-local-2fa
# admin-local sperren (Notfall-Reserve)
docker exec forgejo-db psql -U forgejo -d forgejo \
-c "UPDATE \"user\" SET prohibit_login = true WHERE lower_name = 'admin-local';"
```
## Zugang
- **Normal (empfohlen):** Via Authentik OIDC. `code.sdda.eu/user/login` → "Anmelden mit authentik" — oder direkt aus dem Authentik-Dashboard (Silent SSO).
- **Voraussetzung:** Mitglied der Authentik-Gruppe `forgejo-users`
- **Emergency-Fallback:** `admin-local` User (in Runbook, nicht hier)
## Files in diesem Ordner
- `docker-compose.yml` — Stack-Definition
- `.env.example` — Template, **niemals echte `.env` committen**
- `backup.sh` — Nightly-Backup-Script (pg_dump + tar)
- `Agent.md` — AI-Briefing für Sessions an diesem Stack
- `README.md` — diese Datei
## Live vs. Repo
Dieses Repo ist die **versionierte Wahrheit** des Stacks. Wenn du auf ai-apps eine Änderung machst (z.B. `docker-compose.yml` anpasst), **bitte auch hier einchecken**. Andernfalls driftet das Repo vom Server weg und verliert seinen Wert.
## OpenProject
- M7.1 — Forgejo Deployment mit Authentik SSO (abgeschlossen)
- M7.2 — Repo initialisiert (dieser Commit)

29
stacks/forgejo/backup.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
# Forgejo Backup — nightly cron
# Runs pg_dump + tar of /data, retention 14 days.
# Schedule: 0 3 * * * /opt/ai-apps/forgejo/backup.sh >> /opt/ai-apps/forgejo/backup.log 2>&1
set -euo pipefail
TS=$(date +%Y%m%d-%H%M%S)
DEST=/opt/backups/forgejo
mkdir -p "$DEST"
echo "[$(date -Iseconds)] backup start"
# Postgres dump
docker exec forgejo-db pg_dump -U forgejo forgejo | gzip > "$DEST/forgejo-db-$TS.sql.gz"
echo " db dump: $(ls -lh $DEST/forgejo-db-$TS.sql.gz | awk '{print $5}')"
# Data volume tar
docker run --rm \
-v forgejo_forgejo-data:/data:ro \
-v "$DEST":/backup \
alpine \
tar -czf "/backup/forgejo-data-$TS.tar.gz" -C /data .
echo " data tar: $(ls -lh $DEST/forgejo-data-$TS.tar.gz | awk '{print $5}')"
# Retention: 14 days
find "$DEST" -type f -mtime +14 -delete
echo "[$(date -Iseconds)] backup complete"

View file

@ -0,0 +1,103 @@
# Forgejo Stack — self-hosted Git hosting with Authentik SSO
# Part of M7.1 (Operations & Documentation Foundation)
# Network: traefik-public (public via Traefik) + forgejo-internal (service ↔ DB)
services:
forgejo:
image: codeberg.org/forgejo/forgejo:10
container_name: forgejo
restart: unless-stopped
env_file: .env
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: forgejo-db:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__database__PASSWD: ${DB_PASSWORD}
FORGEJO__server__DOMAIN: code.sdda.eu
FORGEJO__server__ROOT_URL: https://code.sdda.eu/
FORGEJO__server__SSH_DOMAIN: code.sdda.eu
FORGEJO__server__SSH_PORT: "222"
FORGEJO__server__START_SSH_SERVER: "true"
FORGEJO__server__SSH_LISTEN_PORT: "2222"
FORGEJO__server__HTTP_PORT: "3000"
FORGEJO__server__LFS_START_SERVER: "true"
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__security__SECRET_KEY: ${FORGEJO_SECRET_KEY}
FORGEJO__security__INTERNAL_TOKEN: ${FORGEJO_INTERNAL_TOKEN}
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "true"
FORGEJO__service__SHOW_REGISTRATION_BUTTON: "false"
FORGEJO__service__ENABLE_NOTIFY_MAIL: "true"
FORGEJO__service__DEFAULT_KEEP_EMAIL_PRIVATE: "true"
FORGEJO__openid__ENABLE_OPENID_SIGNIN: "false"
FORGEJO__openid__ENABLE_OPENID_SIGNUP: "false"
FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION: "true"
FORGEJO__oauth2_client__USERNAME: email
FORGEJO__oauth2_client__UPDATE_AVATAR: "true"
FORGEJO__oauth2_client__ACCOUNT_LINKING: "auto"
FORGEJO__mailer__ENABLED: "true"
FORGEJO__mailer__PROTOCOL: smtp
FORGEJO__mailer__SMTP_ADDR: 10.0.0.2
FORGEJO__mailer__SMTP_PORT: "587"
FORGEJO__mailer__FROM: "Forgejo <forgejo@sdda.eu>"
FORGEJO__mailer__USER: ${SMTP_USER}
FORGEJO__mailer__PASSWD: ${SMTP_PASSWORD}
FORGEJO__log__LEVEL: Info
volumes:
- forgejo-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
networks:
- traefik-public
- forgejo-internal
ports:
- "222:2222"
depends_on:
forgejo-db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:3000/api/healthz"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-public"
- "traefik.http.services.forgejo.loadbalancer.server.port=3000"
- "traefik.http.routers.forgejo.rule=Host(`code.sdda.eu`)"
- "traefik.http.routers.forgejo.entrypoints=websecure"
- "traefik.http.routers.forgejo.tls=true"
- "traefik.http.routers.forgejo.tls.certresolver=letsencrypt"
- "traefik.http.routers.forgejo.service=forgejo"
forgejo-db:
image: postgres:16-alpine
container_name: forgejo-db
restart: unless-stopped
environment:
POSTGRES_DB: forgejo
POSTGRES_USER: forgejo
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- forgejo-db-data:/var/lib/postgresql/data
networks:
- forgejo-internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U forgejo -d forgejo"]
interval: 10s
timeout: 5s
retries: 5
volumes:
forgejo-data:
forgejo-db-data:
networks:
traefik-public:
external: true
forgejo-internal:
driver: bridge