GitHub-Backup mit Gickup und Forgejo

In meinem über Jahre gewachsenen GitHub-Konto codebude steckt ein ziemlich großer Teil meiner Arbeit. Rund 70 Repositories sind es inzwischen, öffentliche und private, darunter auch Forks. Du kennst das: Ein gehostetes Konto ist bequem, aber die Kopie liegt trotzdem bei jemand anderem.

Mein Ziel ist deshalb überschaubar und konkret: Alle Repositories sollen einmal täglich automatisch auf meine eigene, selbst gehostete Forgejo-Instanz gespiegelt werden. Nicht als händischer Export, den ich irgendwann vergesse, sondern als Routine, die auch dann läuft, wenn ich gerade andere Dinge im Kopf habe.

Damit liegt eine vollständige Kopie in meiner Hand, aus der ich jederzeit klonen kann, unabhängig davon, was mit meinem GitHub-Konto oder dem Dienst passiert. Das ist kein Schutz vor jedem denkbaren Datenverlust, aber es entfernt eine wichtige Abhängigkeit.

Im ersten Abschnitt kläre ich, warum ich diesen Umweg überhaupt gehe. Danach bauen wir den Ablauf Schritt für Schritt mit Gickup, Docker und Forgejo auf.

Warum ein eigenes Backup für GitHub

GitHub ist ein Dienst, kein Backup-System für deine persönliche Infrastruktur. GitHub selbst beschreibt, wie du ein einzelnes Repository sicherst, und Git bringt mit Klonen und allen Refs schon viel mit. Was dabei fehlt, ist eine täglich aktualisierte Kopie aller Repositories an einem Ort, den du selbst kontrollierst.

Ein einzelner Klon pro Projekt wäre machbar, aber bei rund 70 Repositories wird daraus schnell eine Liste von Sonderfällen. Dazu kommen Forks, private Repositories und Wikis. Ich wollte weniger Pflege, nicht mehr davon.

Warum Gickup und ein eigenes Forgejo

Gickup liest die Repositories über die GitHub-Schnittstelle und schiebt alle Refs in das Ziel. Forgejo ist dafür die eigene Ablage: eine Weboberfläche und eine Git-Instanz, aus der ich jederzeit klonen kann. Die Alternative wäre ein selbst geschriebenes Skript mit API-Paginierung, Clone-Schleifen und Fehlerbehandlung gewesen. Das klingt zunächst nach einem netten Wochenendprojekt und endet erfahrungsgemäß mit einem zweiten Wochenendprojekt.

Vergleich von GitHub-Konto und Forgejo-Kopie mit etwa 70 Repositories
GitHub-Konto und Forgejo-Kopie im Vergleich

Wichtig ist die Einschränkung: Gickup überträgt Git-Daten und auf Wunsch Wikis. Issues, Pull Requests, Releases und Actions sind Metadaten bei GitHub und kommen in dieser Kopie nicht mit. Wer ein vollständiges Archiv dieser Dinge braucht, muss dafür einen eigenen Export ergänzen.

Die Repositories auf der Forgejo-Seite sind normale Repositories, keine Pull-Mirrors. Forgejo holt nichts nach, es gibt keinen Rückweg. Gickup ist der einzige Antrieb. Nachlesen lässt sich das in der Forgejo-Projektseite und in der Dokumentation zu Repository-Mirrors; in diesem Aufbau kommt die Mirror-Funktion aber nicht zum Einsatz.

Voraussetzungen

Du brauchst ein GitHub-Token mit Zugriff auf die gewünschten Repositories, eine eigene Forgejo-Instanz mit einem Benutzer, Docker und einen Speicherort für die Git-Daten. Der Name der Forgejo-Instanz muss außerdem aus dem Container heraus auflösbar sein.

Das Zielverzeichnis liegt bei mir auf einer Netzfreigabe. Genau dort war der entscheidende Stolperstein, den ich im Artikel Wenn Docker schneller als das NAS ist bereits beschrieben habe: Die Instanz darf nicht starten, bevor die Freigabe eingebunden ist.

Der Aufbau, Schritt für Schritt

GitHub-Token anlegen

Lege ein Token für den Zugriff auf deine Repositories an. Die GitHub-Dokumentation zu Personal Access Tokens beschreibt die aktuelle Auswahl. Verwende den kleinstmöglichen Umfang, der für deine privaten und öffentlichen Repositories genügt, und trage den Wert später nur über eine Umgebungsvariable ein.

Forgejo-Token und der nötige Rechteumfang

Erzeuge in Forgejo ein Token für den Zielbenutzer. Der Rechteumfang muss write:user enthalten, weil Gickup fehlende Repositories selbst anlegt. Ein zu knappes Token lässt sich nachträglich nicht erweitern, also musst du es neu erzeugen und in der Konfiguration ersetzen.

Zielverzeichnis für die Git-Daten

Lege das Verzeichnis auf dem Speicher an, der deine Kopie aufnehmen soll. Prüfe vor dem Start, dass die Forgejo-Instanz genau dieses Verzeichnis sieht. Wenn sie leer startet, sieht Gickup beim Push auf jedes Repository denselben Fehler, obwohl das Werkzeug selbst korrekt arbeitet.

Compose-Datei

Die Compose-Datei startet Gickup mit einer festen Zeitzone und bindet die Konfiguration ein. Die Tokens stehen nicht in dieser Datei, sondern kommen aus Umgebungsvariablen.

