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:
- KeePassXC
- GNU make
- Python 3.14
- Go
- OpenSSH
- GNU Privacy Guard (GPG)
- Git-crypt
- uv
- direnv
- dhall conversion programs
- dhall-json
- dhall-yaml
- dhall-toml
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.
- Container bauen
make build
- 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.
-
SSH-Schlüssel zum Agent hinzufügen:
ssh-add -
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.
-
Tool über die mitgelieferte
Makefileinstallieren. Schlägt dies fehl, installieren Sie zunächst Go.make install -
creds.jsonmit den richtigen Zugangsdaten anlegen. Ein fertiges Snippet liegt in der Schlüsseldatenbank unter dem Eintragdnscontrol creds.json.{ "cloudns": { "TYPE": "CLOUDNS", "auth-id": "XXX", "auth-password": "XXX" }, "hetzner_v2": { "TYPE": "HETZNER_V2", "api_token": "XXX" } } -
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.
-
Passwort
Ansible Vaultaus der Schlüsseldatenbank kopieren. -
Passwort in der Shell einlesen:
read -s pw -
Passwort in die vorgesehene Datei schreiben:
echo "$pw" > ansible/vaultpass
Schritt 4: Ansible installieren
Ziel: Ansible ist einsatzbereit.
-
uv installieren.
-
Ins Ansible-Verzeichnis wechseln:
cd ansible. -
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.
-
PGP-Schlüssel generieren — mit E-Mail-Adresse, ohne Ablaufdatum, Kommentar "Git-Crypt":
gpg --full-generate-key -
Ö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> -
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.
-
Pubkey in den eigenen Schlüsselbund importieren:
gpg --import keyfile.gpg -
In das Configs-Repository wechseln.
-
Neuen Schlüssel autorisieren:
git-crypt add-gpg-user <KEY-ID> -
Ä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.
-
SSH-Schlüssel zum Agent hinzufügen:
ssh-add -
Repository klonen:
git clone git@gitlab.com:ferrosoft/configs.git cd configsSchlägt der Klon fehl, vergewissern Sie sich, dass Ihr SSH-Schlüssel in Gitlab hinterlegt ist und Sie Zugriff auf die Ferrosoft-Gruppe haben.
-
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.
- Programme vom dhall-haskell-Projekt herunterladen.
- Binaries
dhall-to-json,dhall-to-tomlunddhall-to-yamlin das Unterverzeichnisbinentpacken. - Sicherstellen, dass
direnvkorrekt eingerichtet ist, damit beim Wechsel in das Configs-RepositoryPATHautomatisch um das Unterverzeichnisbinerweitert 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
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
name | Text | - | Name der Umgebung. Muss dem Dateinamen entsprechen. |
platform | Platform.Config | - | Ferrosoft-Platform-Konfiguration |
postgres | Platform.Postgres | - | PostgreSQL-Konfiguration |
caddy | Platform.Caddy | - | Caddy-Konfiguration |
valkey | Optional Platform.ImageSpec | None | Valkey-Container-Image |
victoria | Optional Platform.ImageSpec | None | Victoria-Container-Image |
weasyprint | Optional Platform.ImageSpec | None | Weasyprint-Container-Image |
docker_socket_proxy | Optional Platform.ImageSpec | None | Docker-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.
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
listen_address | Text | 0.0.0.0 | Adresse, auf der Ferrosoft Platform lauscht. |
listen_port | Natural | 80 | Port, auf dem Ferrosoft Platform lauscht. |
django_secret_key | Text | - | Geheimer Schlüssel für Django (Session-Verschlüsselung u.ä.) |
sentry_dsn | Text | - | Sentry-DSN für Fehlertracking |
service_fqdn_platform | Text | - | Domain für das Web-UI. Muss über das Internet erreichbar sein — TLS-Zertifikate werden über Let's Encrypt ausgestellt. |
service_fqdn_emiflow | Text | - | Domain für das Emiflow-Web-UI. Muss über das Internet erreichbar sein — TLS-Zertifikate werden über Let's Encrypt ausgestellt. |
geoapify_api_key | Text | - | API-Schlüssel für Geoapify (Kartendienst; erforderlich für die Distanzermittlung) |
django | Optional (DjangoSettings → DjangoSettings) | None | Siehe 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"
}
)
}
-
λ(d : Platform.DjangoSettings.Type) →DHALL-Syntax für Funktionen:
λ(Argument) -> Body -
⫽DHALL-Operator "Shallow right-biased record value merge":
record-a ⫽ record-b
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.
-
Ins Ansible-Verzeichnis wechseln:
cd ansible. -
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.
-
Ins Ansible-Verzeichnis wechseln:
cd ansible. -
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. Nachupdate-os.ymlobligatorisch — 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
-
SSH-Schlüssel in den Agent laden, falls noch nicht geschehen:
ssh-add -
Im Infrastruktur-Repository ins Unterverzeichnis
ansiblewechseln:cd ansible -
Gewünschte Playbooks ausführen. Ersetzen Sie
xxxdurch 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
<GRUPPEN_NAME>— eine der verfügbaren Host-Gruppen<UNIT_NAME>— frei wählbar; kurzer, thematischer Name, z.B.ssh
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:
-
Im Infrastruktur-Repository ins Unterverzeichnis
ansiblewechseln. -
Passwort einlesen:
read -s pw -
Passwort verschlüsseln:
echo -n "$pw" | uv run ansible-vault encrypt_string --vault-id ferrosoft@vaultpass -
Verschlüsselten Block kopieren (beginnt mit
!vault | $ANSIBLE_VAULT...). -
Block als Variablen-Wert in die YAML-Datei einfügen.
Variablen-Referenz
| Playbook | Variable | Beschreibung |
|---|---|---|
| bootstrap.yml | fb_authorized_keys | Öffentliche SSH-Schlüssel für root |
| bootstrap.yml | fb_operator_authorized_keys | Öffentliche SSH-Schlüssel für operator |
| bootstrap.yml | fb_group_packages | APT-Pakete, die installiert werden sollen |
| bootstrap.yml | fb_snapshot_enabled | Automatische Snapshots für BTRFS-Dateisysteme aktivieren |
| update-os.yml | auto_update_apply_updates | Unbeaufsichtigte Installation von Updates aktivieren |
| platform.yml | fp2_docker_registry | Name der privaten Container-Registry |
| platform.yml | fp2_docker_registry_user | Benutzername für die Container-Registry |
| platform.yml | fp2_docker_registry_password | Passwort für die Container-Registry |
| platform.yml | fp2_caddy_network | Name des externen Netzwerks. Muss mit dem Wert im Configs-Environment übereinstimmen |
| platform.yml | fp2_compose_files | Auszuführende Compose-Dateien |
| platform.yml | fp2_migration_management_commands | Django-Management-Befehle für die Migration |
Backup-Variablen
Diese Variablen betreffen Sicherung und Wiederherstellung.
| Playbook | Variable | Beschreibung |
|---|---|---|
| platform.yml | fp2_restic_s3_location | Standort des S3-Buckets (Teil des fp2_restic_repository-Standardwerts) |
| platform.yml | fp2_restic_s3_bucket | Bucket-Name (Teil des fp2_restic_repository-Standardwerts) |
| platform.yml | fp2_restic_repository | Restic-Repository-URL (Standard: Hetzner-Cloud-S3-URL aus fp2_restic_s3_location und fp2_restic_s3_bucket) |
| platform.yml | fp2_restic_s3_access_key | S3-Access-Key — über Hetzner Cloud Console beziehen |
| platform.yml | fp2_restic_s3_secret_key | S3-Secret-Key — über Hetzner Cloud Console beziehen |
| platform.yml | fp2_restic_password | Passwort zur Verschlüsselung der Sicherungsdateien |
| platform.yml | fp2_backup_keep_daily | Anzahl täglicher Sicherungen (Standard: 7) |
| platform.yml | fp2_backup_keep_weekly | Anzahl wöchentlicher Sicherungen (Standard: 4) |
| platform.yml | fp2_backup_keep_monthly | Anzahl monatlicher Sicherungen (Standard: 6) |
| platform.yml | fp2_backup_job_schedule | Zeitplan des Sicherungs-Jobs — systemd.time calendar events (Standard: *-*-* 03:00:00) |
| platform.yml | fp2_backup_job_timeout_sec | Timeout 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.
| Playbook | Variable | Beschreibung |
|---|---|---|
| platform.yml | fp2_configs_src | Lokales Quellverzeichnis mit den vom Configs-Repository erzeugten Konfigurationsdateien. Wird nach /etc/ferrosoft auf den Server übertragen. |
| platform.yml | fp2_docker_registry_reauthorize | Neu-Authentifizierung an der Registry erzwingen |
| platform.yml | fp2_migration_vacuumdb | vacuumdb 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
-
Ins Configs-Repository wechseln.
-
Secret-Key für Django erzeugen:
openssl rand -base64 50 | tr -dc 'a-zA-Z0-9' | head -c 50 -
Environment-Datei anlegen.
Die DHALL-Datei (Endung
.dhall) kommt in den Unterordnerenvironments— Dateien dort werden transparent verschlüsselt. Vorlage: Environment-Snippet. -
Konfigurationsdateien bauen (
ENV= Name der Environment-Datei ohne.dhall-Endung):make ENV=xxxDie 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.
-
Ins Infrastruktur-Repository wechseln.
-
Docker Container Engine installieren:
uv run ./ansible-playbook --host "xxx" playbooks/docker-ce.yml -
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.
-
Initialen Mandanten anlegen. Erstes Argument: Mandant-Name, zweites Argument: Organisations-Name.
Ferrosoftist die eingebaute Organisation:./container-run --host "xxx" -- addtenant Ferrosoft Ferrosoft -
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_dsnin der Environment-Datei), dort den Fehler im Detail untersuchen. - Flüchtige Fehler — per SSH auf den Server verbinden, nach
/etc/ferrosoftwechseln und mitdocker 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:
| Argument | Beschreibung |
|---|---|
-f, --file | Docker-Compose-Datei auf dem Server (Standard: /etc/ferrosoft/compose.yml; für Caddy-Administration: /etc/ferrosoft/compose.caddy.yml) |
-p, --project | Docker-Compose-Projektname (Standard: ferrosoft; für Caddy-Administration: caddy) |
-s, --service | Service-Name (Standard: platform-manage) |
-h, --host | SSH-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-Name | Befehl | Beschreibung |
|---|---|---|
| auth | changepassword | Passwort eines Benutzers ändern |
| auth | createsuperuser | Benutzer mit uneingeschränkten Berechtigungen anlegen |
| dataimport | dataimport_json | JSON-Katalogdatei importieren |
| django | check | Deployment-Check ausführen |
| django | loaddata | Django-Fixture in Master-Datenbank importieren |
| django | migrate | Datenbankmigrationen in Master-Datenbank anwenden |
| django | sendtestemail | Test-E-Mail senden |
| emiflow | copy_transport | Transportkette kopieren |
| emiflow | gen_rapidstart | Rapidstart-Datei erzeugen (für Microsoft Business Central 365) |
| emiflow | import_catalog | JSON-Katalogdatei importieren (Emiflow-spezifisch) |
| ferrobase | addtenant | Mandant anlegen (auch über Web-UI möglich) |
| ferrobase | deltenant | Mandant löschen (auch über Web-UI möglich) |
| ferrobase | ferrobase_cleanup | Aufräumarbeiten ausführen (z.B. abgelaufene Dateien löschen) |
| ferrobase | ferrobase_update | Datenbankinhalt nach Update aktualisieren |
| ferrobase | listtenants | Mandanten auflisten (auch über Web-UI möglich) |
| ferrobase | loaddata_tenant | Django-Fixture in allen Mandant-Datenbanken importieren |
| ferrobase | migratetenants | Datenbankmigrationen in allen Mandant-Datenbanken anwenden |
| sessions | clearsessions | Abgelaufene Sitzungen löschen |
| staticfiles | collectstatic | Statische Dateien ins WWW-Verzeichnis kopieren |
Durch Deployment ausgeführte Befehle
Das Ansible-Playbook platform.yml führt folgende Befehle automatisch aus:
migrate(undmigratetenants)collectstatic --noinputferrobase_cleanupferrobase_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.jsonsrc/ferrosoft/apps/emiflow/static/emiflow/examples/de/glec-operations-extract.jsonsrc/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.
-
SSH-Shell als
rootöffnen. -
Aus der letzten Sicherung wiederherstellen:
ferrosoft-restore
Anderen Snapshot verwenden
-
SSH-Shell als
rootöffnen. -
Restic-Zugangsdaten laden:
source /etc/restic/env -
Verfügbare Snapshots auflisten:
restic snapshots -
Gewünschten Snapshot wiederherstellen (
xxxdurch Snapshot-ID ersetzen):ferrosoft-restore xxx
Ablauf der Wiederherstellung
- Alle Dienste werden gestoppt.
- Dateien werden aus dem Restic-Snapshot in einen temporären Ordner extrahiert.
- Docker Volumes werden wiederhergestellt:
- Nicht vorhandene Volumes werden angelegt.
- Vorhandene Dateien im Volume werden gelöscht.
- Das komprimierte Volume-Archiv wird ins Volume ausgepackt.
- Postgres wird gestartet und der Datenbank-Dump eingespielt.
- Alle übrigen Dienste werden gestartet.
- Der temporäre Ordner wird gelöscht.
Troubleshooting
-
ferrosoft-restorebricht mit Fehler ab — Vollständige Ausgabe des Skripts analysieren. Fehler beheben, Skript erneut ausführen. Für detaillierte Befehlsausgabeset -xin/usr/local/bin/ferrosoft-restoreeinfügen und Skript neu starten. -
Wiederherstellung läuft durch, Website antwortet nicht — Dienststatus prüfen:
docker compose -f /etc/ferrosoft/compose.yml psLogs einzelner Dienste prüfen, z.B. für
platform:docker compose -f /etc/ferrosoft/compose.yml logs platformFehlerquelle 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 --showliefert 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
| Parameter | Wert | Beschreibung |
|---|---|---|
enabled | Y | zswap aktivieren |
compressor | lz4 | Schnelle Komprimierung. Alternativ zstd für bessere Kompressionsrate. |
max_pool_percent | 20 | Maximaler 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
- Kernel-Dokumentation: zswap
- Chris Down — Debunking zswap and zram myths (2026)
- LinuxBlog — zswap IS better than zram (2025)
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-worldschlä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"
| Flag | Beschreibung |
|---|---|
--url | Basis-URL der GitLab-Instanz (oder https://gitlab.com für GitLab SaaS) |
--token | Authentifizierungstoken aus der GitLab-Oberfläche |
--executor | Muss shell sein — Jobs laufen direkt auf dem Host und können docker aufrufen |
--description | Anzeigename in der GitLab-Oberfläche |
--tag-list | Kommagetrennte 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