Die Technik hinter dieser Website

OS
Talos Linux
Host
Hetzner, DE
Cluster
Kubernetes
CNI
Cilium (eBPF)
Ingress
Traefik
GitOps
Flux
Web
nginx
TLS
X25519MLKEM768
Hugo
0.166.0
Build
2026-11-03
Commit
967185b
Seiten
13
CSS/JS
30.5/5.2 KB
Fonts
63 KB
Cookies
0

Diese Website soll vor allem eines sein: schnell und aufgeräumt, damit der Inhalt im Vordergrund steht. Unter der Haube steckt dafür deutlich mehr. Ich schreibe die Texte in Markdown und lege sie in Git ab. Bei jeder Änderung baut eine Pipeline mit Hugo die Website neu, und Flux spielt sie automatisch auf meinen Kubernetes-Cluster. Einen Server muss ich dafür nie anfassen. Für mich ist das Ganze auch eine Spielwiese, auf der ich Technik ausprobiere, mit der ich beruflich zu tun habe. Auf dieser Seite erkläre ich, wie das zusammenpasst.

Überblick

Die Grafik zeigt, welche Teile zusammenspielen, wenn ich eine Änderung veröffentliche und Sie die Seite aufrufen. Die Nummern erklärt die Liste darunter.

KUBERNETES · TALOS LINUX · HETZNER, DEUTSCHLANDAUSSENBrowserBesucherLet's Encryptkostenlose CAIchMarc Fischergit pushGitHubCode, CI, RegistryTraefikIngressnginxWebsiteUmamiStatistikPostgreSQLDatenbankcert-managerTLS-ZertifikateFluxGitOpsCiliumNetzwerk, eBPFHTTPSACMEZertifikatpulldeploy1234
  1. Push: Ich pushe eine Änderung zu GitHub. GitHub Actions baut die Website mit Hugo und legt das Container-Image in der GitHub Container Registry ab.
  2. Deployment: Flux im Cluster bemerkt die neue Version im Deploy-Branch und tauscht den nginx-Container aus.
  3. Zertifikat: cert-manager holt die TLS-Zertifikate bei Let’s Encrypt und erneuert sie automatisch.
  4. Aufruf: Ihr Browser verbindet sich verschlüsselt mit Traefik, das die Anfrage an nginx weiterreicht.

Vom Text zur Website

HTML schreibe ich nie von Hand. Texte stehen in Markdown, Lebenslauf und Trainings als Daten in YAML, und Hugo setzt beides in die Templates ein. Der Lebenslauf entsteht aus denselben Daten, die Trainings kommen aus einem eigenen Repository dazu.

WEITERE REPOSLebenslaufExportTrainingsPull RequestWEBSITE-REPOMarkdownTexteTemplatesHTML, CSS, JSDatenYAMLLebenslauf,TrainingsGITHUB ACTIONS · BEI JEDEM PUSHHugo~0,2 spublic/HTML-SeitenDE und ENCSS, JSminifiziertSitemap & Co.llms.txt, curlImagenginx + public/Deployment: oben

Woher die Daten kommen

Lebenslauf, Projekte, Zertifizierungen und Skills pflege ich an genau einer Stelle, in einem eigenen Git-Repository. Daraus entstehen der PDF-Lebenslauf (gesetzt mit Typst, auf Deutsch und Englisch, ausführlich und als Kurzfassung) und die Daten für diese Website. Bekomme ich eine neue Zertifizierung auf Credly, legt eine Automatisierung sie mir als Vorschlag an.

Auch die Trainings, die ich gebe, pflege ich nicht von Hand:

  • Aus der Mail: Eine Automatisierung mit n8n liest die Auftragsbestätigungen der Schulungszentren aus und legt jedes Training als kleine Datei in einem privaten Git-Repository ab.
  • Was zählt: Ein Training zählt erst, wenn es tatsächlich stattgefunden hat und abgeschlossen ist. Geplante oder abgesagte Termine landen nie auf der Website.
  • Prüfung und Freigabe: Jede Änderung wird automatisch auf Plausibilität geprüft. Danach entsteht ein Pull Request für diese Website, den ich ansehe und freigebe.

