diff --git a/stacks/forgejo/.env.example b/stacks/forgejo/.env.example new file mode 100644 index 0000000..80cbf91 --- /dev/null +++ b/stacks/forgejo/.env.example @@ -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= + +# Forgejo internal crypto +FORGEJO_SECRET_KEY= +FORGEJO_INTERNAL_TOKEN= + +# SMTP via Mailcow +SMTP_USER=electric-horses@sdda.eu +SMTP_PASSWORD= + +# OIDC (Authentik) — applied via CLI after first boot, reference only here +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= +OIDC_DISCOVERY_URL=https://welcome.sdda.eu/application/o/forgejo/.well-known/openid-configuration diff --git a/stacks/forgejo/Agent.md b/stacks/forgejo/Agent.md new file mode 100644 index 0000000..8871b46 --- /dev/null +++ b/stacks/forgejo/Agent.md @@ -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//.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-.sql.gz` + `forgejo-data-.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) diff --git a/stacks/forgejo/README.md b/stacks/forgejo/README.md new file mode 100644 index 0000000..25b6f81 --- /dev/null +++ b/stacks/forgejo/README.md @@ -0,0 +1,63 @@ +# Forgejo — Self-Hosted Git + +**Live:** https://code.sdda.eu +**Git SSH:** `ssh://git@code.sdda.eu:222//.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)|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 "" \ + --admin --must-change-password=false + +# OIDC einrichten +docker exec -u git forgejo forgejo admin auth add-oauth \ + --name authentik \ + --provider openidConnect \ + --key "" \ + --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) diff --git a/stacks/forgejo/backup.sh b/stacks/forgejo/backup.sh new file mode 100755 index 0000000..f6d4d99 --- /dev/null +++ b/stacks/forgejo/backup.sh @@ -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" diff --git a/stacks/forgejo/docker-compose.yml b/stacks/forgejo/docker-compose.yml new file mode 100644 index 0000000..02880ce --- /dev/null +++ b/stacks/forgejo/docker-compose.yml @@ -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__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