Skip to main content

Forgejo sicher mit PostgreSQL und Reverse Proxy betreiben

Einleitung

Dieser Artikel beschreibt die Bereitstellung eines überschaubaren Forgejo-Servers für private Git-Repositories. Forgejo und PostgreSQL laufen in getrennten Containern, HTTPS übernimmt ein bereits vorhandener Reverse Proxy. Damit bleibt der eigentliche Git-Server recht schlank und die Aufgaben sind sauber verteilt.

Die laufende Benutzer- und Projektverwaltung, Migrationen, Obsidian, Mailbetrieb, Updates und Wiederherstellung stehen im getrennten Artikel Forgejo im Alltag betreiben.

Alle Namen, Adressen und Kennwörter sind Beispiele. Die Adressen aus 192.0.2.0/24 und 198.51.100.0/24 sind reine Dokumentationsnetze und gehören nicht in eine produktive Konfiguration.

Verwendete Versionen

Stand: 7. August 2026

Komponente Verwendete Version
Betriebssystem Rocky Linux 10.2 (Red Quartz)
Linux-Kernel 6.12.0-211.43.1.el10_2.x86_64
Docker Engine 29.7.2
Docker Compose 5.4.0
Forgejo 16.0.1 (Stand 19.08.2026: 16.0.2)
PostgreSQL 17.10
QEMU Guest Agent 10.1.0-16.el10_2.2
SELinux Policy (targeted) 42.1.18-4.el10_2.2

Mit diesen Versionen wurde der Aufbau getestet. Bei einer neuen Installation kann man die aktuellen stabilen Patch-Releases innerhalb derselben Major-Version verwenden. Vor einem Sprung auf eine neue Major-Version lohnt sich ein Blick in die jeweiligen Upgrade-Hinweise.

Zielarchitektur

Komponente Beispiel
Forgejo-Domain git.example.com
Forgejo-Server 192.0.2.10
Reverse Proxy 192.0.2.20
interner HTTP-Port 3000/tcp
Git-over-SSH 2222/tcp, nur internes Netz
Hostadministration 22/tcp, nur Managementnetz
Datenbank PostgreSQL, nur internes Container-Netz
TLS am Reverse Proxy
Persistente Daten /srv/forgejo und Docker-Datenlaufwerk

PostgreSQL bleibt vollständig im Container-Netz und erhält keinen eigenen Host-Port. Forgejo bindet seine veröffentlichten Ports nur an die interne Serveradresse, also weder an 0.0.0.0 noch an eine öffentliche IPv6-Adresse.

Vorbereitungen und Rahmenparameter

Vor dem Start hilft es, ein paar Eckdaten festzulegen:

  1. endgültiger DNS-Name und interne Zieladresse
  2. Quelladresse des Reverse Proxys
  3. erlaubte Managementnetze für SSH
  4. Speicherbedarf für Repositories, LFS, Releases und Packages
  5. Backupziel, Aufbewahrung und gewünschte Wiederanlaufzeit
  6. SMTP-Relay für Benachrichtigungen und Passwortzurücksetzung
  7. Regeln für Registrierung, MFA und Sichtbarkeit
  8. möglicher Bedarf an Actions und einem getrennten Runner-System

Für eine kleine Installation sind zwei vCPUs und 2 GiB RAM ein guter Ausgangspunkt. Weniger RAM kann ebenfalls funktionieren, dann sollte man die Auslastung anfangs etwas genauer beobachten. Ein separates oder zumindest klar überwachtes Datenlaufwerk macht den späteren Betrieb deutlich angenehmer.

Betriebssystem vorbereiten

Zuerst wird das System aktualisiert und neu gestartet:

sudo dnf upgrade -y
sudo systemctl reboot

Danach kurz prüfen, ob SELinux und Firewalld laufen:

getenforce
sudo firewall-cmd --state

Die erwarteten Ausgaben sind Enforcing und running. Wenn SELinux später etwas blockiert, geben die Audit-Logs normalerweise einen guten Hinweis auf die Ursache. Eine gezielte Anpassung ist dann sauberer als SELinux für das gesamte System abzuschalten.

Docker-Daten und Logrotation

Containerdaten wachsen mit der Zeit. Ein eigenes Dateisystem oder zumindest ein klar definierter Pfad erleichtert deshalb Kapazitätsplanung, Überwachung und Wiederherstellung. Für Docker selbst kann eine passende /etc/docker/daemon.json so aussehen:

