Betriebshandbuch

Ihre Mission: Administration der Ferrosoft-Infrastruktur und Deployment der Ferrosoft Plattform (Emiflow). Dieses Handbuch ist Ihre Lagekarte — studieren Sie es, bevor Sie ins Feld gehen.

Voraussetzungen für den Einsatz

Ohne die folgenden Zugänge ist die Mission nicht durchführbar. Beschaffen Sie diese, bevor Sie beginnen:

  • Schlüsseldatenbank FerrosoftOperations.kdbx (KeePassXC) — enthält die gemeinsam genutzten Passwörter für den Betrieb.
    • Zugang erfordert eine Schlüsseldatei.
    • Datenbank und Schlüsseldatei müssen vorab beschafft werden.
  • Gitlab Ferrosoft-Gruppe — Zugang zum Infrastruktur-Repository und zum Configs-Repository.
    • Eigener Account erforderlich. Lassen Sie sich von einem bestehenden Mitglied freischalten.

Anforderungen an das Einsatzsystem

Ihr Gerät muss einsatzbereit sein. Folgende Plattformen sind bekannt:

  • Linux — Bevorzugte Plattform. Distribution irrelevant, solange die erforderlichen Programme installiert werden können.
  • Windows — Nicht im Feld getestet. Verwenden Sie das Windows Subsystem für Linux (WSL2).
  • macOS — Nicht im Feld getestet. Verwenden Sie eine Linux-VM.

Erforderliche Ausrüstung

Stellen Sie sicher, dass folgende Programme einsatzbereit sind:

Containerisierte Ausrüstung

Das Infrastruktur-Repository stellt im Unterordner operator ein einsatzbereites Dockerfile bereit. Dieses enthält sämtliche oben aufgeführten Tools mit Ausnahme von KeePassXC. Nutzen Sie den Container, um den Einrichtungsaufwand zu minimieren. Weitere Details entnehmen Sie der README.md im selben Unterordner.

  1. Container bauen
make build
  1. Container ausführen
make run

Infrastruktur-Repository einrichten

Zweck: Zugriff auf die Infrastruktur erlangen

Zielgruppe: Administrator mit Gitlab-Zugriff

Schritt 1: Repository klonen

Ziel: Zugriff auf Ansible-Playbooks und DNS-Konfiguration.

  1. SSH-Schlüssel zum Agent hinzufügen:

    ssh-add
    
  2. Repository klonen:

    git clone git@gitlab.com:ferrosoft/infrastructure.git
    cd infrastructure
    

Schlägt der Klon fehl, vergewissern Sie sich, dass Ihr SSH-Schlüssel in Gitlab hinterlegt ist und Sie Zugriff auf die Ferrosoft-Gruppe haben.

Schritt 2: DNSControl einrichten

Ziel: DNS-Zonen über dnsconfig.json verwalten.

  1. Tool über die mitgelieferte Makefile installieren. Schlägt dies fehl, installieren Sie zunächst Go.

    make install
    
  2. creds.json mit den richtigen Zugangsdaten anlegen. Ein fertiges Snippet liegt in der Schlüsseldatenbank unter dem Eintrag dnscontrol creds.json.

    {
      "cloudns": {
        "TYPE": "CLOUDNS",
        "auth-id": "XXX",
        "auth-password": "XXX"
      },
      "hetzner_v2": {
        "TYPE": "HETZNER_V2",
        "api_token": "XXX"
      }
    }
    
  3. Einsatzbereitschaft prüfen:

    bin/dnscontrol preview
    

Schritt 3: Ansible Vault einrichten

Ziel: Ansible kann bei der Ausführung von ansible-playbook verschlüsselte Werte entschlüsseln.

  1. Passwort Ansible Vault aus der Schlüsseldatenbank kopieren.

  2. Passwort in der Shell einlesen:

    read -s pw
    
  3. Passwort in die vorgesehene Datei schreiben:

    echo "$pw" > ansible/vaultpass
    

Schritt 4: Ansible installieren

Ziel: Ansible ist einsatzbereit.

  1. uv installieren.

  2. Ins Ansible-Verzeichnis wechseln: cd ansible.

  3. Abhängigkeiten installieren:

    uv sync
    

Configs-Repository

Das Configs-Repository enthält die Konfigurationsdateien für die Ferrosoft Plattform und verwandte Dienste. Die Konfiguration ist in der DHALL-Sprache verfasst. Das Dhall-Projekt stellt Konversionsprogramme bereit, um DHALL-Dateien in JSON, TOML und YAML zu überführen.

Environment-Dateien

Das Herzstück des Configs-Repositorys sind die Environment-Dateien. Sie bündeln alle Informationen, die zur Erzeugung der konkreten Konfigurationsdateien benötigt werden.

Die Dateien liegen im Unterordner environments. Git-Crypt verschlüsselt diesen Ordner automatisch. Bevor Sie auf die Dateien zugreifen können, muss Ihr Schlüssel autorisiert werden.

Environment-Snippet

Fertiges Snippet zum Kopieren und Anpassen — ersetzen Sie alle xxx-Platzhalter:

let Platform = ../platform/package.dhall

in  Platform.Environment::{
    , name = "sandbox"
    , caddy = Platform.Caddy::{=}
    , platform = Platform.Config::{
      , version = "v2.45"
      , django_secret_key = "xxx"
      , sentry_dsn =
          "https://xxx@xxx.ingest.de.sentry.io/xxx"
      , service_fqdn_emiflow = "emiflow.sandbox.ferro.software"
      , service_fqdn_platform = "sandbox.ferro.software"
      , geoapify_api_key = "xxx"
      , django = Some
          ( λ(d : Platform.DjangoSettings.Type) →
                d
              ⫽ { EMAIL_HOST_USER = Some "xxx"
                , EMAIL_HOST_PASSWORD = Some "xxx"
                }
          )
      }
    , postgres = Platform.Postgres::{
      , admin_user_name = "postgres"
      , admin_user_password =
          "xxx"
      , host = "postgres"
      , port = 5432
      , platform_database = "platform_master"
      , platform_user_name = "platform"
      , platform_user_password =
          "xxx"
      }
    }

