The technology behind this 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
- Pages
- 13
- CSS/JS
- 30.5/5.2 KB
- Fonts
- 63 KB
- Cookies
- 0
Above all, this website is meant to be fast and uncluttered, so the content comes first. Under the hood, there is quite a bit more to it. I write the texts in Markdown and keep them in Git. With every change, a pipeline rebuilds the site with Hugo, and Flux rolls it out automatically to my Kubernetes cluster in Germany. I never have to touch a server for this. It is also a playground where I try out technology I work with professionally. This page explains how it all fits together.
Overview
The diagram shows which parts work together when I publish a change and you open the page. The numbers are explained in the list below.
- Push: I push a change to GitHub. GitHub Actions builds the website with Hugo and stores the container image in the GitHub Container Registry.
- Deployment: Flux in the cluster notices the new version in the deploy branch and swaps the nginx container.
- Certificate: cert-manager gets the TLS certificates from Let’s Encrypt and renews them automatically.
- Request: your browser connects to Traefik over an encrypted connection, and Traefik passes the request on to nginx.
From text to website
I never write HTML by hand. Texts are in Markdown, the CV and trainings are data in YAML, and Hugo fills both into the templates. The CV comes from the same data, and the trainings come from a repository of their own.
Where the data comes from
I keep my CV, projects, certifications and skills in exactly one place, a separate Git repository. It produces the PDF CV (typeset with Typst, in German and English, detailed and short) and the data for this website. When I earn a new certification on Credly, an automation proposes it to me as a change.
The trainings I deliver aren’t maintained by hand either:
- From the e-mail: an n8n automation reads the training centres’ order confirmations and stores each training as a small file in a private Git repository.
- What counts: a training only counts once it has actually taken place and is completed. Planned or cancelled dates never reach the website.
- Review and approval: every change is checked for plausibility automatically. Then a pull request for this website is opened, which I review and approve.
So the figures on the website, in the PDF and in my own overview always match.
Hugo
Hugo is a static site generator, open source and written in Go: a single binary with no further dependencies.
- Principle: Hugo reads the Markdown files and the YAML data, fills them into templates and writes finished HTML pages. That happens once at build time, not on every request.
- Build time: the whole website in both languages builds in about 0.2 seconds. Locally I see every change in the browser straight away.
- Languages: German and English come from the same templates, the texts live in translation files.
- During the build: CSS and JavaScript are bundled, minified and get a hash in the file name. The sitemap,
llms.txtand the curl version are generated along the way.
A simplified example. The Markdown file contains only the text:
---
title: "Contact"
---
The best way to reach me is by e-mail.
The template defines the frame of the page and where the title and content go:
<!doctype html>
<html lang="{{ site.Language.Lang }}">
<head>
<title>{{ .Title }} | Marc Fischer</title>
</head>
<body>
<h1>{{ .Title }}</h1>
{{ .Content }}
</body>
</html>
From these, Hugo generates this HTML page during the build:
<!doctype html>
<html lang="en">
<head>
<title>Contact | Marc Fischer</title>
</head>
<body>
<h1>Contact</h1>
<p>The best way to reach me is by e-mail.</p>
</body>
</html>
Deployment with GitOps
The pipeline builds the image and then only writes its new version to a separate deploy branch. Flux in the cluster watches that branch, reconciles the cluster with it every minute and swaps the container with a rolling update, without downtime.
Flux needs only a few lines of YAML for this:
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
So the last step of the pipeline is short:
- 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
Static and stateless
This website used to run on WordPress, with a database, PHP and plugins. Today it is purely static, with Hugo generating every page in advance. That has real advantages:
- Security: no database, no login, no code running on the server when a page is requested, and no plugins that constantly need updates.
- Delivery: the server only hands out files, it has nothing to compute.
- Versioning: every change lives in Git and can be traced or rolled back.
The website is also stateless in the cluster: each version is entirely contained in the container image, which is rebuilt on every change. So it needs no persistent volume and no CSI. For an update, Kubernetes simply starts a new container, and a restart loses nothing. The only persistent data belongs to the visitor statistics, which have a small database of their own.
Platform
The website runs on my own Kubernetes cluster, hosted by Hetzner in Germany. The building blocks and what they do:
- Talos Linux: a minimal, immutable operating system built only for Kubernetes, without SSH access
- Kubernetes: runs the website on a single node. On an update the new version starts first and the old one is stopped only afterwards, so there is no downtime.
- Cilium: networking inside the cluster (CNI), based on eBPF
- Traefik: ingress controller, takes the requests from the internet and routes them to the website
- cert-manager: operator that issues and renews the TLS certificates from Let's Encrypt automatically
- Flux: GitOps operator that keeps the cluster in line with the state in Git
- nginx: web server in a container, without root privileges and with a read-only file system
- GitHub Actions and GitHub Container Registry: CI/CD pipeline and storage for the container images
- Umami: visitor statistics in the same cluster. I do not want to fly completely blind: this way I can see which pages are read and whether nerd mode is used.
Everything running in the cluster is open source and maintained by large communities. Kubernetes, Cilium, Flux and cert-manager are projects of the Cloud Native Computing Foundation (CNCF). Only the pipeline and the image registry use GitHub as a service.
Why there is no cookie banner here
On many websites you first have to click away a banner. The reason: they embed services from other providers that receive data or set cookies when you visit.
Typical examples:
- Google Fonts: merely loading a font sends your IP address to Google. A German court (Munich Regional Court I) ruled this a data protection violation in 2022.
- Google Analytics and similar tools: set cookies and transfer data to servers in the USA.
- Embedded videos, maps or social media buttons: the provider learns about your visit, often before you click anything.
- Many cookie banners themselves load scripts from third parties.
On this website, by contrast:
- Logs and statistics: no IP addresses, neither in the web server logs nor in the visitor statistics.
- Location: the website and the statistics (Umami) run together on my server in Germany, visitor data never leaves the cluster.
- External sources: none. Your browser only talks to marcfischer.de. Fonts, images and scripts are all served from here.
- Cookies: none. The only thing stored is what you choose yourself, such as nerd mode, and it stays in your browser.
Since nothing is stored that would need your consent and no data goes to third parties, this website needs no cookie banner. Details are in the privacy policy (German). The EU Commission also sees a way without banners: it has proposed setting your privacy preference once in the browser instead of on every website. The initiative Kill the Cookie Banner campaigns for it.
Check it yourself: Webbkoll shows cookies and third-party requests, Blacklight looks for trackers and fingerprinting.
Security
- Certificate
- 100
- Protocol Support
- 100
- Key Exchange
- 90
- Cipher Strength
- 90
Why not 100/100? For that I would have to switch off the post-quantum key exchange and drop AES-128 for TLS 1.3, which Traefik doesn’t even allow. Neither makes sense to me, so 90 it is.
- A strict Content Security Policy allows only the site’s own scripts, plus HSTS and other security headers.
- Encryption only with TLS 1.2 and 1.3, using only modern authenticated ciphers (AES-GCM and ChaCha20-Poly1305) with forward secrecy.
- With TLS 1.3 the key exchange is post-quantum secure (hybrid X25519 with ML-KEM-768), so recorded traffic cannot be decrypted later, not even by future quantum computers.
- Qualys rates forward secrecy as “robust”, downgrade protection is active, and known vulnerabilities such as Heartbleed, POODLE or ROBOT do not apply.
- The Let’s Encrypt certificates use compact ECDSA keys (P-256), renew automatically and are publicly recorded in the Certificate Transparency logs. A CAA record in DNS states that only Let’s Encrypt may issue certificates for this domain. HSTS tells browsers to use only encrypted connections for a year.
Check it yourself: the Mozilla HTTP Observatory rates the security headers, currently A+. The Qualys SSL Labs TLS report is linked below the chart.
Fast and lean
The home page weighs around 140 KB, fonts and photo included. There is no JavaScript framework, only a few lines of my own script for the mobile menu and the extras below. Google PageSpeed rates the site 100 in every category. If you like lean websites too: the 512KB Club collects sites that get by with less than 512 KB.
For nerds
- Nerd mode: a click on
>_at the top right switches to a terminal look, boot sequence included. - Command line: in nerd mode,
:opens a Vim-style command line.:helpshows what it can do. - In a terminal:
curl marcfischer.de/en/returns a short CV as coloured text. - For AI search: /en/llms.txt holds a compact summary of this website.
This is how nginx tells a person in a browser from someone in a terminal:
map $http_user_agent $cli_client {
default 0;
"~*^(curl|wget|httpie|xh)/" 1;
}
Content and AI
The content of this website is my own. I used AI tools to help with the English translation and to polish some of the wording. I have reviewed every text myself and take responsibility for it.
The Geist and Geist Mono typefaces are by Vercel (SIL Open Font License), the project logos come from Simple Icons (CC0). Thanks to everyone who works on them.