{
  "data-root": "/srv/docker",
  "live-restore": true,
  "log-driver": "local",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5"
  }
}

Nach dem Neustart von Docker lässt sich direkt kontrollieren, ob Datenpfad und Logtreiber übernommen wurden:

sudo systemctl restart docker
sudo docker info --format '{{.DockerRootDir}} {{.LoggingDriver}}'

Die persistenten Forgejo-Daten lassen sich zusätzlich auf einem eigenen LVM-LV ablegen. Damit bleiben Betriebssystem, Docker-Layer und eigentliche Git-Daten voneinander getrennt. Ein beispielhafter Aufbau mit XFS sieht so aus:

sudo pvcreate /dev/sdX1
sudo vgcreate vg_forgejo /dev/sdX1
sudo lvcreate -L 95G -n lv_data vg_forgejo
sudo mkfs.xfs -L forgejo-data /dev/vg_forgejo/lv_data

/dev/sdX1 ist dabei nur ein Platzhalter. Vor pvcreate wird das neue, leere Gerät immer anhand von Größe, Modell, Signaturen und bestehenden Mounts geprüft. Ein kleiner freier Bereich in der VG ist als kurzfristige Reserve praktisch.

Das Dateisystem wird anschließend per UUID unter /srv/forgejo eingetragen:

UUID=<XFS-UUID> /srv/forgejo xfs defaults,nodev,nosuid 0 0

Bei einer späteren Erweiterung kommt eine weitere Platte als PV zur bestehenden VG hinzu. lvextend -r kann LV und XFS-Dateisystem dann online vergrößern.

Verzeichnisstruktur und Secrets

Die persistenten Daten und Secrets liegen gesammelt unter /srv/forgejo:

sudo install -d -m 0750 /srv/forgejo
sudo install -d -m 0750 /srv/forgejo/data /srv/forgejo/config
sudo install -d -m 0700 /srv/forgejo/postgres
sudo install -d -m 0750 /srv/forgejo/secrets

Für PostgreSQL, Forgejo und interne Signaturen werden getrennte Zufallswerte verwendet. Statt sie direkt in die Compose-Datei zu schreiben, werden sie aus geschützten Dateien gelesen. Forgejo bietet dafür *_URI-Optionen, PostgreSQL die Variable POSTGRES_PASSWORD_FILE.

Beispielhafte Dateinamen:

/srv/forgejo/secrets/postgres_password
/srv/forgejo/secrets/forgejo_secret_key
/srv/forgejo/secrets/forgejo_internal_token

Die Dateien bekommen nur die Eigentümer- und Leserechte, die der jeweilige Container tatsächlich braucht. Dadurch landen die Werte weder in Git noch versehentlich in einer normalen Compose-Ausgabe.

Compose-Aufbau

Bei den Images werden feste Major-Versionen verwendet. Patch-Updates bleiben damit unkompliziert, während Major-Upgrades bewusst durchgeführt werden können. Das folgende Beispiel ist gekürzt, enthält aber die wichtigsten Grenzen zwischen Anwendung, Datenbank und Host:

name: forgejo

services:
  db:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: forgejo
      POSTGRES_USER: forgejo
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets: [postgres_password]
    volumes:
      - /srv/forgejo/postgres:/var/lib/postgresql/data:Z
    networks: [backend]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U forgejo -d forgejo"]
      interval: 10s
      timeout: 5s
      retries: 10
    security_opt: ["no-new-privileges:true"]

  forgejo:
    image: codeberg.org/forgejo/forgejo:16-rootless
    user: "1000:1000"
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      FORGEJO__database__DB_TYPE: postgres
      FORGEJO__database__HOST: db:5432
      FORGEJO__database__NAME: forgejo
      FORGEJO__database__USER: forgejo
      FORGEJO__database__PASSWD_URI: file:/run/secrets/postgres_password
      FORGEJO__server__ROOT_URL: https://git.example.com/
      FORGEJO__server__DOMAIN: git.example.com
      FORGEJO__server__HTTP_PORT: "3000"
      FORGEJO__server__START_SSH_SERVER: "true"
      FORGEJO__server__SSH_LISTEN_PORT: "2222"
      FORGEJO__server__SSH_PORT: "2222"
      FORGEJO__service__DISABLE_REGISTRATION: "true"
      FORGEJO__service__REQUIRE_SIGNIN_VIEW: "true"
      FORGEJO__security__GLOBAL_TWO_FACTOR_REQUIREMENT: all
      FORGEJO__repository__FORCE_PRIVATE: "true"
      FORGEJO__actions__ENABLED: "false"
    volumes:
      - /srv/forgejo/data:/var/lib/gitea:Z
      - /srv/forgejo/config:/etc/gitea:Z
    ports:
      - "192.0.2.10:3000:3000"
      - "192.0.2.10:2222:2222"
    networks: [backend]
    security_opt: ["no-new-privileges:true"]
    cap_drop: [ALL]