Konfigurationsdateien erzeugen

Makefile ausführen. Mit ENV geben Sie die Environment-Datei an (ohne .dhall-Endung). Die fertigen Dateien landen in build/<ENV> und werden beim Deployment der Ferrosoft Plattform benötigt.

make clean all ENV=sandbox

Mitarbeiter autorisieren

Zweck: Zugriff auf die Plattform-Konfiguration erlangen

Zielgruppe: Administrator mit Gitlab-Zugriff

Zeitaufwand: 10 Minuten

Der environments-Ordner ist mittels Git-Crypt (PGP) transparent verschlüsselt. Für den Zugriff wird ein PGP-Schlüssel benötigt, der von einem bereits autorisierten Mitarbeiter freigeschaltet werden muss.

Schritt 1a: PGP-Schlüssel für neuen Mitarbeiter erstellen

Der neue Mitarbeiter erzeugt den Schlüssel auf seiner eigenen Maschine.

  1. PGP-Schlüssel generieren — mit E-Mail-Adresse, ohne Ablaufdatum, Kommentar "Git-Crypt":

    gpg --full-generate-key
    
  2. Öffentlichen Schlüssel (Pubkey) exportieren. Ersetzen Sie <KEY-ID> durch den Schlüsselnamen, z.B. die gewählte E-Mail-Adresse:

    gpg --armor --export <KEY-ID>
    
  3. Pubkey kopieren und an einen bereits autorisierten Mitarbeiter übergeben.

Schritt 1b: Neuen PGP-Schlüssel autorisieren

Der autorisierte Mitarbeiter schaltet den neuen Schlüssel auf seiner Maschine frei.

  1. Pubkey in den eigenen Schlüsselbund importieren:

    gpg --import keyfile.gpg
    
  2. In das Configs-Repository wechseln.

  3. Neuen Schlüssel autorisieren:

    git-crypt add-gpg-user <KEY-ID>
    
  4. Änderungen committen und pushen.

Schritt 2: Configs-Repository entsperren

Sobald der neue Mitarbeiter autorisiert ist, kann er das Repository klonen und die verschlüsselten Dateien entschlüsseln.

  1. SSH-Schlüssel zum Agent hinzufügen:

    ssh-add
    
  2. Repository klonen:

    git clone git@gitlab.com:ferrosoft/configs.git
    cd configs
    

    Schlägt der Klon fehl, vergewissern Sie sich, dass Ihr SSH-Schlüssel in Gitlab hinterlegt ist und Sie Zugriff auf die Ferrosoft-Gruppe haben.

  3. Verschlüsselte Dateien entsperren:

    git-crypt unlock
    

Schritt 3: Konversionsprogramme einrichten

Die Programme dhall-to-json u.ä. sind in den meisten Paketverwaltungen nicht verfügbar und müssen manuell bereitgestellt werden.

  1. Programme vom dhall-haskell-Projekt herunterladen.
  2. Binaries dhall-to-json, dhall-to-toml und dhall-to-yaml in das Unterverzeichnis bin entpacken.
  3. Sicherstellen, dass direnv korrekt eingerichtet ist, damit beim Wechsel in das Configs-Repository PATH automatisch um das Unterverzeichnis bin erweitert wird.

Platform-Environment

Für eine allgemeine Erläuterung zu Platform-Environment-Dateien — Zweck und Verwendung — siehe Configs-Repository.

Diese Seite dokumentiert die verfügbaren Felder der Environment-Datei.

Achtung: Diese Dokumentation kann veralten. Gehen Sie im Zweifelsfall direkt an die Quelle. Um z.B. zu ermitteln, was in platform.yml landet, öffnen Sie platform/files/platform.dhall und folgen Sie dem Pfad.

Top-Level: Platform.Environment

FeldTypStandardBeschreibung
nameText-Name der Umgebung. Muss dem Dateinamen entsprechen.
platformPlatform.Config-Ferrosoft-Platform-Konfiguration
postgresPlatform.Postgres-PostgreSQL-Konfiguration
caddyPlatform.Caddy-Caddy-Konfiguration
valkeyOptional Platform.ImageSpecNoneValkey-Container-Image
victoriaOptional Platform.ImageSpecNoneVictoria-Container-Image
weasyprintOptional Platform.ImageSpecNoneWeasyprint-Container-Image
docker_socket_proxyOptional Platform.ImageSpecNoneDocker-Socket-Proxy-Container-Image

Platform.ImageSpec

Ist ein ImageSpec nicht gesetzt, wird ein fest hinterlegtes Image mit latest-Version verwendet. Welches Image konkret zum Einsatz kommt, entnehmen Sie direkt der Quelldatei.

Platform.Config

Dieser Typ enthält zusätzlich die Felder von Platform.ImageSpec.

FeldTypStandardBeschreibung
listen_addressText0.0.0.0Adresse, auf der Ferrosoft Platform lauscht.
listen_portNatural80Port, auf dem Ferrosoft Platform lauscht.
django_secret_keyText-Geheimer Schlüssel für Django (Session-Verschlüsselung u.ä.)
sentry_dsnText-Sentry-DSN für Fehlertracking
service_fqdn_platformText-Domain für das Web-UI. Muss über das Internet erreichbar sein — TLS-Zertifikate werden über Let's Encrypt ausgestellt.
service_fqdn_emiflowText-Domain für das Emiflow-Web-UI. Muss über das Internet erreichbar sein — TLS-Zertifikate werden über Let's Encrypt ausgestellt.
geoapify_api_keyText-API-Schlüssel für Geoapify (Kartendienst; erforderlich für die Distanzermittlung)
djangoOptional (DjangoSettings → DjangoSettings)NoneSiehe Django-Settings anpassen