So stimmen die Zahlen auf der Website, im PDF und in meiner eigenen Übersicht immer überein.

Hugo

Hugo ist ein Static Site Generator, Open Source und in Go geschrieben: ein einzelnes Binary ohne weitere Abhängigkeiten.

  • Prinzip: Hugo liest die Markdown-Dateien und die YAML-Daten, setzt sie in Templates ein und schreibt fertige HTML-Seiten. Das passiert einmal beim Build, nicht bei jedem Aufruf.
  • Build-Zeit: Die ganze Website in beiden Sprachen baut in etwa 0,2 Sekunden. Lokal sehe ich jede Änderung sofort im Browser.
  • Sprachen: Deutsch und Englisch kommen aus denselben Templates, die Texte stehen in Übersetzungsdateien.
  • Beim Build: CSS und JavaScript werden gebündelt, verkleinert und bekommen einen Hash im Dateinamen. Sitemap, llms.txt und die curl-Version entstehen gleich mit.

Ein vereinfachtes Beispiel. Die Markdown-Datei enthält nur den Text:

---
title: "Kontakt"
---
Sie erreichen mich am besten per E-Mail.

Das Template legt den Rahmen der Seite fest und wo Titel und Inhalt stehen:

<!doctype html>
<html lang="{{ site.Language.Lang }}">
<head>
  <title>{{ .Title }} | Marc Fischer</title>
</head>
<body>
  <h1>{{ .Title }}</h1>
  {{ .Content }}
</body>
</html>

Daraus erzeugt Hugo beim Build diese HTML-Seite:

<!doctype html>
<html lang="de">
<head>
  <title>Kontakt | Marc Fischer</title>
</head>
<body>
  <h1>Kontakt</h1>
  <p>Sie erreichen mich am besten per E-Mail.</p>
</body>
</html>

Deployment per GitOps

Die Pipeline baut das Image und schreibt danach nur dessen neue Version in einen eigenen Deploy-Branch. Flux im Cluster beobachtet diesen Branch, gleicht den Cluster jede Minute damit ab und tauscht den Container per Rolling Update ohne Ausfallzeit aus.

GITHUB ACTIONSCLUSTERImageRegistry (GHCR)Deploy-Branchneue VersionFluxprüft jede MinuteRolling Updateneu vor altmarcfischer.deliveohne Ausfallzeit, ohne Login auf einem Server

Flux braucht dafür nur wenige Zeilen YAML:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: website
  namespace: flux-system
spec:
  interval: 1m
  url: ssh://git@github.com/example/website
  ref:
    branch: deploy  # written only by the pipeline

Der letzte Schritt der Pipeline ist entsprechend kurz:

- name: Update branch deploy
  run: |
    TAG="${GITHUB_SHA::7}"
    sed -i -E "s/^(\s*newTag:).*/\1 ${TAG}/" deploy/kustomization.yaml
    git commit -qam "deploy: website ${TAG}"
    git push -q origin deploy

Statisch und stateless

Früher lief diese Website mit WordPress, also mit Datenbank, PHP und Plugins. Heute ist sie rein statisch, Hugo erzeugt alle Seiten vorab. Das hat handfeste Vorteile:

  • Sicherheit: keine Datenbank, keine Anmeldung, kein Code, der bei einem Aufruf auf dem Server ausgeführt wird, und keine Plugins, die ständig aktualisiert werden müssen.
  • Auslieferung: Der Server liefert nur Dateien aus, er muss nichts berechnen.
  • Versionierung: Jede Änderung liegt in Git und lässt sich nachvollziehen oder zurücknehmen.