networks:
  backend:

secrets:
  postgres_password:
    file: /srv/forgejo/secrets/postgres_password

In der vollständigen Konfiguration können zusätzlich OpenID-Anmeldung, Federation, öffentliche API-Dokumentation und Actions abgeschaltet werden, wenn sie in der eigenen Umgebung nicht gebraucht werden. Für einen rein privaten Server passen außerdem private Benutzer- und Organisationsprofile als Standard.

Empfohlene Härtungsmaßnahmen

Die geschlossene Registrierung und private Repositories sind ein guter Anfang. Ein paar weitere Einstellungen begrenzen Funktionen, über die Forgejo selbst auf andere Systeme zugreift oder die Code auf dem Server ausführen könnten.

Der Reverse Proxy wird möglichst genau eingetragen. Statt allen privaten Netzen zu vertrauen, steht hier nur seine einzelne Adresse:

FORGEJO__security__REVERSE_PROXY_TRUSTED_PROXIES: 192.0.2.20/32
FORGEJO__security__REVERSE_PROXY_LIMIT: "1"
FORGEJO__session__COOKIE_SECURE: "true"

API-Tokens in URLs sind unnötig riskant, weil URLs häufig in Browser-, Proxy- oder Serverlogs auftauchen. Benutzerdefinierte Git-Hooks können Code mit den Rechten des Forgejo-Prozesses ausführen. Webhooks bleiben zunächst ebenfalls aus und werden erst mit einer konkreten Ziel-Allowlist aktiviert:

FORGEJO__security__DISABLE_QUERY_AUTH_TOKEN: "true"
FORGEJO__security__DISABLE_GIT_HOOKS: "true"
FORGEJO__security__DISABLE_WEBHOOKS: "true"

Wenn Projekte hauptsächlich von GitHub und Codeberg kommen, kann die Migrationsfunktion auf diese Ziele begrenzt werden. Private Netze bleiben für serverseitige Migrationen gesperrt:

FORGEJO__migrations__ALLOWED_DOMAINS: github.com,*.github.com,codeberg.org,*.codeberg.org
FORGEJO__migrations__ALLOW_LOCALNETWORKS: "false"

Für eine kleine private Instanz sind außerdem überschaubare Standardgrenzen praktisch. Das folgende Beispiel erlaubt 30 Repositories pro Benutzer oder Organisation, fünf Dateien pro Web-Upload mit jeweils 50 MiB und fünf Anhänge mit jeweils 250 MiB:

FORGEJO__repository__MAX_CREATION_LIMIT: "30"
FORGEJO__repository.upload__FILE_MAX_SIZE: "50"
FORGEJO__repository.upload__MAX_FILES: "5"
FORGEJO__attachment__MAX_SIZE: "250"
FORGEJO__attachment__MAX_FILES: "5"
FORGEJO__service__DEFAULT_KEEP_EMAIL_PRIVATE: "true"
FORGEJO__admin__DISABLE_REGULAR_ORG_CREATION: "true"

LFS, Packages und große Migrationen brauchen eine eigene Kapazitätsplanung. Eine lokale Prüfung des Datenlaufwerks kann ab 80 % Belegung eine Warnung ins Journal schreiben. Diese Meldung wird dann vom vorhandenen Monitoring abgeholt; der Prüfdienst selbst braucht dafür keinen Netzwerkzugriff.

Individualisierung – Logo und Favicons

Forgejo kann eigene statische Assets aus seinem persistenten Custom-Verzeichnis ausliefern. Dafür ist keine Änderung am Container-Image und normalerweise auch kein eigenes Template nötig. Das Logo bleibt dadurch bei einer Neuerstellung des Containers erhalten.

Als Ausgangspunkt eignet sich ein quadratisches PNG oder SVG mit transparentem Hintergrund. Ein nicht quadratisches Motiv wird am besten ohne Beschnitt mittig auf eine transparente quadratische Fläche gesetzt. Für eine vollständige kleine Ausstattung werden diese Dateien vorbereitet:

logo.svg
logo.png
favicon.svg
favicon.png
apple-touch-icon.png

Beim üblichen Containeraufbau landen sie auf dem Host beispielsweise hier:

/srv/forgejo/data/custom/public/assets/img/

Im Container entspricht das dem Pfad /var/lib/gitea/custom/public/assets/img/. Eigentümer und Gruppe werden an die UID/GID des Rootless-Forgejo-Containers angepasst; zum Lesen reichen für die Dateien Rechte 0644.

Bei PNG-Motiven sollte das Bild direkt in die SVG-Datei eingebettet werden. Ein relativer Verweis wie <image href="logo.png"> kann leer bleiben, weil Browser nachladbare Ressourcen innerhalb eines als Bild verwendeten SVGs einschränken. Unter Linux lässt sich eine eigenständige SVG-Datei beispielsweise so erzeugen:

logo_data="$(base64 -w0 logo.png)"
printf '%s\n' \
  '<?xml version="1.0" encoding="UTF-8"?>' \
  '<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">' \
  "  <image href=\"data:image/png;base64,${logo_data}\" width=\"512\" height=\"512\"/>" \
  '</svg>' > logo.svg

Für favicon.svg funktioniert dasselbe Verfahren mit favicon.png und den passenden Abmessungen. Anschließend werden die Dateien installiert und bei einem SELinux-System neu beschriftet:

sudo install -d -m 0755 /srv/forgejo/data/custom/public/assets/img
sudo install -m 0644 logo.svg logo.png favicon.svg favicon.png \
  apple-touch-icon.png /srv/forgejo/data/custom/public/assets/img/
sudo restorecon -RF /srv/forgejo/data/custom
sudo docker compose -f /srv/forgejo/compose.yaml restart forgejo

Die Auslieferung lässt sich ohne Anmeldung prüfen:

curl -I https://git.example.com/assets/img/logo.svg
curl -I https://git.example.com/assets/img/favicon.png

Erwartet werden HTTP 200 und der passende MIME-Typ. Forgejo cached statische Assets standardmäßig mehrere Stunden. Wenn nach dem Neustart noch das alte oder kurzzeitig kein Logo erscheint, hilft ein Hard-Reload beziehungsweise ein privates Browserfenster. Nach einem Forgejo-Major-Upgrade werden Navigation, Anmeldeseite und Favicons kurz kontrolliert, weil Overrides im Custom-Verzeichnis nicht zu den garantiert stabilen Schnittstellen gehören.

Reverse Proxy

Der Reverse Proxy erreicht Forgejo intern unter 192.0.2.10:3000. Auf diesem Weg wird bewusst HTTP verwendet, weil TLS bereits am Proxy endet. Folgende Header werden an Forgejo weitergegeben:

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
client_max_body_size 512M;

Zusätzlich werden WebSockets aktiviert. Die öffentliche Adresse im Proxy und ROOT_URL in Forgejo müssen genau zusammenpassen. Eine eigene Domain wie git.example.com ist dabei unkomplizierter als der Betrieb in einem Unterverzeichnis einer anderen Anwendung.

Firewalld

Port 3000 wird nur für die Adresse des Reverse Proxys geöffnet. Die beiden SSH-Ports bleiben auf das Management- beziehungsweise interne Netz begrenzt:

sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --permanent \
  --add-rich-rule='rule family="ipv4" source address="192.0.2.20/32" port port="3000" protocol="tcp" accept'
sudo firewall-cmd --permanent \
  --add-rich-rule='rule family="ipv4" source address="198.51.100.0/24" port port="22" protocol="tcp" accept'
sudo firewall-cmd --permanent \
  --add-rich-rule='rule family="ipv4" source address="198.51.100.0/24" port port="2222" protocol="tcp" accept'
sudo firewall-cmd --reload

Bei veröffentlichten Containerports gibt es noch eine Besonderheit: Docker verwaltet eigene NAT- und Forwarding-Regeln. Deshalb werden die Ports zusätzlich nur an die interne Hostadresse gebunden. Je nach Docker-Firewall-Backend kann außerdem eine Regel in DOCKER-USER nötig sein, damit die gewünschte Quellbeschränkung auch für den weitergeleiteten Containerverkehr greift. Vorhandene Docker-Regeln lässt man dabei unangetastet und ergänzt nur eine eigene, klar abgegrenzte Regelkette.

SSH-Härtung des Hosts