Platform.DjangoSettings

Dieser Typ deckt ein Subset der Django-Settings ab — gegenwärtig nur jene, die zwingend konfiguriert werden müssen. Die vollständige Liste finden Sie in platform/types.dhall unter let DjangoSettings.

Eine vollständige Dokumentation der Django-Settings ist nicht Teil dieses Handbuchs. Konsultieren Sie die offizielle Django-Dokumentation sowie die Quelldateien im Platform-Repository.

Django-Settings anpassen

Das Feld django in Platform.Config erwartet keine bloße Platform.DjangoSettings-Struktur, sondern eine Funktion: Sie erhält die Default-Settings als Argument und gibt die angepassten Settings zurück.

Die Funktion hat dabei freie Hand — sie kann einzelne Felder überschreiben oder die Settings vollständig ersetzen. Das Environment-Snippet zeigt eine Funktion, die zwei Felder der Default-Settings überschreibt:

, django = Some
	( λ(d : Platform.DjangoSettings.Type) →
		  d
		⫽ { EMAIL_HOST_USER = Some "ferrosoft.net"
		  , EMAIL_HOST_PASSWORD = Some "5NC3fQsiHGbSorIe"
		  }
	)
}

Server-Setup

Die folgenden Unterseiten führen Sie durch die Einrichtung eines Servers bis zur einsatzbereiten Ferrosoft Plattform.

Der manuelle Aufwand ist gering — das Gros erledigen Ansible Playbooks. Gehen Sie die Schritte in der angegebenen Reihenfolge durch:

  • Server Bootstrap — initiale Einrichtung eines frischen Servers zur Vereinheitlichung der Umgebung.
  • Playbooks — verfügbare Ansible Playbooks und Aufruf von ansible-playbook.
  • Konfigurationsmanagement — relevante Variablen zur Anpassung des Servers.
  • Platform Deployment — Deployment der Ferrosoft Plattform (inkl. Emiflow). Server Bootstrap und Konfigurationsmanagement müssen abgeschlossen sein, bevor Sie diesen Schritt ausführen.
  • Platform Management — Befehle zur laufenden Wartung der Ferrosoft Plattform in technischen Bereichen, die nicht von der Benutzerdokumentation abgedeckt sind.

Hardware-Anforderungen