Die Website ist im Cluster außerdem stateless: Jede Version steckt komplett im Container-Image, das bei jeder Änderung neu gebaut wird. Sie braucht deshalb kein Persistent Volume und kein CSI. Für ein Update startet Kubernetes einfach einen neuen Container, und ein Neustart kostet keine Daten. Persistente Daten gibt es nur für die Besucherstatistik, die eine kleine eigene Datenbank hat.

Plattform

Die Website läuft auf einem eigenen Kubernetes-Cluster bei Hetzner in Deutschland. Die Bausteine und ihre Aufgaben:

  • Talos Linux: schlankes, unveränderliches Betriebssystem, das nur für Kubernetes gebaut ist und keinen SSH-Zugang kennt
  • Kubernetes: betreibt die Website auf einem einzelnen Node. Bei einem Update startet zuerst die neue Version, die alte wird erst danach beendet, deshalb gibt es keine Ausfallzeit.
  • Cilium: Netzwerk im Cluster (CNI) auf Basis von eBPF
  • Traefik: Ingress-Controller, nimmt die Anfragen aus dem Internet entgegen und leitet sie an die Website weiter
  • cert-manager: Operator, der die TLS-Zertifikate von Let's Encrypt automatisch ausstellt und erneuert
  • Flux: GitOps-Operator, der den Cluster laufend mit dem Stand in Git abgleicht
  • nginx: Webserver im Container, ohne Root-Rechte und mit schreibgeschütztem Dateisystem
  • GitHub Actions und GitHub Container Registry: CI/CD-Pipeline und Ablage der Container-Images
  • Umami: Besucherstatistik im selben Cluster. Ganz blind will ich nicht unterwegs sein: Ich sehe so, welche Seiten gelesen werden und ob der Nerd-Mode verwendet wird.

Alles, was im Cluster läuft, ist Open Source und wird von großen Communities gepflegt. Kubernetes, Cilium, Flux und cert-manager sind Projekte der Cloud Native Computing Foundation (CNCF). Nur für Pipeline und Image-Ablage nutze ich GitHub als Dienst.

Auf vielen Websites muss man zuerst einen Banner wegklicken. Der Grund: Sie binden Dienste anderer Anbieter ein, die beim Besuch Daten erhalten oder Cookies setzen.

TYPISCHE WEBSITEDIESE WEBSITEBrowserbeispiel.deGoogle FontsAnalyticsYouTubeFacebook-PixelBrowsermarcfischer.deServer in DEHTTPSjeder Anbieter erfährt vom Besucheine Adresse, keine Cookies

Typische Beispiele:

  • Schriften von Google Fonts: Schon das Laden einer Schrift überträgt die IP-Adresse an Google. Das Landgericht München I hat das 2022 als Datenschutzverstoß gewertet.
  • Google Analytics und ähnliche Werkzeuge: setzen Cookies und übermitteln Daten an Server in den USA.
  • Eingebettete Videos, Karten oder Social-Media-Buttons: Der Anbieter erfährt vom Besuch, oft schon, bevor man etwas anklickt.
  • Viele Cookie-Banner selbst laden Skripte von Drittanbietern.

Auf dieser Website gilt dagegen:

  • Logfiles und Statistik: keine IP-Adressen, weder in den Logfiles des Webservers noch in der Besucherstatistik.
  • Standort: Website und Statistik (Umami) laufen gemeinsam auf meinem Server in Deutschland, Besucherdaten verlassen den Cluster nicht.
  • Externe Quellen: keine. Ihr Browser spricht nur mit marcfischer.de. Schriften, Bilder und Skripte kommen alle von hier.
  • Cookies: keine. Gespeichert wird nur, was Sie selbst auswählen, etwa der Nerd-Mode, und das bleibt in Ihrem Browser.

Weil nichts gespeichert wird, dem Sie zustimmen müssten, und keine Daten an Dritte gehen, braucht diese Website keinen Cookie-Banner. Details stehen in der Datenschutzerklärung. Dass es auch ohne Banner gehen kann, sieht die EU-Kommission ähnlich: Sie hat vorgeschlagen, die Datenschutz-Einstellung einmal im Browser festzulegen statt auf jeder Website. Die Initiative Kill the Cookie Banner setzt sich dafür ein.