Bevor die Kennwortanmeldung abgeschaltet wird, sollte der Public-Key-Zugang in einer zweiten Sitzung erfolgreich getestet sein. So bleibt der Rückweg offen, falls sich in der Konfiguration ein Fehler eingeschlichen hat. Ein Drop-in unter /etc/ssh/sshd_config.d/ kann beispielsweise so aussehen:

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
X11Forwarding no
AllowUsers adminuser
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no

Vor dem Reload prüft sshd die Konfiguration auf Syntaxfehler:

sudo sshd -t && sudo systemctl reload sshd

Konten, Registrierung und MFA

Der erste Administrator wird über die Forgejo-CLI angelegt. Das zufällige Startkennwort wird außerhalb des Servers sicher abgelegt und beim ersten Login geändert.

Für eine geschlossene Instanz passen unter anderem diese Einstellungen:

[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true

[security]
INSTALL_LOCK = true
GLOBAL_TWO_FACTOR_REQUIREMENT = all
MIN_PASSWORD_LENGTH = 14
PASSWORD_HASH_ALGO = argon2

Neue Benutzer werden damit nur durch einen Administrator angelegt, und alle Konten richten MFA ein. Die zugehörigen Recovery-Codes passen am besten in den Passwortmanager oder eine andere geschützte Offline-Ablage.

Ohne SMTP sind Benachrichtigungen und die normale Passwortzurücksetzung eingeschränkt. Sobald mehrere Personen den Server verwenden, ist ein authentifiziertes SMTP-Relay daher eine sinnvolle Ergänzung. Nach der Einrichtung reicht eine Testnachricht, um Absender, Zustellung und mögliche Spamfilter einmal zu prüfen.

Actions und Runner

Forgejo Actions führen Code aus Repositories aus und brauchen deshalb etwas mehr Abgrenzung als die eigentliche Forgejo-Anwendung. Für CI/CD ist eine separate, ersetzbare Runner-VM mit wenigen Netzrechten ein guter Aufbau. Der Runner bekommt dabei keinen Zugriff auf den Docker-Socket der Forgejo-VM. Wenn Actions zunächst nicht gebraucht werden, können sie einfach deaktiviert bleiben.

Backup und Wiederherstellung

Zum vollständigen Backup gehören:

  • Forgejo-Daten und Konfiguration
  • kryptografische Schlüssel
  • Git-Repositories, LFS, Releases und Packages
  • PostgreSQL-Datenbank
  • Compose- und Betriebsdateien

Bei einer kleinen Installation mit lokalem Datenlaufwerk sind konsistente VM-Sicherungen eine praktische Grundlage. Zusätzliche PostgreSQL-Dumps erleichtern einzelne Wiederherstellungsszenarien. Entscheidend ist weniger die Anzahl der Sicherungen als ein gelegentlicher Test, ob sich daraus wirklich wieder ein lauffähiger Server herstellen lässt.

Vor einem Forgejo-Major-Upgrade bietet sich ein frisches, konsistentes Backup an. Nach dem Upgrade werden Anmeldung, Clone, Push, LFS, Datenbank und Logs geprüft. forgejo doctor check --all ergänzt diesen Funktionstest um die internen Forgejo-Prüfungen.

Abschlussprüfung

Ein paar technische Prüfungen liefern einen schnellen Überblick:

sudo docker compose ps
curl -fsS http://192.0.2.10:3000/api/healthz
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|authenticationmethods'
getenforce
sudo firewall-cmd --list-all
sudo ausearch -m AVC -ts recent

Danach folgen die praktischen Tests:

  1. als Administrator anmelden und MFA einrichten
  2. prüfen, dass keine öffentliche Registrierung angeboten wird
  3. den anonymen Zugriff auf private Inhalte testen
  4. ein Repository anlegen, über HTTPS und SSH klonen und Änderungen pushen
  5. den Server neu starten und die Healthchecks erneut kontrollieren
  6. ein Backup in einer isolierten Umgebung testweise wiederherstellen

Wenn diese Punkte funktionieren, ist nicht nur die Installation fertig, sondern auch der grundlegende Betriebsweg einmal durchgespielt.

Weiterführender Betrieb

Für Benutzerverwaltung, Projektorganisation, Migrationen von GitHub oder Codeberg, Obsidian, Mail, Updates und Wiederherstellung geht es hier weiter: Forgejo im Alltag betreiben.

Zuletzt aktualisiert am 19.08.2026 · 23 Aufrufe