Minimum

  • 4 GB RAM
  • 2 CPU-Kerne
  • Internet-Konnektivität (TLS-Zertifikate werden von Let's Encrypt per HTTP-Challenge ausgestellt)

Empfohlen

Der RAM-Bedarf wird maßgeblich durch den PDF-Rendering-Service bestimmt: Der eingebettete Web-Browser zieht bei großen Reports mehrere GB RAM — schweres Gerät, das seinen Preis fordert.

  • 16 GB RAM
  • 4 CPU-Kerne

Server Bootstrap

Zweck: Initiale Einrichtung eines Servers; Vorbereitung für die Ferrosoft Plattform Zielgruppe: Administrator mit SSH-Zugang auf den Root-Account Geschätzter Gesamtaufwand: 5 Minuten Wiederholungen: Keine — nur einmal nach Servererstellung ausführen


Der Bootstrap-Vorgang vereinheitlicht die Serverumgebung und wird genau einmal nach der Erstellung des Servers ausgeführt.

Vorbedingungen

  • Das Infrastruktur-Repository ist lokal eingerichtet.
  • SSH-Zugriff per Public-Key auf den root-Benutzer ist vorhanden.
  • Der Server läuft mit Debian (getestet mit Version 13 "Trixie").

Schritte

1. Variablen konfigurieren

Siehe Konfigurationsmanagement. In der Regel ist dieser Schritt nicht notwendig, da die Standardkonfiguration bereits passt.

2. Pflicht-Playbooks ausführen

Ziel: Serverumgebung vereinheitlichen, Updates einspielen, kontrollierten Reboot durchführen.

  1. Ins Ansible-Verzeichnis wechseln: cd ansible.

  2. Playbooks ausführen — siehe auch die Playbook-Beschreibungen:

    uv run ./ansible-playbook --host "xxx" playbooks/{bootstrap,update-os,reboot}.yml
    

3. Optionale Playbooks ausführen

Ziel: Zusätzliche Komponenten installieren und einrichten.

  1. Ins Ansible-Verzeichnis wechseln: cd ansible.

  2. Benötigte Playbooks ausführen, z.B. für die Docker Container Engine:

    uv run ./ansible-playbook --host "xxx" playbooks/docker-ce.yml
    

Ansible Playbooks

Zweck: Das Infrastruktur-Repository setzt Ansible Playbooks für Konfigurationsmanagement und Deployment ein — wiederholbar, versioniert und auf die Ferrosoft Infrastruktur zugeschnitten.

Zielgruppe: Administrator mit SSH-Zugriff auf den Root-Account

Vorbedingungen

  • SSH-Zugriff auf die Zielmaschine ist vorhanden.
  • Der SSH-Schlüssel ist im SSH-Agent geladen.

Verfügbare Playbooks

  • bootstrap.yml: Setzt Zeitzone und Lokalisierung, konfiguriert SSH, sudo, Firewall, etckeeper und Systemdienste, installiert Basispakete.
  • update-os.yml: Spielt Betriebssystem-Updates ein und richtet unbeaufsichtigte Aktualisierungen ein.
  • reboot.yml: Startet den Server neu. Nach update-os.yml obligatorisch — Kernel-Updates werden erst nach einem Reboot aktiv.
  • platform.yml: Deployt die Ferrosoft Plattform (Emiflow): kopiert Konfigurationsdateien auf den Server und startet alle Dienste, einschließlich Datenbank und Webserver.

Wrapper-Skript für ansible-playbook

Das Skript ansible/ansible-playbook im Infrastruktur-Repository ist ein Wrapper um das echte ansible-playbook. Es nimmt das zusätzliche Argument --host entgegen, über das der Ziel-Hostname übergeben wird. Alle weiteren Argumente werden unverändert durchgereicht. Das korrekte --vault-id-Argument wird automatisch gesetzt.

Playbooks ausführen

  1. SSH-Schlüssel in den Agent laden, falls noch nicht geschehen:

    ssh-add
    
  2. Im Infrastruktur-Repository ins Unterverzeichnis ansible wechseln:

    cd ansible
    
  3. Gewünschte Playbooks ausführen. Ersetzen Sie xxx durch den Hostnamen der Zielmaschine:

    uv run ./ansible-playbook --host "xxx" playbooks/bootstrap.yml
    

Konfigurationsmanagement

Das Infrastruktur-Repository enthält YAML-Dateien mit Ansible-Variablen, die das Verhalten des Servers steuern. Die Dateien liegen in ansible/playbooks/group_vars.

Verfügbare Host-Gruppen

Das Gruppen-Feature von Ansible ist hier bewusst auf das Nötigste reduziert — es gibt nur einen Server. Man schickt keine Division auf einen Mann. Der ansible-playbook-Wrapper trägt den per --host angegebenen Host automatisch in die Gruppen all, ferrosoft, platform und wrapper ein. Variablen in group_vars für jede dieser Gruppen treffen daher immer die Zielmaschine.

Variablen setzen

Variable in folgende Datei eintragen:

ansible/playbooks/group_vars/<GRUPPEN_NAME>/<UNIT_NAME>.yml

Nach der Änderung das betreffende Playbook erneut ausführen.

Variablen verschlüsseln

⚠️ BEFEHL: Passwörter und sensible Daten werden ausnahmslos mit Ansible Vault verschlüsselt. Kein Klartext im Repository. Kein Klartext im Commit. Keine Ausnahmen. Kein Pardon.

So verschlüsseln Sie einen Wert:

  1. Im Infrastruktur-Repository ins Unterverzeichnis ansible wechseln.

  2. Passwort einlesen:

    read -s pw
    
  3. Passwort verschlüsseln:

    echo -n "$pw" | uv run ansible-vault encrypt_string --vault-id ferrosoft@vaultpass
    
  4. Verschlüsselten Block kopieren (beginnt mit !vault | $ANSIBLE_VAULT...).

  5. Block als Variablen-Wert in die YAML-Datei einfügen.

Variablen-Referenz

PlaybookVariableBeschreibung
bootstrap.ymlfb_authorized_keysÖffentliche SSH-Schlüssel für root
bootstrap.ymlfb_operator_authorized_keysÖffentliche SSH-Schlüssel für operator
bootstrap.ymlfb_group_packagesAPT-Pakete, die installiert werden sollen
bootstrap.ymlfb_snapshot_enabledAutomatische Snapshots für BTRFS-Dateisysteme aktivieren
update-os.ymlauto_update_apply_updatesUnbeaufsichtigte Installation von Updates aktivieren
platform.ymlfp2_docker_registryName der privaten Container-Registry
platform.ymlfp2_docker_registry_userBenutzername für die Container-Registry
platform.ymlfp2_docker_registry_passwordPasswort für die Container-Registry
platform.ymlfp2_caddy_networkName des externen Netzwerks. Muss mit dem Wert im Configs-Environment übereinstimmen
platform.ymlfp2_compose_filesAuszuführende Compose-Dateien
platform.ymlfp2_migration_management_commandsDjango-Management-Befehle für die Migration

Backup-Variablen

Diese Variablen betreffen Sicherung und Wiederherstellung.

PlaybookVariableBeschreibung
platform.ymlfp2_restic_s3_locationStandort des S3-Buckets (Teil des fp2_restic_repository-Standardwerts)
platform.ymlfp2_restic_s3_bucketBucket-Name (Teil des fp2_restic_repository-Standardwerts)
platform.ymlfp2_restic_repositoryRestic-Repository-URL (Standard: Hetzner-Cloud-S3-URL aus fp2_restic_s3_location und fp2_restic_s3_bucket)
platform.ymlfp2_restic_s3_access_keyS3-Access-Key — über Hetzner Cloud Console beziehen
platform.ymlfp2_restic_s3_secret_keyS3-Secret-Key — über Hetzner Cloud Console beziehen
platform.ymlfp2_restic_passwordPasswort zur Verschlüsselung der Sicherungsdateien
platform.ymlfp2_backup_keep_dailyAnzahl täglicher Sicherungen (Standard: 7)
platform.ymlfp2_backup_keep_weeklyAnzahl wöchentlicher Sicherungen (Standard: 4)
platform.ymlfp2_backup_keep_monthlyAnzahl monatlicher Sicherungen (Standard: 6)
platform.ymlfp2_backup_job_scheduleZeitplan des Sicherungs-Jobs — systemd.time calendar events (Standard: *-*-* 03:00:00)
platform.ymlfp2_backup_job_timeout_secTimeout des Sicherungs-Jobs (Standard: 1 Stunde)

Ad-Hoc-Variablen

Diese Variablen per -e-Flag direkt beim Aufruf von ansible-playbook übergeben — in den Gruppen-Variablen sind sie fehl am Platz.

PlaybookVariableBeschreibung
platform.ymlfp2_configs_srcLokales Quellverzeichnis mit den vom Configs-Repository erzeugten Konfigurationsdateien. Wird nach /etc/ferrosoft auf den Server übertragen.
platform.ymlfp2_docker_registry_reauthorizeNeu-Authentifizierung an der Registry erzwingen
platform.ymlfp2_migration_vacuumdbvacuumdb im Postgres-Container ausführen

Platform Deployment

Zweck: Initiale Einrichtung der Ferrosoft Plattform

Zielgruppe: Administrator mit SSH-Zugang auf den Root-Account

Geschätzter Gesamtaufwand: 30 Minuten


Schritte

1. Configs-Repository einrichten

Siehe Einrichtung des Configs-Repositorys.

2. Konfigurationsdateien erzeugen

  1. Ins Configs-Repository wechseln.

  2. Secret-Key für Django erzeugen:

    openssl rand -base64 50 | tr -dc 'a-zA-Z0-9' | head -c 50
    
  3. Environment-Datei anlegen.

    Die DHALL-Datei (Endung .dhall) kommt in den Unterordner environments — Dateien dort werden transparent verschlüsselt. Vorlage: Environment-Snippet.

  4. Konfigurationsdateien bauen (ENV = Name der Environment-Datei ohne .dhall-Endung):

    make ENV=xxx
    

    Die fertigen Dateien liegen danach in build/xxx.

3. Compose-Projekt starten

Die Plattform und alle Dienste — Datenbank, Webserver und mehr — werden über Docker Compose verwaltet. Bereitstellung und Compose-Befehle übernimmt das Playbook platform.yml.

  1. Ins Infrastruktur-Repository wechseln.

  2. Docker Container Engine installieren:

    uv run ./ansible-playbook --host "xxx" playbooks/docker-ce.yml
    
  3. Plattform-Playbook ausführen. <CONFIGS_DIR> durch den Pfad zu den im vorigen Schritt erzeugten Konfigurationsdateien ersetzen:

    uv run ./ansible-playbook --host "xxx" playbooks/platform.yml -e fp2_configs_src=<CONFIGS_DIR>
    

    Nützliche Ad-Hoc-Variablen für diesen Aufruf sind im Konfigurationsmanagement dokumentiert.

4. Adminzugang einrichten

Überspringen Sie diesen Schritt, wenn Sie die Plattform aus einer Sicherung wiederherstellen.

  1. Initialen Mandanten anlegen. Erstes Argument: Mandant-Name, zweites Argument: Organisations-Name. Ferrosoft ist die eingebaute Organisation:

    ./container-run --host "xxx" -- addtenant Ferrosoft Ferrosoft
    
  2. Initialen Administrator anlegen. Die Organisations-ID ist die ID der eingebauten Ferrosoft-Organisation:

    ./container-run --host "xxx" -- createsuperuser --organization_id=0198a8b4-9109-7d8a-992f-3a4ed6ad0194
    

Mission erfüllt. Die Ferrosoft Plattform ist einsatzbereit. Zur Überprüfung die in der Environment-Datei konfigurierte URL aufrufen (platform.service_fqdn_platform).

Plattform aus Sicherung wiederherstellen

Siehe Schritte zur Wiederherstellung. Die dort beschriebenen Schritte werden nach der Einrichtung gemäß diesem Kapitel ausgeführt — Schritt 4 (Adminzugang) ausgenommen.

Troubleshooting

Playbook gescheitert oder Browser zeigt nicht das Erwartete?

  • Playbook hängt bei "Check if restic repo is initialized" — S3-Zugangsdaten prüfen, typischerweise in playbooks/group_vars/ferrosoft/backup.yml.
  • Ansible-Fehler — Ausgabe des ansible-playbook-Aufrufs genau lesen; die genaue Fehlermeldung steckt fast immer darin.
  • Fehler in der Django-App — falls Sentry konfiguriert ist (platform.sentry_dsn in der Environment-Datei), dort den Fehler im Detail untersuchen.
  • Flüchtige Fehler — per SSH auf den Server verbinden, nach /etc/ferrosoft wechseln und mit docker compose-Befehlen den Zustand inspizieren.

Platform Management

Die Web-Oberfläche der Ferrosoft Plattform deckt Routineaufgaben ab (siehe Benutzerhandbuch). Bestimmte Backend-Operationen erfordern jedoch direkten Zugriff über die Django-Management-CLI. Diese Befehle sind hier dokumentiert.

Management-Skript

Das Infrastruktur-Repository enthält im Unterordner ansible das Skript container-run. Es führt beliebige Befehle in Diensten der Ferrosoft Plattform aus — als Wrapper um ssh und docker compose run.

Standardmäßig wird der Dienst platform-manage gestartet: ein spezieller Kurzzeit-Dienst im Docker-Compose-Profil tools, der ausschließlich für die Django-Management-CLI vorgesehen ist.

Unterstützte Argumente:

ArgumentBeschreibung
-f, --fileDocker-Compose-Datei auf dem Server (Standard: /etc/ferrosoft/compose.yml; für Caddy-Administration: /etc/ferrosoft/compose.caddy.yml)
-p, --projectDocker-Compose-Projektname (Standard: ferrosoft; für Caddy-Administration: caddy)
-s, --serviceService-Name (Standard: platform-manage)
-h, --hostSSH-Hostname (Pflichtfeld)

Alle weiteren Optionen werden direkt an docker compose run --rm -i durchgereicht.

Beispiel: Argumente für container-run und den Dienst mit Doppel-Dash -- trennen:

./container-run --host "xxx" -- migratetenants

Django-Management-CLI

Alle verfügbaren Befehle auflisten:

./container-run --host "xxx" -- --help

Optionen eines bestimmten Befehls anzeigen (Beispiel: migratetenants):

./container-run --host "xxx" -- help migratetenants

Befehl ausführen (Beispiel: migratetenants):

./container-run --host "xxx" -- migratetenants

Gebräuchliche Management-Befehle

First-Party-Befehle stammen aus den Apps dataimport, emiflow, ferrobase, ferromaps und ticketing. Befehle aus ferromaps und ticketing sind nicht im Einsatz und werden hier nicht aufgeführt. Für Third-Party-Befehle konsultieren Sie die jeweilige Dokumentation.

Die fett markierten Befehle sind vorrangig relevant.

App-NameBefehlBeschreibung
authchangepasswordPasswort eines Benutzers ändern
authcreatesuperuserBenutzer mit uneingeschränkten Berechtigungen anlegen
dataimportdataimport_jsonJSON-Katalogdatei importieren
djangocheckDeployment-Check ausführen
djangoloaddataDjango-Fixture in Master-Datenbank importieren
djangomigrateDatenbankmigrationen in Master-Datenbank anwenden
djangosendtestemailTest-E-Mail senden
emiflowcopy_transportTransportkette kopieren
emiflowgen_rapidstartRapidstart-Datei erzeugen (für Microsoft Business Central 365)
emiflowimport_catalogJSON-Katalogdatei importieren (Emiflow-spezifisch)
ferrobaseaddtenantMandant anlegen (auch über Web-UI möglich)
ferrobasedeltenantMandant löschen (auch über Web-UI möglich)
ferrobaseferrobase_cleanupAufräumarbeiten ausführen (z.B. abgelaufene Dateien löschen)
ferrobaseferrobase_updateDatenbankinhalt nach Update aktualisieren
ferrobaselisttenantsMandanten auflisten (auch über Web-UI möglich)
ferrobaseloaddata_tenantDjango-Fixture in allen Mandant-Datenbanken importieren
ferrobasemigratetenantsDatenbankmigrationen in allen Mandant-Datenbanken anwenden
sessionsclearsessionsAbgelaufene Sitzungen löschen
staticfilescollectstaticStatische Dateien ins WWW-Verzeichnis kopieren

Durch Deployment ausgeführte Befehle

Das Ansible-Playbook platform.yml führt folgende Befehle automatisch aus:

  • migrate (und migratetenants)
  • collectstatic --noinput
  • ferrobase_cleanup
  • ferrobase_update

Konfigurationsdrift beheben

Neue Plattform-Versionen können Daten in bestehenden Mandant-Datenbanken erfordern, die nur bei der initialen Mandant-Anlage erzeugt werden. Der Befehl ferrobase_update behebt dies für die meisten Fälle — er legt fehlende Daten nach einem Update an oder aktualisiert sie.

Für JSON-Katalogdateien (u.a. GLEC-Emissionsdaten) greift dieser Mechanismus jedoch nicht. Diese müssen bei Änderungen Mandant für Mandant manuell eingespielt werden.

⚠️ Bringt ein Release geänderte JSON-Katalogdateien mit, sind alle Mandant-Datenbanken manuell zu aktualisieren. Mandant für Mandant. Keine Ausnahmen.

./container-run --host "$HOST" -- listtenants

# Folgende Befehle für jeden Mandant wiederholen:

./container-run --host "$HOST" -- import_catalog -T "$TENANT" -S catalog.schema.json \
	/app/lib/python3.14/site-packages/ferrosoft/apps/emiflow/static/emiflow/examples/de/glec-public-eu.json
./container-run --host "$HOST" -- import_catalog -T "$TENANT" -S operation.schema.json \
	/app/lib/python3.14/site-packages/ferrosoft/apps/emiflow/static/emiflow/examples/de/glec-operations-extract.json
./container-run --host "$HOST" -- dataimport_json \
	-s /app/lib/python3.14/site-packages/ferrosoft/apps/dataimport/static/dataimport/dataimport.schema.json \
	/app/lib/python3.14/site-packages/ferrosoft/apps/emiflow/static/emiflow/examples/de/importcollection.json

Die Drift betrifft Änderungen an folgenden Dateien im Platform-Repository (jeweils de- und en-Variante sowie die zugehörigen Schemadateien):

  • src/ferrosoft/apps/emiflow/static/emiflow/examples/de/glec-public-eu.json
  • src/ferrosoft/apps/emiflow/static/emiflow/examples/de/glec-operations-extract.json
  • src/ferrosoft/apps/emiflow/static/emiflow/examples/de/importcollection.json

Sicherung und Wiederherstellung

Sicherungen werden mit Restic erstellt. Ein Snapshot enthält:

  • Docker-Compose-Dateien und zugehörige Konfigurationsdateien
  • Alle Docker Volumes der Compose Services
  • Einen logischen Datenbank-Dump (pg_dumpall)

Das Playbook platform.yml richtet automatische Sicherungen ein. Sicherungsdateien werden verschlüsselt in einem S3-kompatiblen Objektspeicher in Hetzner Cloud abgelegt. Die Konfiguration erfolgt über das Konfigurationsmanagement.

Voraussetzungen für die Wiederherstellung

  • Der Objektspeicher enthält verwertbare Snapshots. Bei einem reinen Serverausfall ist das in der Regel der Fall. Bei einem Ransomware-Angriff muss damit gerechnet werden, dass der Objektspeicher gelöscht wurde — Hetzner Cloud bietet gegenwärtig keinen Schutz dagegen. In diesem Fall gibt es keine Verstärkung. Keine Evakuierung. Kein Zurück.
  • Der Bootstrap-Vorgang wurde abgeschlossen.
  • Der Deployment-Vorgang wurde abgeschlossen — Anlage des Adminzugangs ausgenommen. Die Service-Domain muss die Anmelden/Registrieren-Seite anzeigen.

Schritte zur Wiederherstellung

Das Playbook platform.yml legt das Skript ferrosoft-restore auf dem Server ab. Es stellt alle Dienste vollständig wieder her. Eine Teilwiederherstellung wird nicht unterstützt — falls erforderlich, muss die Sicherung auf einem neuen Server wiederhergestellt und manuell übertragen werden.

  1. SSH-Shell als root öffnen.

  2. Aus der letzten Sicherung wiederherstellen:

    ferrosoft-restore
    

Anderen Snapshot verwenden

  1. SSH-Shell als root öffnen.

  2. Restic-Zugangsdaten laden:

    source /etc/restic/env
    
  3. Verfügbare Snapshots auflisten:

    restic snapshots
    
  4. Gewünschten Snapshot wiederherstellen (xxx durch Snapshot-ID ersetzen):

    ferrosoft-restore xxx
    

Ablauf der Wiederherstellung

  1. Alle Dienste werden gestoppt.
  2. Dateien werden aus dem Restic-Snapshot in einen temporären Ordner extrahiert.
  3. Docker Volumes werden wiederhergestellt:
    • Nicht vorhandene Volumes werden angelegt.
    • Vorhandene Dateien im Volume werden gelöscht.
    • Das komprimierte Volume-Archiv wird ins Volume ausgepackt.
  4. Postgres wird gestartet und der Datenbank-Dump eingespielt.
  5. Alle übrigen Dienste werden gestartet.
  6. Der temporäre Ordner wird gelöscht.

Troubleshooting

  • ferrosoft-restore bricht mit Fehler ab — Vollständige Ausgabe des Skripts analysieren. Fehler beheben, Skript erneut ausführen. Für detaillierte Befehlsausgabe set -x in /usr/local/bin/ferrosoft-restore einfügen und Skript neu starten.

  • Wiederherstellung läuft durch, Website antwortet nicht — Dienststatus prüfen:

    docker compose -f /etc/ferrosoft/compose.yml ps
    

    Logs einzelner Dienste prüfen, z.B. für platform:

    docker compose -f /etc/ferrosoft/compose.yml logs platform
    

    Fehlerquelle identifizieren und neutralisieren.

Manuelle Einrichtungen

Die folgenden Unterseiten behandeln Aufgaben, die noch nicht in ein Ansible Playbook überführt wurden.

Betrachten Sie diese Seite als die Handgranaten-Schublade der Infrastruktur: Dinge, die funktionieren, aber eigentlich in bessere Hände gehören.

Swap-Datei und zswap einrichten

Übersicht

Dieses Runbook richtet eine festplattengestützte Swap-Datei mit zswap als komprimiertem In-Memory-Cache davor ein. zswap komprimiert ausgelagerte Seiten im RAM, bevor sie auf den Datenträger treffen — der Großteil des Swap-Traffics verbleibt dadurch im Arbeitsspeicher.

Geltungsbereich: Frisches Debian 13 (Trixie) auf Hetzner Cloud VPS.

Ergebnis: Eine persistente Swap-Datei mit zswap, die Neustarts übersteht.


Vorbedingungen

  • Root-Zugriff auf den Server
  • Kein aktiver Swap (swapon --show liefert keine Ausgabe)

Schritte

1. Swap-Datei anlegen

4 GB ist ein sinnvoller Standardwert für einen VPS mit 4–8 GB RAM.

fallocate -l 4G /swapfile

2. Berechtigungen setzen und formatieren

chmod 600 /swapfile
mkswap /swapfile

3. Swap aktivieren

swapon /swapfile

Einsatzbereitschaft prüfen:

swapon --show
free -h

Die Ausgabe muss /swapfile mit Typ file und der gewählten Größe zeigen.

4. Swap persistent machen

echo '/swapfile none swap sw 0 0' | tee -a /etc/fstab

5. zswap zur Laufzeit aktivieren

echo Y   | tee /sys/module/zswap/parameters/enabled
echo lz4 | tee /sys/module/zswap/parameters/compressor
echo 20  | tee /sys/module/zswap/parameters/max_pool_percent
ParameterWertBeschreibung
enabledYzswap aktivieren
compressorlz4Schnelle Komprimierung. Alternativ zstd für bessere Kompressionsrate.
max_pool_percent20Maximaler RAM-Anteil für den komprimierten Pool (Standard: 20).

Aktivierung prüfen:

cat /sys/module/zswap/parameters/enabled       # Y
cat /sys/module/zswap/parameters/compressor    # lz4

6. zswap persistent machen

Kernel-Boot-Parameter über GRUB setzen:

sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 zswap.enabled=1 zswap.compressor=lz4 zswap.max_pool_percent=20"/' /etc/default/grub
update-grub

7. Swappiness anpassen

Mit zswap ist der Standardwert von 60 korrekt. Auslagern ist günstig, da die meisten Seiten komprimiert im RAM verbleiben.

echo 'vm.swappiness=60' | tee /etc/sysctl.d/99-swap.conf
sysctl --system

8. Neustart und Abnahme

reboot

Nach dem Neustart — alle Parameter abnehmen:

swapon --show
cat /sys/module/zswap/parameters/enabled
cat /sys/module/zswap/parameters/compressor
cat /sys/module/zswap/parameters/max_pool_percent

Optional: zswap-Statistiken einsehen:

grep -r . /sys/kernel/debug/zswap/ 2>/dev/null

Funktionsweise

Arbeitsspeicherdruck
        │
        ▼
   ┌─────────┐
   │   RAM   │  Normaler Arbeitsspeicher
   └────┬────┘
        │ Kernel lagert Seite aus
        ▼
   ┌─────────┐
   │  zswap  │  Komprimiert im RAM (bis max_pool_percent)
   └────┬────┘
        │ Pool voll → kälteste Seiten werden verdrängt
        ▼
   ┌──────────┐
   │ /swapfile│  Festplatten-Swap (nur wenn zswap-Pool voll)
   └──────────┘

Der Großteil des Swap-Traffics erreicht den Datenträger nie. Die Swap-Datei ist das letzte Auffangnetz.


Rücknahme

# Swap deaktivieren
swapoff /swapfile
rm /swapfile

# fstab-Eintrag entfernen
sed -i '\|^/swapfile|d' /etc/fstab

# zswap-Boot-Parameter entfernen
sed -i 's/ zswap\.enabled=1 zswap\.compressor=lz4 zswap\.max_pool_percent=20//' /etc/default/grub
update-grub

# Swappiness-Override entfernen
rm -f /etc/sysctl.d/99-swap.conf
sysctl --system

Referenzen

GitLab Runner einrichten

Plattform: Hetzner Cloud · Debian 13 (Trixie) · Shell Executor

Voraussetzung: Server ist vollständig eingerichtet und Docker läuft.


Vorbedingungen

  • SSH-Zugriff auf den Zielserver
  • Docker installiert und einsatzbereit (docker run --rm hello-world schlägt durch)
  • GitLab Runner Authentifizierungstoken — zu beziehen unter Settings → CI/CD → Runners → New project runner im GitLab-Projekt oder der Gruppe

1 — GitLab Runner installieren

Offizielles Repository einbinden und Paket installieren:

curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" \
  | bash

apt install -y gitlab-runner

Installation prüfen:

gitlab-runner --version

2 — Runner-Benutzer Docker-Zugriff erteilen

Die Installation legt den Systembenutzer gitlab-runner an. Er muss in der Gruppe docker sein, damit Shell-Executor-Jobs docker ohne Root aufrufen können.

usermod -aG docker gitlab-runner

Zugriff auf den Docker-Daemon prüfen:

sudo -u gitlab-runner docker info

Die Ausgabe muss Docker-Engine-Details ohne Fehler liefern. Bei "permission denied" den Dienst neu starten (Schritt 4) und erneut prüfen.


3 — Runner registrieren

Platzhalter durch GitLab-URL und Authentifizierungstoken ersetzen:

gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com" \
  --token "glrt-XXXXXXXXXXXXXXXXXXXX" \
  --executor "shell" \
  --description "hetzner-shell-runner" \
  --tag-list "shell,docker,hetzner"
FlagBeschreibung
--urlBasis-URL der GitLab-Instanz (oder https://gitlab.com für GitLab SaaS)
--tokenAuthentifizierungstoken aus der GitLab-Oberfläche
--executorMuss shell sein — Jobs laufen direkt auf dem Host und können docker aufrufen
--descriptionAnzeigename in der GitLab-Oberfläche
--tag-listKommagetrennte Tags zur Job-Zuweisung

Die Konfiguration wird nach /etc/gitlab-runner/config.toml geschrieben.


4 — Runner konfigurieren (optional)

Konfigurationsdatei öffnen und bei Bedarf Parallelität oder weitere Einstellungen anpassen:

nano /etc/gitlab-runner/config.toml

Relevante Parameter:

concurrent = 4          # maximale Anzahl paralleler Jobs

[[runners]]
  name = "hetzner-shell-runner"
  executor = "shell"
  [runners.cache]
    MaxUploadedArchiveSize = 0   # 0 = unbegrenzte Cache-Archivgröße

Nach Änderungen Dienst neu starten:

systemctl restart gitlab-runner

5 — Dienst aktivieren und abnehmen

Autostart sicherstellen und Dienststatus prüfen:

systemctl enable gitlab-runner
systemctl status gitlab-runner

Die Ausgabe muss active (running) zeigen. Anschließend Verbindung zu GitLab prüfen:

gitlab-runner verify

Erwartete Ausgabe:

Verifying runner... is valid    runner=XXXXXXXX

Der Runner muss unter Settings → CI/CD → Runners im GitLab-Projekt als online erscheinen.


6 — Feuerprobe

Testpipeline anlegen — .gitlab-ci.yml im GitLab-Projekt anlegen oder ergänzen:

test-runner:
  tags:
    - shell
  script:
    - echo "Runner ist einsatzbereit"
    - docker info
    - docker run --rm hello-world

Änderung pushen und unter CI/CD → Pipelines den erfolgreichen Abschluss bestätigen.


Troubleshooting

Runner nimmt keine Jobs an — Tags in .gitlab-ci.yml müssen mit der Tag-Liste des Runners übereinstimmen. Alternativ Run untagged jobs in den Runner-Einstellungen in GitLab aktivieren.

"permission denied" bei Docker-Befehlen — Gruppenmitgliedschaft mit id gitlab-runner prüfen — Ausgabe muss docker enthalten. Falls die Gruppe soeben hinzugefügt wurde, Dienst neu starten: systemctl restart gitlab-runner.

Runner erscheint offline in GitLab — Dienststatus prüfen (systemctl status gitlab-runner) und sicherstellen, dass ausgehender HTTPS-Traffic zur GitLab-Instanz nicht durch eine Firewall geblockt wird.

Jobs hängen oder laufen in Timeout — Job-Log in GitLab und Runner-Log auf dem Server prüfen: journalctl -u gitlab-runner -f.


Wartung

  • Runner aktualisieren: apt update && apt upgrade gitlab-runner
  • Logs einsehen: journalctl -u gitlab-runner --since "1 hour ago"
  • Runner abmelden: gitlab-runner unregister --token "glrt-XXX"
  • Konfigurationsdatei: /etc/gitlab-runner/config.toml