Selbst nachprüfen: Webbkoll zeigt Cookies und Anfragen an Drittanbieter, Blacklight sucht nach Trackern und Fingerprinting.

Sicherheit

A+Qualys SSL Labs
Certificate
100
Protocol Support
100
Key Exchange
90
Cipher Strength
90
Stand: November 2026 · Aktuellen Bericht prüfen

Warum nicht 100/100? Dafür müsste ich den Post-Quantum Key Exchange abschalten und bei TLS 1.3 auf AES-128 verzichten, was Traefik gar nicht zulässt. Beides ergibt für mich keinen Sinn, also bleibt es bei 90.

  • Eine strenge Content Security Policy erlaubt nur eigene Skripte, dazu HSTS und weitere Sicherheits-Header.
  • Verschlüsselung nur mit TLS 1.2 und 1.3, ausschließlich moderne, authentifizierte Verfahren (AES-GCM und ChaCha20-Poly1305) mit Forward Secrecy.
  • Bei TLS 1.3 ist der Key Exchange post-quantum-sicher (hybrid X25519 mit ML-KEM-768). Aufgezeichneter Datenverkehr lässt sich damit auch mit künftigen Quantencomputern nicht nachträglich entschlüsseln.
  • Forward Secrecy wird von Qualys als „robust“ bewertet, ein Schutz gegen Downgrade-Angriffe ist aktiv, und bekannte Schwachstellen wie Heartbleed, POODLE oder ROBOT treffen nicht zu.
  • Die Zertifikate von Let’s Encrypt nutzen kompakte ECDSA-Schlüssel (P-256), werden automatisch erneuert und sind öffentlich in den Certificate-Transparency-Logs eingetragen. Ein CAA-Eintrag im DNS legt fest, dass nur Let’s Encrypt Zertifikate für diese Domain ausstellen darf. HSTS weist Browser an, die Seite ein Jahr lang nur verschlüsselt aufzurufen.

Selbst nachprüfen: Das Mozilla HTTP Observatory bewertet die Sicherheits-Header, derzeit mit A+. Der TLS-Bericht von Qualys SSL Labs steht unter der Grafik.

Schnell und schlank

Die Startseite wiegt rund 140 KB, inklusive Schriften und Foto. Es gibt kein JavaScript-Framework, nur ein paar Zeilen eigenes Skript für das mobile Menü und die Extras weiter unten. Google PageSpeed bewertet die Seite in allen Kategorien mit 100 Punkten. Wer auch schlanke Websites mag: Der 512KB Club sammelt Seiten, die mit weniger als 512 KB auskommen.

Für Nerds

  • Nerd-Mode: Ein Klick auf >_ oben rechts schaltet in einen Terminal-Look, inklusive Boot-Sequenz.
  • Befehlszeile: Im Nerd-Mode öffnet : eine Befehlszeile wie in Vim. :help zeigt, was möglich ist.
  • Im Terminal: curl marcfischer.de liefert einen kurzen Lebenslauf als farbigen Text.
  • Für KI-Suchen: Unter /llms.txt steht eine kompakte Zusammenfassung dieser Website.

So erkennt nginx, ob die Startseite ein Mensch mit Browser oder jemand im Terminal aufruft:

map $http_user_agent $cli_client {
  default 0;
  "~*^(curl|wget|httpie|xh)/" 1;
}

Inhalte und KI

Die Inhalte dieser Website stammen von mir. Bei der englischen Übersetzung und beim Feinschliff einzelner Formulierungen habe ich KI-Werkzeuge unterstützend eingesetzt. Alle Texte habe ich selbst geprüft und verantworte sie.

Die Schriften Geist und Geist Mono stammen von Vercel (SIL Open Font License), die Logos der Projekte von Simple Icons (CC0). Danke an alle, die daran mitarbeiten.