services:
  gickup:
    image: buddyspencer/gickup
    restart: unless-stopped
    env_file:
      - .env
    environment:
      TZ: Europe/Berlin
      GITHUB_TOKEN: ${GITHUB_TOKEN}
      FORGEJO_TOKEN: ${FORGEJO_TOKEN}
    entrypoint: ["/usr/local/bin/start-gickup.sh"]
    volumes:
      - ./start-gickup.sh:/usr/local/bin/start-gickup.sh:ro
      - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro # für selbst ausgestellte Zertifikate
    networks:
      - forgejo

networks:
  forgejo:
    external: true
#!/bin/sh
set -eu
cat > /tmp/gickup.yml <<EOF
cron: "0 5 * * *"
source:
  github:
    - token: ${GITHUB_TOKEN}
      wiki: true
destination:
  gitea:
    - url: https://git.example.com
      token: ${FORGEJO_TOKEN}
      user: backup
      createorg: false
      lfs: true
      force: false
metrics:
  push:
    ntfy:
      - url: https://ntfy.sh
        topic: github-backup
EOF
exec /gickup/gickup /tmp/gickup.yml

Gickup-Konfiguration

In der Konfiguration stehen Quelle, Ziel und Benachrichtigung. Die Datei wird beim Containerstart aus der Umgebung erzeugt. Die Struktur entspricht der Beispielkonfiguration von Gickup.

cron: "0 5 * * *"
source:
  github:
    - token: <dein GitHub-Token>
      wiki: true
destination:
  gitea:
    - url: https://git.example.com
      token: <dein Forgejo-Token>
      user: backup
      createorg: false
      lfs: true
      force: false
metrics:
  push:
    ntfy:
      - url: https://ntfy.sh
        topic: github-backup

Tokens nicht ins Repository

Lege die Tokenwerte in einer Umgebung ab, die nicht versioniert wird, und schließe sie aus deinem Repository aus. Die Benachrichtigung über ntfy ist praktisch, weil ein fehlgeschlagener Lauf nicht still im Hintergrund verschwinden muss.

Erster Lauf und Zeitplan

Starte den ersten Lauf von Hand und sieh dir danach die Logs an.

docker compose up -d gickup
docker compose logs -f gickup

Der produktive Zeitplan läuft bei mir täglich um 05:00 Ortszeit. Ohne TZ landet der Container in UTC, der Lauf verschiebt sich um zwei Stunden. Zusätzliche Argumente in der Compose-Datei erreichen Gickup nicht, weil das Image seinen eigenen Einstiegspunkt mitbringt; Logik gehört deshalb in ein eigenes Skript.

Falls du testweise alle 30 Minuten laufen willst, lautet der Cron-Ausdruck */30 * * * *. 0 */30 * * * bedeutet nicht alle 30 Minuten, sondern einmal täglich um 00:00, weil die Stunden von 0 bis 23 laufen.

Prüfen, dass die Kopie vollständig ist

Vergleiche die Repository-Liste von GitHub mit der Liste in Forgejo und blättere bei der API-Auswertung durch alle Seiten. Die Forgejo-Schnittstelle liefert pro Antwort höchstens 50 Einträge. Ohne Pagination hältst du 50 schnell für die Gesamtzahl. Bei mir stehen rund 70 Repositories in meinem Konto, in Forgejo liegen 71 Verzeichnisse. Die Differenz ist kein Fehler: Gickup spiegelt alles, was das Token erreichen kann, und darunter sind zwei Repositories, die nicht in meinem Konto liegen.

Bei selbst ausgestellten Zertifikaten muss der Truststore in den Container gemountet werden. Sonst endet der Push mit x509: certificate signed by unknown authority. Auch die LFS-Daten solltest du stichprobenartig prüfen, wenn du sie aktiviert hast.

Aus dem Spiegel wiederherstellen

Die Wiederherstellung ist absichtlich unspektakulär. Du klonst direkt aus deiner Forgejo-Instanz und arbeitest mit diesem Repository weiter:

git clone https://git.example.com/backup/example.git

Was dabei schiefgeht

Der auffälligste Fehler war ERR pkt-line 3: EOF git=push stage=gitea bei jedem Repository, gefolgt von Exiting with status=1. In meinem Fall war die Ursache kein kaputtes Werkzeug, sondern ein Mountproblem: Forgejo war gestartet, bevor die Netzfreigabe eingebunden war, und sah deshalb kein einziges Repository.

Ein Token ohne write:user lässt jeden Push scheitern, weil fehlende Repositories angelegt werden müssen. Eine falsche Zeitzone verschiebt den Lauf. Ein falscher Cron-Ausdruck startet zur falschen Zeit. Und ein selbst ausgestelltes Zertifikat braucht Vertrauen im Container. Das sind keine spektakulären Bugs, aber genau die Sorte Fehler, die Logs gern wie ein Orakel präsentieren.

Fazit

Der Aufbau sichert rund 70 Repositories aus meinem Konto in meine eigene Forgejo-Instanz. Der Lauf startet täglich um 05:00 Ortszeit und dauert bei einer vollständigen Runde knapp fünf Minuten, gemessen waren 4 Minuten 57 Sekunden. Der Preis dafür ist ein eigener Forgejo, Speicherplatz, Docker und etwas Aufmerksamkeit bei Tokens, Mounts und Logs.

Wenn du ebenfalls viel Arbeit in einem gehosteten Konto liegen hast, sichere dein eigenes Konto, bevor du es brauchst. Wie löst du das, und welches dieser Fehlerbilder ist dir schon begegnet?

Raffi

Seit 2011 blogge ich hier über Programmierung, meine Software, schreibe Tutorials und versuche mein Wissen, so gut es geht, mit meinen Lesern zu teilen.

Hinterlasse einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Sie dient nur dem Spamschutz.